From 967b8b06754f3ecd9863cea118dc50792a8c353f Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 13 Jul 2026 15:01:24 -0700 Subject: [PATCH 1/2] fix: bound nesting depth to prevent stack-overflow DoS Deeply nested arrays or inline tables drove the recursive-descent parser past the call-stack limit, crashing with an uncatchable RangeError (GHSA-82x6-q7mm-w9cf). A depth guard on the `value` rule now caps nesting (default 500) and raises a normal parse error instead. The limit is configurable via `toml.parse(input, { maxDepth })`. The guard pairs each increment with a decrement on both the matching and backtracking paths, so a failed value probe (e.g. after a trailing comma) cannot leak the counter and falsely reject valid input. --- README.md | 8 ++ index.d.ts | 11 +- index.js | 4 +- lib/parser.js | 294 ++++++++++++++++++++++++++++------------------ src/toml.pegjs | 19 +++ test/test_toml.js | 61 ++++++++++ 6 files changed, 278 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index bb47206..1860a60 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,14 @@ try { } ``` +### Nesting Depth Limit + +To guard against stack overflow on maliciously deep input, arrays and inline tables may nest at most 500 levels deep by default; input past the limit throws a normal parse error. Adjust the limit with the `maxDepth` option: + +```javascript +toml.parse(someTomlString, { maxDepth: 100 }); +``` + ### Date/Time Values Offset date-times are returned as JavaScript `Date` objects. Local date-times, local dates, and local times are returned as strings since they have no timezone information and can't be losslessly represented as `Date`: diff --git a/index.d.ts b/index.d.ts index 7e9052b..d5e9723 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,3 +1,12 @@ declare module 'toml' { - export function parse(input: string): any; + export interface ParseOptions { + /** + * Maximum nesting depth for arrays and inline tables. Parsing input nested + * deeper than this throws a parse error rather than overflowing the stack. + * Defaults to 500. + */ + maxDepth?: number; + } + + export function parse(input: string, options?: ParseOptions): any; } diff --git a/index.js b/index.js index 57f8283..2c6e21e 100644 --- a/index.js +++ b/index.js @@ -2,9 +2,9 @@ var parser = require('./lib/parser'); var compiler = require('./lib/compiler'); module.exports = { - parse: function(input) { + parse: function(input, options) { var str = input.toString(); - var nodes = parser.parse(str); + var nodes = parser.parse(str, options); return compiler.compile(nodes, str); } }; diff --git a/lib/parser.js b/lib/parser.js index 019d181..35cb268 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -283,75 +283,79 @@ function peg$parse(input, options) { function peg$f7(keys, value) { addNode(node('Assign', value, offset(), keys)) } function peg$f8(node) { return node.value } function peg$f9(node) { return node.value } - function peg$f10(sign) { return node('Float', sign === '-' ? -Infinity : Infinity, offset()) } - function peg$f11(sign) { return node('Float', NaN, offset()) } - function peg$f12(body) { return node('String', body, offset()) } - function peg$f13(chars) { return node('String', chars.join(''), offset()) } - function peg$f14(body) { return node('String', body, offset()) } - function peg$f15(chars) { return node('String', chars.join(''), offset()) } - function peg$f16(head, parts, tail) { + function peg$f10() { if (++depth > MAX_DEPTH) { depth--; genError("Maximum nesting depth of " + MAX_DEPTH + " exceeded.", offset()); } return true; } + function peg$f11(v) { return v; } + function peg$f12(v) { depth--; return v; } + function peg$f13() { depth--; return false; } + function peg$f14(sign) { return node('Float', sign === '-' ? -Infinity : Infinity, offset()) } + function peg$f15(sign) { return node('Float', NaN, offset()) } + function peg$f16(body) { return node('String', body, offset()) } + function peg$f17(chars) { return node('String', chars.join(''), offset()) } + function peg$f18(body) { return node('String', body, offset()) } + function peg$f19(chars) { return node('String', chars.join(''), offset()) } + function peg$f20(head, parts, tail) { var result = head.join(''); for (var i = 0; i < parts.length; i++) { result += parts[i][0] + parts[i][1].join(''); } return result + (tail || ''); } - function peg$f17() { genError("Invalid escape sequence", offset()) } - function peg$f18() { return "\n" } - function peg$f19() { return '' } - function peg$f20() { return '""' } - function peg$f21() { return '"' } - function peg$f22() { return '""' } - function peg$f23() { return '"' } - function peg$f24(head, parts, tail) { + function peg$f21() { genError("Invalid escape sequence", offset()) } + function peg$f22() { return "\n" } + function peg$f23() { return '' } + function peg$f24() { return '""' } + function peg$f25() { return '"' } + function peg$f26() { return '""' } + function peg$f27() { return '"' } + function peg$f28(head, parts, tail) { var result = head.join(''); for (var i = 0; i < parts.length; i++) { result += parts[i][0] + parts[i][1].join(''); } return result + (tail || ''); } - function peg$f25() { return "\n" } - function peg$f26() { return "''" } - function peg$f27() { return "'" } - function peg$f28() { return "''" } - function peg$f29() { return "'" } - function peg$f30() { genError("Invalid escape sequence", offset()) } - function peg$f31(left, right) { return node('Float', parseFloat(stripUnderscores(left + 'e' + right)), offset()) } - function peg$f32(text) { return node('Float', parseFloat(stripUnderscores(text)), offset()) } - function peg$f33(sign, digits, frac) { return (sign === '-' ? '-' : '') + digits + '.' + frac } - function peg$f34(sign, digits, frac) { return (sign === '-' ? '-' : '') + digits + '.' + frac } - function peg$f35(sign, digits) { return (sign === '-' ? '-' : '') + digits } - function peg$f36() { return '0' } - function peg$f37(sign, digits) { return (sign || '') + digits } - function peg$f38(digits) { return node('Integer', parseInt(stripUnderscores(digits), 16), offset()) } - function peg$f39(digits) { return node('Integer', parseInt(stripUnderscores(digits), 8), offset()) } - function peg$f40(digits) { return node('Integer', parseInt(stripUnderscores(digits), 2), offset()) } - function peg$f41(text) { return node('Integer', parseInt(stripUnderscores(text), 10), offset()) } - function peg$f42(sign) { return (sign || '') + '0' } - function peg$f43(sign, digits) { return (sign || '') + digits } - function peg$f44() { return node('Boolean', true, offset()) } - function peg$f45() { return node('Boolean', false, offset()) } - function peg$f46() { return node('Array', [], offset()) } - function peg$f47(head, v) { return v } - function peg$f48(head, tail) { + function peg$f29() { return "\n" } + function peg$f30() { return "''" } + function peg$f31() { return "'" } + function peg$f32() { return "''" } + function peg$f33() { return "'" } + function peg$f34() { genError("Invalid escape sequence", offset()) } + function peg$f35(left, right) { return node('Float', parseFloat(stripUnderscores(left + 'e' + right)), offset()) } + function peg$f36(text) { return node('Float', parseFloat(stripUnderscores(text)), offset()) } + function peg$f37(sign, digits, frac) { return (sign === '-' ? '-' : '') + digits + '.' + frac } + function peg$f38(sign, digits, frac) { return (sign === '-' ? '-' : '') + digits + '.' + frac } + function peg$f39(sign, digits) { return (sign === '-' ? '-' : '') + digits } + function peg$f40() { return '0' } + function peg$f41(sign, digits) { return (sign || '') + digits } + function peg$f42(digits) { return node('Integer', parseInt(stripUnderscores(digits), 16), offset()) } + function peg$f43(digits) { return node('Integer', parseInt(stripUnderscores(digits), 8), offset()) } + function peg$f44(digits) { return node('Integer', parseInt(stripUnderscores(digits), 2), offset()) } + function peg$f45(text) { return node('Integer', parseInt(stripUnderscores(text), 10), offset()) } + function peg$f46(sign) { return (sign || '') + '0' } + function peg$f47(sign, digits) { return (sign || '') + digits } + function peg$f48() { return node('Boolean', true, offset()) } + function peg$f49() { return node('Boolean', false, offset()) } + function peg$f50() { return node('Array', [], offset()) } + function peg$f51(head, v) { return v } + function peg$f52(head, tail) { tail.unshift(head); return node('Array', tail, offset()) } - function peg$f49() { return node('InlineTable', [], offset()) } - function peg$f50(head, e) { return e } - function peg$f51(head, tail) { + function peg$f53() { return node('InlineTable', [], offset()) } + function peg$f54(head, e) { return e } + function peg$f55(head, tail) { tail.unshift(head); return node('InlineTable', tail, offset()) } - function peg$f52(keys, value) { return node('InlineTableValue', value, offset(), keys) } - function peg$f53(parts, last) { return parts.concat(last) } - function peg$f54(k) { return [k] } - function peg$f55(k) { return k } - function peg$f56(t, frac) { return frac ? t + frac : t } - function peg$f57(t) { return t + ':00' } - function peg$f58() { return "Z" } - function peg$f59(d, t, o) { var off = offset(); validateDate(d, off); validateTime(t, off); validateOffset(o, off); return node('Date', new Date(d + "T" + t + o), off) } - function peg$f60(d, t) { var off = offset(); validateDate(d, off); validateTime(t, off); return node('LocalDateTime', d + "T" + t, off) } - function peg$f61(d) { var off = offset(); validateDate(d, off); return node('LocalDate', d, off) } - function peg$f62(t) { var off = offset(); validateTime(t, off); return node('LocalTime', t, off) } - function peg$f63(ch) { return ch === 'n' ? '\n' : ch === 't' ? '\t' : ch === 'r' ? '\r' : ch === '\\' ? '\\' : ch === '"' ? '"' : ch === 'b' ? '\b' : ch === 'f' ? '\f' : '\x1B' } - function peg$f64(digits) { return convertCodePoint(digits) } - function peg$f65(digits) { return convertCodePoint(digits) } - function peg$f66(digits) { return convertCodePoint(digits) } + function peg$f56(keys, value) { return node('InlineTableValue', value, offset(), keys) } + function peg$f57(parts, last) { return parts.concat(last) } + function peg$f58(k) { return [k] } + function peg$f59(k) { return k } + function peg$f60(t, frac) { return frac ? t + frac : t } + function peg$f61(t) { return t + ':00' } + function peg$f62() { return "Z" } + function peg$f63(d, t, o) { var off = offset(); validateDate(d, off); validateTime(t, off); validateOffset(o, off); return node('Date', new Date(d + "T" + t + o), off) } + function peg$f64(d, t) { var off = offset(); validateDate(d, off); validateTime(t, off); return node('LocalDateTime', d + "T" + t, off) } + function peg$f65(d) { var off = offset(); validateDate(d, off); return node('LocalDate', d, off) } + function peg$f66(t) { var off = offset(); validateTime(t, off); return node('LocalTime', t, off) } + function peg$f67(ch) { return ch === 'n' ? '\n' : ch === 't' ? '\t' : ch === 'r' ? '\r' : ch === '\\' ? '\\' : ch === '"' ? '"' : ch === 'b' ? '\b' : ch === 'f' ? '\f' : '\x1B' } + function peg$f68(digits) { return convertCodePoint(digits) } + function peg$f69(digits) { return convertCodePoint(digits) } + function peg$f70(digits) { return convertCodePoint(digits) } let peg$currPos = options.peg$currPos | 0; let peg$savedPos = peg$currPos; const peg$posDetailsCache = [{ line: 1, column: 1 }]; @@ -1011,6 +1015,57 @@ function peg$parse(input, options) { } function peg$parsevalue() { + let s0, s1, s2; + + s0 = peg$currPos; + peg$savedPos = peg$currPos; + s1 = peg$f10(); + if (s1) { + s1 = undefined; + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s2 = peg$parsevalue_choice(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f11(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsevalue_choice() { + let s0, s1; + + s0 = peg$currPos; + s1 = peg$parsevalue_body(); + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f12(s1); + } + s0 = s1; + if (s0 === peg$FAILED) { + peg$savedPos = peg$currPos; + s0 = peg$f13(); + if (s0) { + s0 = undefined; + } else { + s0 = peg$FAILED; + } + } + + return s0; + } + + function peg$parsevalue_body() { let s0; s0 = peg$parsestring(); @@ -1053,7 +1108,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f10(s1); + s0 = peg$f14(s1); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1079,7 +1134,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f11(s1); + s0 = peg$f15(s1); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1141,7 +1196,7 @@ function peg$parse(input, options) { } if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f12(s3); + s0 = peg$f16(s3); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1181,7 +1236,7 @@ function peg$parse(input, options) { } if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f13(s2); + s0 = peg$f17(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1220,7 +1275,7 @@ function peg$parse(input, options) { } if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f14(s3); + s0 = peg$f18(s3); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1260,7 +1315,7 @@ function peg$parse(input, options) { } if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f15(s2); + s0 = peg$f19(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1340,7 +1395,7 @@ function peg$parse(input, options) { s3 = null; } peg$savedPos = s0; - s0 = peg$f16(s1, s2, s3); + s0 = peg$f20(s1, s2, s3); return s0; } @@ -1370,7 +1425,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f17(); + s0 = peg$f21(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1390,7 +1445,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f18(); + s1 = peg$f22(); } s0 = s1; if (s0 === peg$FAILED) { @@ -1457,7 +1512,7 @@ function peg$parse(input, options) { s5 = peg$parseNLS(); } peg$savedPos = s0; - s0 = peg$f19(); + s0 = peg$f23(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1500,7 +1555,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f20(); + s0 = peg$f24(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1537,7 +1592,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f21(); + s0 = peg$f25(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1581,7 +1636,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f22(); + s0 = peg$f26(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1618,7 +1673,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f23(); + s0 = peg$f27(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1699,7 +1754,7 @@ function peg$parse(input, options) { s3 = null; } peg$savedPos = s0; - s0 = peg$f24(s1, s2, s3); + s0 = peg$f28(s1, s2, s3); return s0; } @@ -1717,7 +1772,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f25(); + s1 = peg$f29(); } s0 = s1; if (s0 === peg$FAILED) { @@ -1784,7 +1839,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f26(); + s0 = peg$f30(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1821,7 +1876,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f27(); + s0 = peg$f31(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1865,7 +1920,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f28(); + s0 = peg$f32(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1902,7 +1957,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f29(); + s0 = peg$f33(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1939,7 +1994,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f30(); + s0 = peg$f34(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2035,7 +2090,7 @@ function peg$parse(input, options) { s3 = peg$parsefloat_exp_text(); if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f31(s1, s3); + s0 = peg$f35(s1, s3); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2053,7 +2108,7 @@ function peg$parse(input, options) { s1 = peg$parsefloat_text(); if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f32(s1); + s1 = peg$f36(s1); } s0 = s1; } @@ -2094,7 +2149,7 @@ function peg$parse(input, options) { } if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f33(s1, s2, s4); + s0 = peg$f37(s1, s2, s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2144,7 +2199,7 @@ function peg$parse(input, options) { } if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f34(s1, s2, s4); + s0 = peg$f38(s1, s2, s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2172,7 +2227,7 @@ function peg$parse(input, options) { s2 = peg$parseFLOAT_DEC_INT(); if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f35(s1, s2); + s0 = peg$f39(s1, s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2195,7 +2250,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f36(); + s1 = peg$f40(); } s0 = s1; if (s0 === peg$FAILED) { @@ -2234,7 +2289,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f37(s1, s2); + s0 = peg$f41(s1, s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2264,7 +2319,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f38(s2); + s0 = peg$f42(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2292,7 +2347,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f39(s2); + s0 = peg$f43(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2320,7 +2375,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f40(s2); + s0 = peg$f44(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2334,7 +2389,7 @@ function peg$parse(input, options) { s1 = peg$parsedec_integer_text(); if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f41(s1); + s1 = peg$f45(s1); } s0 = s1; } @@ -2401,7 +2456,7 @@ function peg$parse(input, options) { } if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f42(s1); + s0 = peg$f46(s1); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2452,7 +2507,7 @@ function peg$parse(input, options) { } if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f43(s1, s2); + s0 = peg$f47(s1, s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2859,7 +2914,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f44(); + s1 = peg$f48(); } s0 = s1; if (s0 === peg$FAILED) { @@ -2873,7 +2928,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f45(); + s1 = peg$f49(); } s0 = s1; } @@ -2908,7 +2963,7 @@ function peg$parse(input, options) { } if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f46(); + s0 = peg$f50(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2960,7 +3015,7 @@ function peg$parse(input, options) { s9 = peg$parsevalue(); if (s9 !== peg$FAILED) { peg$savedPos = s5; - s5 = peg$f47(s3, s9); + s5 = peg$f51(s3, s9); } else { peg$currPos = s5; s5 = peg$FAILED; @@ -2995,7 +3050,7 @@ function peg$parse(input, options) { s9 = peg$parsevalue(); if (s9 !== peg$FAILED) { peg$savedPos = s5; - s5 = peg$f47(s3, s9); + s5 = peg$f51(s3, s9); } else { peg$currPos = s5; s5 = peg$FAILED; @@ -3036,7 +3091,7 @@ function peg$parse(input, options) { } if (s8 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f48(s3, s4); + s0 = peg$f52(s3, s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3095,7 +3150,7 @@ function peg$parse(input, options) { } if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f49(); + s0 = peg$f53(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3147,7 +3202,7 @@ function peg$parse(input, options) { s9 = peg$parseinline_table_entry(); if (s9 !== peg$FAILED) { peg$savedPos = s5; - s5 = peg$f50(s3, s9); + s5 = peg$f54(s3, s9); } else { peg$currPos = s5; s5 = peg$FAILED; @@ -3182,7 +3237,7 @@ function peg$parse(input, options) { s9 = peg$parseinline_table_entry(); if (s9 !== peg$FAILED) { peg$savedPos = s5; - s5 = peg$f50(s3, s9); + s5 = peg$f54(s3, s9); } else { peg$currPos = s5; s5 = peg$FAILED; @@ -3223,7 +3278,7 @@ function peg$parse(input, options) { } if (s8 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f51(s3, s4); + s0 = peg$f55(s3, s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3270,7 +3325,7 @@ function peg$parse(input, options) { s5 = peg$parsevalue(); if (s5 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f52(s1, s5); + s0 = peg$f56(s1, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3325,7 +3380,7 @@ function peg$parse(input, options) { s3 = peg$parsesimple_key(); if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f53(s1, s3); + s0 = peg$f57(s1, s3); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3339,7 +3394,7 @@ function peg$parse(input, options) { s1 = peg$parsesimple_key(); if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f54(s1); + s1 = peg$f58(s1); } s0 = s1; } @@ -3374,7 +3429,7 @@ function peg$parse(input, options) { } if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f55(s2); + s0 = peg$f59(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3608,7 +3663,7 @@ function peg$parse(input, options) { s2 = null; } peg$savedPos = s0; - s0 = peg$f56(s1, s2); + s0 = peg$f60(s1, s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3679,7 +3734,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f57(s1); + s0 = peg$f61(s1); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3706,7 +3761,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f58(); + s1 = peg$f62(); } s0 = s1; if (s0 === peg$FAILED) { @@ -3785,7 +3840,7 @@ function peg$parse(input, options) { s4 = peg$parseoffset(); if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f59(s1, s3, s4); + s0 = peg$f63(s1, s3, s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3811,7 +3866,7 @@ function peg$parse(input, options) { s3 = peg$parsetime_part(); if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f60(s1, s3); + s0 = peg$f64(s1, s3); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3840,7 +3895,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f61(s1); + s0 = peg$f65(s1); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3854,7 +3909,7 @@ function peg$parse(input, options) { s1 = peg$parsetime_part(); if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f62(s1); + s1 = peg$f66(s1); } s0 = s1; } @@ -4078,7 +4133,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f63(s2); + s0 = peg$f67(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4165,7 +4220,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f64(s2); + s0 = peg$f68(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4219,7 +4274,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f65(s2); + s0 = peg$f69(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4261,7 +4316,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f66(s2); + s0 = peg$f70(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4280,6 +4335,13 @@ function peg$parse(input, options) { var nodes = []; var inputText = input; + // Bound on how deeply arrays / inline tables may nest. The generated parser + // is recursive-descent, so nesting depth maps directly onto call-stack depth; + // without a limit, deeply nested input overflows the stack with an uncatchable + // RangeError instead of a normal parse error (GHSA-82x6-q7mm-w9cf). + var depth = 0; + var MAX_DEPTH = (options && options.maxDepth != null) ? options.maxDepth : 500; + function resolveLineCol(off) { var line = 1, col = 1; for (var i = 0; i < off; i++) { diff --git a/src/toml.pegjs b/src/toml.pegjs index 18d325b..61be02b 100644 --- a/src/toml.pegjs +++ b/src/toml.pegjs @@ -2,6 +2,13 @@ var nodes = []; var inputText = input; + // Bound on how deeply arrays / inline tables may nest. The generated parser + // is recursive-descent, so nesting depth maps directly onto call-stack depth; + // without a limit, deeply nested input overflows the stack with an uncatchable + // RangeError instead of a normal parse error (GHSA-82x6-q7mm-w9cf). + var depth = 0; + var MAX_DEPTH = (options && options.maxDepth != null) ? options.maxDepth : 500; + function resolveLineCol(off) { var line = 1, col = 1; for (var i = 0; i < off; i++) { @@ -131,7 +138,19 @@ quoted_key = node:double_quoted_single_line_string { return node.value } / node:single_quoted_single_line_string { return node.value } +// The depth guard brackets every value. `value_choice` always pairs the +// increment with a decrement — on the matching path via its action, and on the +// non-matching path via the trailing predicate — so backtracking (e.g. the +// failed `value` probe after a trailing comma) can't leak the counter. value + = &{ if (++depth > MAX_DEPTH) { depth--; genError("Maximum nesting depth of " + MAX_DEPTH + " exceeded.", offset()); } return true; } + v:value_choice { return v; } + +value_choice + = v:value_body { depth--; return v; } + / &{ depth--; return false; } + +value_body = string / number_or_date / boolean / array / inline_table // Unified entry point for numbers and dates — avoids backtracking across diff --git a/test/test_toml.js b/test/test_toml.js index 43a1796..06f0861 100644 --- a/test/test_toml.js +++ b/test/test_toml.js @@ -453,6 +453,67 @@ describe("error handling", function () { }); }); +describe("nesting depth limit (GHSA-82x6-q7mm-w9cf)", function () { + function nestArray(depth) { + return "a=" + "[".repeat(depth) + "1" + "]".repeat(depth); + } + + function nestInlineTable(depth) { + return "a=" + "{b=".repeat(depth) + "1" + "}".repeat(depth); + } + + it("rejects deeply nested arrays with a catchable parse error, not a RangeError", function () { + try { + toml.parse(nestArray(5000)); + assert.fail("Should have thrown"); + } catch (e) { + assert.strictEqual(e instanceof RangeError, false); + assert.strictEqual(typeof e.line, "number"); + } + }); + + it("rejects deeply nested inline tables with a catchable parse error", function () { + try { + toml.parse(nestInlineTable(5000)); + assert.fail("Should have thrown"); + } catch (e) { + assert.strictEqual(e instanceof RangeError, false); + assert.strictEqual(typeof e.line, "number"); + } + }); + + it("allows nesting up to the default limit", function () { + // The innermost scalar is itself a value, so N brackets reach depth N + 1. + assert.doesNotThrow(function () { + toml.parse(nestArray(499)); + }); + }); + + it("rejects nesting past the default limit", function () { + assert.throws(function () { + toml.parse(nestArray(500)); + }); + }); + + it("honors a custom maxDepth option", function () { + assert.doesNotThrow(function () { + toml.parse(nestArray(9), { maxDepth: 10 }); + }); + assert.throws(function () { + toml.parse(nestArray(10), { maxDepth: 10 }); + }); + }); + + it("does not miscount sibling values via backtracking", function () { + // Each trailing comma triggers a failed `value` probe that the parser + // backtracks over; the guard must not leak depth across these siblings. + var siblings = []; + for (var i = 0; i < 2000; i++) siblings.push("[1,]"); + var result = toml.parse("a=[" + siblings.join(",") + "]", { maxDepth: 4 }); + assert.strictEqual(result.a.length, 2000); + }); +}); + describe("prototype pollution hardening", function () { it("rejects table paths that descend through scalar values", function () { rejectsWithoutObjectPrototypeMutation( From a2d6dbbb06251c1ddcdbb5a2e86fe4eba5207df6 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 13 Jul 2026 15:01:25 -0700 Subject: [PATCH 2/2] Update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5acade8..2935d13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ -4.1.2 - June 30 2026 +4.2.0 - July 13 2026 ===================== -* Address [security advisory `GHSA-v5mp-jgw5-2x6j`](https://github.com/BinaryMuse/toml-node/security/advisories/GHSA-v5mp-jgw5-2x6j) (CVE pending), in which a specially crafted TOML string could pollute `Object.prototype` process-wide.. +* Address [security advisory `GHSA-82x6-q7mm-w9cf`](https://github.com/BinaryMuse/toml-node/security/advisories/GHSA-82x6-q7mm-w9cf) (CVE pending), in which deeply nested arrays or inline tables could overflow the call stack and crash the process with an uncatchable `RangeError`. Nesting is now bounded (default 500 levels), and input past the limit throws a normal parse error. The limit is configurable via `toml.parse(input, { maxDepth })`. 4.1.1 - March 31 2026 =====================