Severity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
Library is typically reached via network-fed records with no auth or interaction; integrity is high (arbitrary prototype writes), with low availability impact from likely logic crashes and no direct confidentiality loss.
Primary rating from Vendor (https://github.com/joaonuno/flat-to-nested-js).
CVSS VectorVendor: https://github.com/joaonuno/flat-to-nested-js
Lifecycle Timeline
2DescriptionCVE.org
Summary
convert() builds the nested tree by using each flat record's id and parent field values directly as object keys, with no guard against __proto__ / constructor / prototype. A record whose parent is the string "__proto__" makes temp[parent] resolve to Object.prototype, and the following initPush(...) writes attacker-controlled data onto the global prototype. Any application that passes attacker-influenced records to convert() is affected, and the base prototype methods stay intact so the pollution is stealthy.
Details
In index.js, convert() (FlatToNested.prototype.convert):
temp = {}(line 45) andpendingChildOf = {}(line 46) are plain objects, so they inherit fromObject.prototype.- For each record,
parent = flatEl[this.config.parent](line 51) is taken verbatim from input. - Line 57:
if (temp[parent] ! undefined)- whenparent = "__proto__",temp["__proto__"]resolves via the prototype chain toObject.prototype, which is!== undefined, so the
branch is taken.
- Line 59:
initPush(this.config.children, temp[parent], flatEl)→ effectivelyinitPush("children", Object.prototype, flatEl). initPush(lines 4-9):Object.prototype["children"] = []thenObject.prototype["children"].push(flatEl)- attacker-controlled data is written onto the globalObject.prototype.
There is no sanitization of id / parent anywhere; they flow straight into temp[id], temp[parent], and pendingChildOf[parent] as dynamic keys.
PoC
const FlatToNested = require('flat-to-nested');
new FlatToNested().convert([
{ id: 1, parent: '__proto__', polluted: 'PWNED' }
]);
console.log(({}).children); // => [ { id: 1, polluted: 'PWNED' } ]
A freshly-created, unrelated object {} now carries an attacker-controlled children property. ({}).toString === Object.prototype.toString remains true, so existing methods are untouched (stealthy). If the consumer configures a custom children key, that arbitrary prototype property is polluted instead.Impact
Prototype pollution (CWE-1321). Any service that builds a tree from attacker-influenced flat records (the package's core purpose - e.g. records derived from a DB/REST/user input) can have Object.prototype polluted. Consequences range from application-logic corruption and denial of service to serving as a gadget toward privilege escalation or RCE depending on downstream sinks. No special privileges or user interaction required; the malicious value is ordinary input data.
Suggested fix
Use prototype-less lookup tables so inherited keys like __proto__ cannot be reached: var temp = Object.create(null); var pendingChildOf = Object.create(null); (Optionally also reject id/parent values equal to __proto__, constructor, or prototype.) Verified: with Object.create(null) for both temp and pendingChildOf, the PoC no longer pollutes Object.prototype and normal nesting output is unchanged. A patch with a regression test is ready.
AnalysisAI
Prototype pollution in the npm package flat-to-nested (versions <= 1.1.1) lets attackers who control input records pollute Object.prototype by supplying a record with parent set to the string '__proto__'. The convert() function uses plain objects as lookup tables and writes attacker-controlled data through the prototype chain via initPush(), affecting any application that feeds untrusted flat records (REST input, DB rows, user-supplied data) into the library. No CISA KEV listing and no public exploit identified at time of analysis beyond the proof-of-concept embedded in the GitHub Security Advisory.
Technical ContextAI
flat-to-nested is a small JavaScript utility (pkg:npm/flat-to-nested) that converts flat parent/child records into a nested tree. In index.js, convert() initializes temp = {} and pendingChildOf = {} as plain objects that inherit from Object.prototype, then uses each record's id and parent fields directly as dynamic object keys without any allowlist or denylist. When parent === '__proto__', temp['__proto__'] resolves to Object.prototype and initPush() writes children data onto the global prototype. The root cause class is CWE-1321 / CWE-915 (improperly controlled modification of object prototype attributes), a well-known JavaScript sink-pattern where unsanitized strings used as keys cross the boundary between own properties and inherited prototype properties.
RemediationAI
Vendor-released patch: upgrade flat-to-nested to 1.1.2 or later, which replaces the plain-object lookup tables with Object.create(null) so that '__proto__', 'constructor', and 'prototype' become harmless own keys rather than prototype-chain references (commit 680a5ebe1194edda16fa93baaa56ff14fe0e3d7f, advisory GHSA-hp36-v28f-w3r4). If upgrading is not immediately possible, pre-sanitize input records before calling convert() by rejecting or stripping any record whose id or parent equals '__proto__', 'constructor', or 'prototype' - note this is a strict allowlist on key strings and will reject any legitimate record that happens to use those exact identifiers. As an additional defense, run the consumer process under Node's --frozen-intrinsics flag or call Object.freeze(Object.prototype) at startup; this blocks the write but will break any library that legitimately augments Object.prototype, so test thoroughly before enabling in production.
Same technique Privilege Escalation
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-77691
GHSA-hp36-v28f-w3r4