Skip to content

fix: a recipe is refused for its depth whichever style it nests in - #64

Merged
donislawdev merged 1 commit into
mainfrom
security/recipe-depth
Sep 6, 2026
Merged

fix: a recipe is refused for its depth whichever style it nests in#64
donislawdev merged 1 commit into
mainfrom
security/recipe-depth

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

The limit that stops a small recipe from exhausting the machine counted flow collections - the ones written with brackets and braces - because that is how the bomb it was written for was written. The same nesting in block style walks past it: - - - - x is four nested sequences in eight bytes and carries no bracket at all.

Measured against the previous build, with tfg validate

input size time result
- x20 000 40 kB 0.40 s refused, by a limit inside the parser rather than by ours
- x100 000 200 kB 7.41 s refused
- x250 000 500 kB 88.2 s fatal error: out of memory, 41 kB of Go runtime stack on stderr, exit 2
- x500 000 1 MB, the most MaxBytes allows 70.7 s exit 1, and not one word of output

Exit 2 is what the frozen table gives a mistyped flag. The machine has 31.9 GB.

The cost is the nesting rather than the size, and that is measured. 500 kB of flat mapping - a hundred thousand lines of a: 1 - is refused cleanly in 0.62 s. An alias bomb does not amplify at all: the schema refuses it in 0.2 s before anything expands. So neither a limit on bytes nor one on token count could have answered this.

The fix

Block sequences are counted from the column each dash sits in - further right opens a sequence inside the last one, the same column is the next entry, further left closes - and that is added to the flow depth already counted. The limit stays at 32. An ordinary recipe reaches one and an archive declaring its contents reaches two. The 1 MB file is now refused in 0.76 s.

The whole defence rests on the pre-scan being cheap, since it has to run before the parser. That was measured rather than assumed, with tools/probes/yamldepth: lexer.Tokenize on the bomb is 7 ms at 40 kB, 42 ms at 200 kB and 422 ms at 1 MB, linear, against the parser's 70 to 88 seconds.

Guards

Three new cases in TestAHostileRecipeCannotHangTheReader: the block bomb refused, and two the fix could have broken - 200 targets in one list, and a list inside a list. Four mutations, all caught, including one that had gone stale when limits.go was rewritten and was repaired rather than counted.

What this counter over-counts is written beside it: two sibling sequences under different keys, the second indented further, read as nested. Reaching 32 that way needs 32 keys each indented further than the last.

🤖 Generated with Claude Code

The limit that stops a small recipe from exhausting the machine counted
flow collections - the ones written with brackets and braces - because
that is how the bomb it was written for was written. The same nesting in
block style walks past it: "- - - - x" is four nested sequences in eight
bytes and carries no bracket at all.

Measured against the previous build with tfg validate:

  "- " x20 000     40 kB    0.40 s   refused, by a limit inside the parser
  "- " x100 000   200 kB    7.41 s   refused
  "- " x250 000   500 kB   88.2 s    fatal error: out of memory, exit 2
  "- " x500 000     1 MB   70.7 s    exit 1, and not one word of output

The third printed 41 kB of Go runtime stack and left with the exit code
the frozen table gives a mistyped flag. The machine has 31.9 GB.

The cost is the nesting rather than the size, and that is measured: 500 kB
of flat mapping is refused cleanly in 0.62 s. An alias bomb does not
amplify at all, because the schema refuses it before anything expands.

Block sequences are now counted from the column each dash sits in, added
to the flow depth that was already counted, and the limit stays at 32. An
ordinary recipe reaches one and an archive declaring its contents reaches
two. The same file is now refused in 0.76 s.

The whole defence rests on the pre-scan being cheap, so that was measured
rather than assumed - tools/probes/yamldepth: lexer.Tokenize on the bomb
is 42 ms and 28.9 MB at 200 kB and 422 ms and 145 MB at 1 MB, against the
parser's 70 seconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev force-pushed the security/recipe-depth branch from 316236f to 42ee699 Compare September 6, 2026 13:15
@donislawdev
donislawdev merged commit 4cf0ae0 into main Sep 6, 2026
18 checks passed
@donislawdev
donislawdev deleted the security/recipe-depth branch September 6, 2026 13:26
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