Skip to content

Add depth cap to KDL parser - #554

Merged
TomWright merged 1 commit into
masterfrom
advisory-kdl-depth-cap
Jul 31, 2026
Merged

Add depth cap to KDL parser#554
TomWright merged 1 commit into
masterfrom
advisory-kdl-depth-cap

Conversation

@TomWright

Copy link
Copy Markdown
Owner

Fixes GHSA-v72w-jh9m-fv4r (high, CVSS 7.5, CWE-674). Reported by @vnykmshr.

Problem

The KDL parser is mutually recursive over children blocks:

parseDocument -> parseNode -> parseNodeEntries -> parseDocument

Each { } block added a set of call frames with no depth counter and no limit, so deeply nested input overflowed the 1GB goroutine stack. A stack overflow is a fatal error that recover() cannot intercept, so any caller passing untrusted KDL through kdlReader.Read() or internal.Parse() could be crashed. No auth or preconditions beyond supplying KDL input.

KDL was missed when 4c91d0d added the same depth caps to the JSON and XML readers.

Affected: v3.10.0 (KDL support) through v3.11.2.

Two recursion sites, not one

The advisory named the children block. There are actually two call sites recursing into parseDocument, and the second is separately reachable:

site trigger before
children block (parser.go:156) node { x 2M fatal error: stack overflow
slashdashed children (parser.go:193) node /-{ x 2M fatal error: stack overflow

Capping only the site named in the advisory would have left the slashdash path as a bypass. Both are capped here, and both have regression tests.

Fix

depth is threaded through parseDocument -> parseNode -> parseNodeEntries, with the guard at the top of parseDocument. This mirrors decodeObject/decodeArray in the JSON reader and parseElement in the XML reader — same 10,000 limit, same error shape. ErrKDLMaxDepthExceeded is re-exported from parsing/kdl because the parser lives in an internal package that callers cannot import.

Verification

Reproduced both crashes on master first, with stack traces showing the expected parseDocument frames. After the fix both return kdl nesting depth exceeded, and 10,000 levels still parses.

Tests cover: 10,001 children blocks (error), 10,001 slashdashed children blocks (error), exactly 10,000 (succeeds), and ordinary nesting (succeeds).

Adjacent recursion checked

nodesToValue/nodeToValue in the reader and writeDocument/writeNode in the generator are also mutually recursive, so I checked whether they are separately reachable with deep input. They are not — every reader caps at 10,000: JSON and XML explicitly, YAML via its library (yaml: exceeded max depth of 10000, confirmed by piping 200,000-deep YAML to KDL). Capping the parser bounds the tree, so no further changes were needed.

go build, go vet, and the full suite are clean.

The KDL parser is mutually recursive over children blocks:

    parseDocument -> parseNode -> parseNodeEntries -> parseDocument

Each { } block added a set of call frames with no depth counter and no
limit, so deeply nested input overflowed the 1GB goroutine stack. A stack
overflow is a fatal error that recover() cannot intercept, so any caller
passing untrusted KDL through kdlReader.Read() could be crashed.

KDL was missed when 4c91d0d added the same caps to the JSON and XML
readers. It now enforces the same 10,000 level limit and returns
ErrKDLMaxDepthExceeded.

Both recursion sites into parseDocument are covered - the children block
and the slashdashed children block, which is reachable via "node /-{ "
and was equally exploitable.

GHSA-v72w-jh9m-fv4r
@TomWright
TomWright merged commit f515c15 into master Jul 31, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant