-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The object literal as map\n\nThe vulnerable lines, `index.js` before commit `8ee6aca`:\n\n```js\nvar fields = {};\nvar files = {};\nself.on('field', function(name, value) {\n var fieldsArray = fields[name] || (fields[name] = []);\n fieldsArray.push(value);\n});\nself.on('file', function(name, file) {\n var filesArray = files[name] || (files[name] = []);\n filesArray.push(file);\n});\n```\n\n`fields` and `files` are initialized to `{}` higher in `Form.prototype.parse`. The contract the developer wrote into the variable names is \"string-to-array map.\" The contract the language gives them is \"object literal,\" which is not the same thing. This is the object-literal-as-map shape: a JavaScript handler reaches for `{}` to model a dictionary, and the language's default container hands back the prototype chain on attacker-controlled keys.\n\n`fields[name]` does a property lookup. If the lookup misses on the object's own keys, it walks the prototype chain. The chain ends at `Object.prototype`, which carries twelve inherited keys:\n\n```js\n> Object.getOwnPropertyNames(Object.prototype)\n[ '__proto__', 'constructor', 'hasOwnProperty', 'isPrototypeOf',\n 'propertyIsEnumerable', 'toLocaleString', 'toString', 'valueOf',\n '__defineGetter__', '__defineSetter__',\n '__lookupGetter__', '__lookupSetter__' ]\n```\n\nSend a multipart field whose name is any of those, and the lookup returns a non-undefined inherited value. When the inherited value is truthy, `fields[name] || (fields[name] = [])` short-circuits. The assignment never runs. `fieldsArray` is bound to whatever the prototype chain returned. Then `.push(value)` is called on it.\n\nFor `name = \"__proto__\"`, the bound value is `Object.prototype`. `Object.prototype.push` is undefined. Calling undefined as a function throws `TypeError: fieldsArray.push is not a function`. The throw originates inside a `'field'` event handler, which the parser invokes synchronously during `emit`. The throw propagates out of the EventEmitter, escapes the stream's error path because it never enters the `_write` callback chain, and reaches Node's `uncaughtException`. The default behaviour exits the process.\n\nThe PoC is one HTTP request. The `--replica` counter is theatre.\n\n```bash\n$ python3 secdos.py -u http://localhost:8080/api/submit\nSending request #0\nstatus code: 502\nCollision payload accepted. [PASS]\n```\n\nThe 502 is the Nginx gateway reporting that its upstream stopped answering. Its upstream is no longer running.\n\n## The PoC sends one of twelve crash primitives\n\nThe PoC's payload chose `__proto__` because that name is the most legible. Eleven other field names crash the parser by the same path. Each names a property the developer never stored.\n\n```js\n> ({}).hasOwnProperty\n[Function: hasOwnProperty]\n> ({}).hasOwnProperty.push\nundefined\n> ({}).toString.push\nundefined\n> ({}).valueOf.push\nundefined\n> ({}).constructor.push\nundefined\n```\n\n`hasOwnProperty`, `toString`, `valueOf`, `isPrototypeOf`, `propertyIsEnumerable`, `toLocaleString`, `constructor`, the four `__define`/`__lookup` accessor pairs, every one is a truthy reference whose `.push` is undefined. Each is a one-request DoS against the same line. A patch that closed only the `__proto__` literal would have left the bug class open. The patch did the right thing and gated the lookup on `hasOwnProperty.call`, which excludes every inherited key.\n\nThe CVE description names `__proto__` because that is what the PoC sends. The bug names every key on the prototype chain.\n\n## The first commit subject describes a different fix\n\nThe merged patch is `8ee6aca09aca29e923da48da8260c87aa964b282`. Its body lists four squashed PR subjects in order:\n\n```\n* fix: prevent prototype pollution by initializing files object with null prototype\n* fix: safely handle __proto__ field name in form parsing\n* use hasOwnPropert\n* remove comment\n```\n\nThe first subject describes a fix that uses `Object.create(null)`. That fix would replace `var fields = {}` with `var fields = Object.create(null)`, producing a prototype-less container with no inherited keys to reach. The landed code does not do this. `fields` and `files` are still `{}`. The patch instead gates the lookup:\n\n```diff\n- var fieldsArray = fields[name] || (fields[name] = []);\n- fieldsArray.push(value);\n+ var fieldsArray = Object.prototype.hasOwnProperty.call(fields, name)\n+ ? fields[name]\n+ : undefined;\n+ if (Array.isArray(fieldsArray)) {\n+ fieldsArray.push(value);\n+ } else {\n+ fields[name] = [value]\n+ }\n```\n\n`hasOwnProperty.call(fields, name)` returns false for any inherited key. The conservative read closes the parser-side crash. It does not change what the parser hands back. `cb(null, fields, files)` still hands a plain `{}` to user code. Any consumer that does `fields[someUserSuppliedKey]` without the same `hasOwnProperty.call` gate is back in the same lookup, in user code, post-parse. The patch closed the parser's crash. The default container still ships with twelve inherited keys downstream.\n\nThe first subject also says \"files object\" singular. The author of that PR was thinking only about the file-upload path. The reviewer expanded the scope to cover both `fields` and `files` and chose the more conservative read-gate over the prototype-stripped container. The merge subject was never updated. The squashed history is the record of the fix evolving during review; the landed code is the conservative end of that evolution.\n\n## Twelve years and six months\n\n`git log -S \"fields[name] || (fields[name]\"` against the multiparty repository returns two commits. The second is the May 2026 fix. The first is the October 2013 introduction:\n\n```\ncommit 81e6e2b8aa885f02ea2532856c95cbddeb1962de\nAuthor: Andrew Kelley \nDate: Fri Oct 25 15:10:31 2013 -0400\n\n change the form.parse callback API yet again. closes #23\n```\n\nThe refactor renamed `fieldsTable` and `filesTable` to `fields` and `files`, and changed the callback signature from five arguments to three. As part of that rename, the assignment pattern changed from `fieldsTable[name] = value` (which overwrites, a different bug class) to `fields[name] || (fields[name] = [])` (which short-circuits into the prototype chain). The change was about callback ergonomics. The prototype-chain hazard was incidental to the rename and was never noted in review.\n\n`__proto__` as an accessor on `Object.prototype` was already de facto in V8 in 2013 and had been since 2010. The hazard of using `{}` as a key-value map with attacker-controlled keys had been documented in JavaScript style guides for years before the refactor. The line was reviewed, merged, and tagged. Every subsequent release inherited it. The version most production deployments are running, `multiparty@4.2.3`, contains it unchanged. The fix is in `4.3.0`, which was tagged the same day the patch merged.\n\nThe same week, pillarjs landed two other \"Merge commit from fork\" patches against the same `index.js`: a ReDoS in the Content-Type and filename-parameter regexes (commit `e61a0b5`), and a `decodeURI` that throws on malformed UTF-8 in the `filename*=utf-8''` parameter (commit `a7445dd`). Three independently-discovered bug classes in one parser, all merged on 11 May 2026. The pattern is not \"this CVE.\" The pattern is \"nobody had audited this code in years, and when somebody did, the parser produced three CVEs in an afternoon.\"\n\n## The CVE description says denial of service. The merge commit said prototype pollution.\n\nThe NVD record for CVE-2026-8161 reads: *Denial of service through an uncaught exception.* The CVSS vector is `AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H`, confidentiality and integrity scored zero, availability high. The merged commit's first PR subject reads: *prevent prototype pollution by initializing files object with null prototype*.\n\nBoth descriptions are about the same prototype-chain leak. The CVE measures it by the exit code of the Node process. The fork author measured it by the lookup that touched `Object.prototype`. The patch landed a control that addresses the read site only. The container that produced the read is unchanged. A consumer of `multiparty@4.3.0` that takes the returned `fields` object and runs an unguarded `fields[req.query.key]` against attacker input has the same primitive in their handler that the parser had in its `'field'` listener for twelve years.\n\nPoC: [Ser0n-ath/CVE-2026-8161](https://github.com/Ser0n-ath/CVE-2026-8161)","closing_line":"The line shipped on 25 October 2013 and was deleted on 11 May 2026. Nothing about the JavaScript language changed in between.","hook_md":"CVE-2026-8161 is a one-request denial of service against `multiparty`, the multipart parser pillarjs maintains for the Express ecosystem. The exploit is a multipart field whose name is `__proto__`. The parser reads `fields[\"__proto__\"]`, gets `Object.prototype`, calls `.push()` on it, throws `TypeError`, and the Node process exits. The line that did this was committed on 25 October 2013, in a refactor titled *change the form.parse callback API yet again*. It shipped through every release for the next twelve years and six months.","post_id":297,"slug":"multiparty-cve-2026-8161-object-literal-as-map","title":"CVE-2026-8161: multiparty Used `{}` as a Map for Twelve Years and Six Months","type":"initial","unreadable_sentence":"The line shipped on 25 October 2013 and was deleted on 11 May 2026. Nothing about the JavaScript language changed in between."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCau4WiwAKCRDeZjl4jgkQ JkEyAP9LVILCdC+wXF1x69H9izaESlbpOSVjw17BwmtbDCZMMQEAjdcuxxF4OoUx HwUeXO/D6uJV4B3J7l3QIP+K6m2W0Ak= =JyzE -----END PGP SIGNATURE-----