{"id":"CVE-2026-114143","kind":"vulnerability","aliases":["CVE-2026-114143","GHSA-9j65-hcg3-gc64"],"related":[],"title":"toml-node: Quadratic-complexity DoS via long dotted keys or table paths in toml.parse()","description":"## Summary\n\n`toml.parse()` is vulnerable to a quadratic-complexity (O(n²)) denial-of-service via a single\nlong dotted key, table header, or inline-table key. A ~100KB crafted document causes a\nmulti-second synchronous hang; a ~200KB document crashes the Node.js process outright with an\nuncatchable out-of-memory error. This is a separate, unpatched code path from the previously\nfixed stack-overflow DoS (`GHSA-82x6-q7mm-w9cf`) — it requires no array/inline-table nesting at\nall, and is confirmed unaffected by the `maxDepth` option that fixed that earlier issue.\n\n## Affected component / version\n\n- Package: `toml` (npm), repository `BinaryMuse/toml-node`\n- Functions: `assign()`, `deepRef()`, `setNestedKey()` in `lib/compiler.js`\n- Confirmed present in the audited checkout at commit `1b2ac40` (2026-07-16), one commit past\n  the `v5.0.0` tag (a changelog-only change) — i.e. present in `v5.0.0`, the latest release.\n  Not exhaustively verified against every prior release, but the affected functions are\n  unrelated to `v5.0.0`'s BigInt/Temporal changes (per `CHANGELOG.md`), so the defect likely\n  predates that release as well.\n\n## Details\n\n`toml.parse(input, options)` (`index.js:5-9`) forwards the caller-supplied string to\n`parser.parse()` and then `compiler.compile()` with no length or structural validation.\nInside `lib/compiler.js`, three separate functions each walk a dotted key's segments in a\n`for` loop and, on **every** iteration, recompute the full path-prefix string from scratch:\n\n- `assign()` (lines 69-107, the sink at line 81): `var intermediatePath =\n  makeFullPath(keys.slice(0, i + 1));` — reached for top-level dotted-key assignments like\n  `a.b.c = 1`.\n- `deepRef()` (lines 247-281, the sink at line 252): the identical pattern via\n  `pathKey(keys.slice(0, i + 1))` — reached from table headers `[a.b.c]` and array-of-tables\n  `[[a.b.c]]` via `setPath()`/`addTableArray()`.\n- `setNestedKey()` (lines 184-202, the sink at line 187): the identical pattern — reached from\n  inline-table dotted keys like `t = { a.b.c = 1 }`.\n\n`keys.slice(0, i + 1)` allocates a new array of length `i`; `pathKey()`/`makeFullPath()`\n(lines 311-317) then does `.map(quoteDottedString).join(\".\")`, also O(i). Since this runs on\nevery one of the `n` segments of a dotted key, total work is `O(1 + 2 + ... + n) = O(n²)`, not\n`O(n)`. The string is computed unconditionally on the happy path (only ever *used* if an error\nis about to be thrown), so there is no early exit that makes this cheap in practice.\n\nNo limit exists anywhere on dotted-key segment count: `dot_ended_table_key_part+` and\n`inline_dot_key_part+` in `src/toml.pegjs` (lines 122, 311) are unbounded `+` repetitions,\nparsed iteratively rather than through the grammar's `value` rule — so the existing\n`MAX_DEPTH`/`maxDepth` nesting guard (`src/toml.pegjs:141-151`, enforced in the generated\n`lib/parser.js:1022`) does not apply to key length at all. We confirmed this directly: setting\n`options.maxDepth: 1` (the strictest possible value) against a 16,000-segment key produced no\nmeasurable timing difference from the unrestricted case.\n\n## Proof of Concept\n\n```js\nconst toml = require('toml');\n\n// n-segment dotted key: \"k0.k1.k2. ... .k{n-1} = 1\"\nfunction segKey(n) {\n  const parts = [];\n  for (let i = 0; i < n; i++) parts.push('k' + i);\n  return parts.join('.') + ' = 1\\n';\n}\n\n// ~100.9 KB input -- takes ~6.6-6.8 seconds\ntoml.parse(segKey(16000));\n\n// ~200 KB input -- crashes the Node process:\n// \"FATAL ERROR: Ineffective mark-compacts near heap limit - JavaScript heap out of memory\"\ntoml.parse(segKey(32000));\n```\n\nMeasured timings (Node v22, direct execution against this repo):\n\n| n (segments) | input size | time |\n|---|---|---|\n| 1,000 | ~4.9 KB | 34 ms |\n| 8,000 | ~46.9 KB | 1.6 s |\n| 16,000 | ~100.9 KB | 6.8 s |\n| 32,000 | ~200 KB | **process crash (OOM)** |\n\nDoubling `n` from 8,000→16,000 roughly quadruples runtime (~4.2x), the signature of O(n²)\nrather than the expected O(n) (~2x). Independently reproduced with the identical scaling\nsignature for the table-header variant (`[k0. ... .k15999]`, exercising `deepRef()`) and the\ninline-table variant (`t = { k0. ... .k15999 = 1 }`, exercising `setNestedKey()`).\n\nWe also generalized this into a property-based fuzzing harness (fast-check, run under Node)\nthat automatically tests the \"doubling input size should not more than roughly double parse\ntime\" invariant across 12 structural dimensions of the grammar/compiler; it independently\nre-flags all three code paths above on every run, with no additional dimension confirmed\nvulnerable after follow-up investigation. The harness is available on request.\n\n## Impact\n\nNode.js is single-threaded — a synchronous multi-second-to-crashing `parse()` call blocks the\nentire event loop of the process calling it, stalling every other request/operation that\nprocess is handling, not just the attacker's own. At the upper end this is not merely a hang:\nan uncaught, process-terminating out-of-memory crash from a ~200KB attacker-supplied string is\na full denial of service requiring no authentication — just the ability to get one string\nparsed by an application using this library (config upload, API request body, imported\nmanifest, or any of this library's intended use cases).\n\n## Suggested remediation\n\nBuild the path prefix as an incrementally-extended string as the loop walks each segment\n(append one already-quoted segment per iteration to a running string) instead of re-slicing an\narray and re-joining the entire prefix from scratch on every iteration. For example, in\n`assign()`:\n\n```js\nfunction assign(node) {\n  var keys = node.key;\n  var value = node.value;\n  var off = node.offset;\n  if (!Array.isArray(keys)) keys = [keys];\n  var reduced = reduceValueNode(value);\n\n  var target = context;\n  var pathStr = pathKey(currentPath); // computed once, not per segment\n  for (var i = 0; i < keys.length - 1; i++) {\n    var k = keys[i];\n    var quotedK = quoteDottedString(k);\n    var intermediatePath = pathStr ? pathStr + \".\" + quotedK : quotedK; // O(new segment), not O(i)\n    pathStr = intermediatePath;\n    // ...same conditional logic as today, using intermediatePath\n    target = target[k];\n  }\n  // ...\n}\n```\n\nThe identical treatment applies to `deepRef()` and `setNestedKey()`. As defense-in-depth,\nconsider also capping total key-segment count or overall input size, the same way `maxDepth`\nalready caps nesting.\n\n\n## Credit\n\nDiscovered and reported by SecDim Security Research\nsecdim.com | @secdim | security@secdim.com","summary":"The npm package toml (toml-node) has a quadratic-time parsing path: a single very long dotted key, table header or inline-table key makes toml.parse() block the Node.js event loop for seconds, and about 200KB of input crashes the process with an out-of-memory error. An attacker who can get one crafted string parsed by an application using this library can cause a denial of service without authentication.","severity":"high","cvss":8.7,"epss":0.0029100000392645597,"exploited":false,"exploited_at":null,"products":[{"name":"toml","fixed":["5.0.1"],"affected":"<= 5.0.0","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["config-upload","cwe-407","node.js","npm","package","self-hosted","toml"],"not_affected_if":["default-config","managed-hosting","no-untrusted-toml-input"],"how_to_check":"Run npm ls toml to find the installed version; affected versions are 5.0.0 and below.\nSearch the codebase for require('toml') or import of 'toml' and note where parse() receives untrusted input such as uploads, request bodies or imported manifests.","fix":"Upgrade the toml package to 5.0.1 or later.\nUntil upgraded, limit the size of TOML input accepted from untrusted sources before calling toml.parse().","status":"open","published_at":"2026-10-21T21:17:56+00:00","first_seen_at":"2026-10-23T19:11:31+00:00","triaged_at":"2026-10-23T20:06:39+00:00","updated_at":"2026-10-23T21:57:29+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-23T20:06:39Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade the toml package to 5.0.1 or later.\nUntil upgraded, limit the size of TOML input accepted from untrusted sources before calling toml.parse().","summary":"The npm package toml (toml-node) has a quadratic-time parsing path: a single very long dotted key, table header or inline-table key makes toml.parse() block the Node.js event loop for seconds, and about 200KB of input crashes the process with an out-of-memory error. An attacker who can get one crafted string parsed by an application using this library can cause a denial of service without authentication.","category":"package","who_cares":["npm","node.js","toml","self-hosted","config-upload"],"confidence":0.8,"analyst_note":"","how_to_check":"Run npm ls toml to find the installed version; affected versions are 5.0.0 and below.\nSearch the codebase for require('toml') or import of 'toml' and note where parse() receives untrusted input such as uploads, request bodies or imported manifests.\nunknown","needs_analyst":false,"not_affected_if":["managed-hosting","no-untrusted-toml-input","default-config"],"severity_if_unknown":"high"},"cost_usd":0.000655,"input_tokens":4579,"output_tokens":395,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-07':234C '-1':605C '-107':338C '-114143':19A,27A '-151':548C '-16':235C '-202':398C '-2026':18A,26A '-281':366C '-317':433C '-407':33B '-6.8':631C '-9':288C '0':348C,378C,419C,615C '000':577C,675C,681C,687C,693C,703C,705C '1':350C,362C,380C,417C,421C,460C,569C,606C,624C,674C '1.6':684C '100.9':626C,688C '100kb':144C '1022':554C '122':523C '141':547C '16':576C,686C,704C '16000':635C '184':397C '187':403C '1b2ac40':232C '2':461C '200':636C,694C '200kb':79B,155C '2026':233C '247':365C '252':371C '2x':722C '311':432C,524C '32':692C '32000':659C '34':678C '4.2':709C '4.9':676C '46.9':682C '6.6':630C '6.8':690C '69':337C '8':680C,702C '81':343C '82x6':187C '9j65':22A 'a.b.c':361C,385C,391C,416C 'addtablearray':394C 'affect':210C,263C 'alloc':422C 'also':439C 'anywher':503C 'appli':557C 'applic':103B 'array':388C,425C 'array-of-t':387C 'array/inline-table':193C 'assign':219C,335C,359C 'attack':93B 'audit':228C 'authent':114B 'bigint/temporal':270C 'binarymuse/toml-node':217C 'block':70B 'caller':292C 'caller-suppli':291C 'case':588C 'caus':108B,147C 'chang':246C,271C 'changelog':244C 'changelog-on':243C 'changelog.md':273C 'cheap':497C 'checkout':229C 'code':175C 'commit':231C,237C 'compact':648C 'compiler.compile':299C 'complex':6A,123C 'compon':211C 'comput':471C 'concept':591C 'config':30B 'config-upload':29B 'confirm':199C,224C,564C 'const':593C,610C 'count':509C 'craft':98B,145C 'crash':82B,157C,639C,697C 'cve':17A,25A 'cwe':32B 'deepref':220C,363C 'defect':276C 'denial':110B,127C 'denial-of-servic':126C 'depth':542C 'detail':283C 'differ':584C 'direct':566C,664C 'document':146C,156C 'dos':7A,184C 'dot':10A,59B,134C,314C,357C,412C,454C,506C,510C,517C,600C 'dotted-key':356C,505C 'doubl':699C 'earli':492C 'earlier':208C 'end':511C 'enforc':549C 'error':91B,169C,482C,644C 'event':73B 'ever':478C 'everi':258C,324C,446C 'execut':665C 'exhaust':255C 'exist':502C,540C 'exit':493C 'expect':719C 'fatal':643C 'fix':180C,206C 'forward':289C 'full':328C 'function':218C,264C,310C,607C 'gc64':24A 'generat':552C 'get':96B 'ghsa':21A,186C 'ghsa-82x6-q7mm-w9cf':185C 'ghsa-9j65-hcg3-gc64':20A 'grammar':534C 'guard':545C 'hang':153C 'happi':475C 'hcg3':23A 'header':62B,137C,384C 'heap':650C,653C 'host':39B 'i.e':247C 'ident':373C,405C 'independ':723C 'index.js:5':287C 'ineffect':645C 'inlin':65B,140C,410C,516C 'inline-t':64B,139C,409C 'input':81B,285C,628C,638C,671C 'insid':306C 'intermediatepath':345C 'issu':209C 'iter':325C,529C 'javascript':652C 'join':438C 'js':592C 'k':603C,620C 'k0.k1.k2':602C 'kb':627C,637C,677C,683C,689C,695C 'key':11A,60B,67B,135C,142C,315C,358C,413C,455C,507C,513C,518C,559C,579C,601C 'keys.slice':347C,377C,418C 'latest':252C 'length':302C,427C,560C 'let':613C 'level':355C 'lib/compiler.js':223C,307C 'lib/parser.js':553C 'librari':106B 'like':277C,360C,414C 'limit':501C,651C 'line':336C,342C,364C,370C,396C,402C,431C,522C 'long':9A,58B,133C 'loop':74B,321C 'make':68B,495C 'makefullpath':346C,430C 'map':436C 'mark':647C 'mark-compact':646C 'max':541C 'maxdepth':203C,543C 'measur':582C,660C 'memori':90B,168C,656C 'ms':679C 'multi':150C 'multi-second':149C 'n':125C,450C,462C,464C,467C,598C,604C,609C,617C,625C,669C,700C,715C,721C 'n-segment':597C 'near':649C 'nest':194C,544C 'new':424C 'node':3A,47B,641C,662C 'node.js':34B,72B,159C 'npm':35B,42B,215C 'o':124C,440C,459C,463C,466C,714C,720C 'one':97B,236C,447C 'oom':698C 'option':204C,286C 'options.maxdepth':568C 'out-of-memori':87B,165C 'outright':161C 'overflow':183C 'packag':36B,43B,213C 'pars':53B,100B,528C 'parser.parse':296C 'part':514C,519C,611C 'parts.join':623C 'parts.push':619C 'past':238C 'path':14A,54B,176C,330C,476C 'path-prefix':329C 'pathkey':376C,429C 'pattern':374C,406C 'per':272C 'possibl':572C 'practic':499C 'predat':278C 'prefix':331C 'present':225C,248C 'previous':179C 'prior':259C 'process':84B,160C,642C,696C 'produc':580C 'proof':589C 'q7mm':188C 'quadrat':5A,51B,122C 'quadratic-complex':4A,121C 'quadratic-tim':50B 'quadrupl':707C 'quotedottedstr':437C 'rather':530C,716C 'reach':351C,381C,407C 'recomput':326C 'releas':253C,260C,280C 'repetit':527C 'repo':668C 'repositori':216C 'reproduc':724C 'requir':191C,595C 'return':622C 'rough':706C 'rule':537C 'run':444C 'runtim':708C 'scratch':334C 'second':76B,151C,632C 'segkey':608C,634C,658C 'segment':317C,451C,508C,578C,599C,670C 'self':38B 'self-host':37B 'separ':173C,309C 'servic':112B,129C 'set':567C 'setnestedkey':221C,395C 'setpath':393C 'signatur':712C 'sinc':442C 'singl':56B,132C 'sink':340C,368C,400C 'size':672C 'src/toml.pegjs':521C,546C 'stack':182C 'stack-overflow':181C 'strictest':571C 'string':99B,294C,332C,469C 'structur':304C 'summari':115C 'suppli':293C 'synchron':152C 'tabl':13A,61B,66B,136C,141C,383C,390C,411C,512C 'tag':241C 'take':629C 'three':308C 'thrown':487C 'time':52B,583C,661C,673C 'toml':2A,28A,40B,44B,46B,214C,594C,596C 'toml-nod':1A,45B 'toml.parse':16A,69B,116C,284C,633C,657C 'top':354C 'top-level':353C 'total':456C 'unaffect':200C 'unbound':526C 'uncatch':164C 'uncondit':472C 'unpatch':174C 'unrel':266C 'unrestrict':587C 'upload':31B 'use':104B,479C 'v22':663C 'v5.0.0':240C,250C,268C 'valid':305C 'valu':536C,573C 'var':344C 'verifi':256C 'version':212C 'via':8A,130C,375C,392C 'vulner':118C 'w9cf':189C 'walk':312C 'well':282C 'without':113B 'work':457C 'x':710C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-21T21:17:56+00:00","kev":null,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/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_by":"security-advisories@github.com","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-9j65-hcg3-gc64","url":"https://github.com/advisories/GHSA-9j65-hcg3-gc64","severity":"high","published_at":"2026-10-22T16:38:06+00:00","modified_at":"2026-10-22T16:38:06+00:00","first_seen_at":"2026-10-23T19:11:31+00:00","fetched_at":"2026-10-23T19:11:31+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-114143","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-114143","severity":null,"published_at":"2026-10-21T21:17:56+00:00","modified_at":"2026-10-22T16:48:25+00:00","first_seen_at":"2026-10-23T21:57:29+00:00","fetched_at":"2026-10-23T22:29:02+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}