Skip to content

fix(security): defense-in-depth against already-polluted Object.prototype in formDataToJSON#7413

Merged
jasonsaayman merged 4 commits into
axios:v1.xfrom
tommyhgunz14:fix/extend-prototype-pollution-protection
May 4, 2026
Merged

fix(security): defense-in-depth against already-polluted Object.prototype in formDataToJSON#7413
jasonsaayman merged 4 commits into
axios:v1.xfrom
tommyhgunz14:fix/extend-prototype-pollution-protection

Conversation

@tommyhgunz14

@tommyhgunz14 tommyhgunz14 commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

Summary
Replace the falsy check with hasOwnProp in the intermediate-path branch of formDataToJSON's buildPath to prevent write-through into inherited objects.

Problem
If Object.prototype is already polluted (e.g. via a third-party library or an earlier vulnerability), user-supplied FormData paths like injected.hijack can traverse the inherited object and mutate Object.prototype in place. The original check:

if (!target[name] || !utils.isObject(target[name]))
follows the prototype chain — so if Object.prototype.injected exists and is truthy/an object, the code walks into it and writes there.

Fix
if (!utils.hasOwnProp(target, name) || !utils.isObject(target[name]))
With hasOwnProp, the inherited slot is shadowed by a new own property on the result object, keeping writes local. No prototype mutation occurs.

Context
This is defense-in-depth, not a standalone CVE fix. The existing proto guard blocks direct prototype injection; this change prevents exploitation of an already-polluted prototype chain. It does not claim to fix constructor.prototype pollution — that path was already handled by the existing CVE test.

Test
Added a write-through regression test that:

Manually pollutes Object.prototype.injected = { hijack: true }
Sends injected.hijack=STOLEN through formDataToJSON
Asserts the result gets its own { hijack: 'STOLEN' } (not appended to the inherited object)
Asserts Object.prototype.injected.hijack remains true (unmutated)
The test fails on unpatched HEAD and passes with the one-line change.

Closes #7209

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

@tommyhgunz14

Copy link
Copy Markdown
Contributor Author

Hi - this has been outstanding for a while now, should still be good.

@jasonsaayman

Copy link
Copy Markdown
Member

Hi - this has been outstanding for a while now, should still be good.

thanks i will check this out asap

@tommyhgunz14 tommyhgunz14 force-pushed the fix/extend-prototype-pollution-protection branch from 17122a4 to 2f78311 Compare March 23, 2026 12:47
@tommyhgunz14

Copy link
Copy Markdown
Contributor Author

@jasonsaayman I dont think there is anything outstanding on this for me? Didnt hear back from your last comment

@jasonsaayman

jasonsaayman commented Apr 28, 2026

Copy link
Copy Markdown
Member

Thanks for the patch. The code change is the right pattern and worth landing, but a couple of things need fixing before it can go in, and the framing around what it actually fixes needs to change too.

I traced and reproduced both the claimed and actual behaviour against the current v1.x HEAD.

The description says this prevents pollution via constructor.prototype chains. That isn't quite accurate — the existing code already handles that case, because target.constructor returns the function Object, which fails utils.isObject (functions don't pass the typeof === 'object' check), so the slot already gets reset to []. Demonstrated against unpatched HEAD:

input:  constructor.prototype.polluted = 'true'
result: {"constructor":{"prototype":{"polluted":"true"}},"safe":"data"}
{}.polluted === undefined
Object.prototype.polluted === undefined

The new test passes against unpatched code too, so it isn't covering the path your change affects.

The real benefit of the change is defence-in-depth against an already-polluted runtime. If something else has set Object.prototype.injected = { hijack: true }, the unpatched code traverses the prototype chain, calls isObject, and writes the user's value into Object.prototype:

without PR:
  result: {}
  Object.prototype.injected.hijack === [true, "STOLEN"]   // mutated through

with PR:
  result: {"injected":{"hijack":"STOLEN"}}
  Object.prototype.injected.hijack === true               // intact

That's the scenario these guards face, and that's what the test should cover.

A few more things to fix:

  1. The test path is wrong. The PR adds tests at test/specs/helpers/formDataToJSON.spec.js. The repo runs Vitest from tests/unit/helpers/formDataToJSON.test.js; package.json only defines test:vitest. The new test won't run in CI.

  2. The test duplicates existing coverage. tests/unit/helpers/formDataToJSON.test.js:78 already has a should resist prototype pollution CVE block that exercises constructor.prototype.y and asserts {}.y === undefined. The added test is essentially the same, with renamed keys.

  3. Suggested replacement test that actually exercises the guard:

it('should not write through to inherited objects on Object.prototype', () => {
  Object.prototype.injected = { hijack: true };
  try {
    const formData = new FormData();
    formData.append('injected.hijack', 'STOLEN');
    const result = formDataToJSON(formData);

    expect(result.injected).toEqual({ hijack: 'STOLEN' });
    expect(Object.prototype.injected.hijack).toEqual(true);
  } finally {
    delete Object.prototype.injected;
  }
});

This would fail against the current HEAD and pass with your one-line change applied.

Could you fold the fix into tests/unit/helpers/formDataToJSON.test.js, drop the duplicate test, and add the inherited-object write-through case? Once that's in, the code change itself is a clean defence-in-depth improvement.

@jasonsaayman jasonsaayman added priority::medium A medium priority commit::fix The PR is related to a bugfix status::changes-requested A reviewer requested changes to the PR status::needs-rebase The PR requires a rebase labels Apr 29, 2026
Replace falsy check with hasOwnProp in the intermediate-path branch of
formDataToJSON's buildPath to prevent write-through into inherited objects.

Without this patch, if Object.prototype is already polluted (e.g. via a
third-party library or earlier vulnerability), user-supplied FormData
paths like 'injected.hijack' traverse the inherited object and mutate
Object.prototype in place. With hasOwnProp, the inherited slot is
shadowed by a new own property, keeping writes local to the result.

This is defense-in-depth: the existing __proto__ guard blocks direct
prototype injection, while this change prevents exploitation of an
already-polluted prototype chain.

Closes axios#7209
@tommyhgunz14 tommyhgunz14 force-pushed the fix/extend-prototype-pollution-protection branch from 2f78311 to aa3b45a Compare April 30, 2026 16:13
@tommyhgunz14 tommyhgunz14 changed the title fix(security): extend prototype pollution protection in formDataToJSON fix(security): defense-in-depth against already-polluted Object.prototype in formDataToJSON Apr 30, 2026
@tommyhgunz14

Copy link
Copy Markdown
Contributor Author

Hi - thanks for the detailed feedback. Please see below -

Updated:

Rebased onto current v1.x (was 164 commits behind). Branch is now up to date with the test/ → tests/ migration and Vitest.
Dropped the duplicate test that was essentially re-testing constructor.prototype (already covered by the existing CVE test).
Added the write-through regression test you suggested — pollutes Object.prototype.injected, sends injected.hijack through formDataToJSON, and asserts the inherited object isn't mutated. Confirmed it fails on unpatched HEAD and passes with the fix.
Corrected framing: this is defense-in-depth against an already-polluted Object.prototype, not a standalone constructor.prototype fix.
Test file is now in tests/unit/helpers/formDataToJSON.test.js (no leftover under old test/specs/).

@jasonsaayman jasonsaayman merged commit 78e8dcf into axios:v1.x May 4, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commit::fix The PR is related to a bugfix priority::medium A medium priority status::changes-requested A reviewer requested changes to the PR status::needs-rebase The PR requires a rebase

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Improve prototype pollution protection in formDataToJSON.js

2 participants