Skip to content

fix(decoder): limit the nesting depth of the decoded data - #143

Merged
Spomky merged 1 commit into
3.3.xfrom
fix/decoder-max-nesting-depth
Aug 31, 2026
Merged

fix(decoder): limit the nesting depth of the decoded data#143
Spomky merged 1 commit into
3.3.xfrom
fix/decoder-max-nesting-depth

Conversation

@Spomky

@Spomky Spomky commented Aug 31, 2026

Copy link
Copy Markdown
Member

Problem

CBOR\Decoder processed nested data items recursively without any bound. A payload made only of nested arrays costs one byte per level (0x81), so ~44 KB of input is enough to build an object graph deep enough to crash the process — a segmentation fault, not a catchable error.

The crash does not happen during parsing: decode() returns successfully, and the process dies later, when the graph is released, because the destructor chain is recursive on the C stack. The threshold follows the stack size (measured: ~1 600 levels with a 256 KB stack, ~4 000 with 512 KB, ~40 000 with the default 8 MB), so a worker with a reduced stack crashes on a much smaller payload.

Nested arrays are not the only vector: nested maps, tag chains (0xcb repeated) and indefinite length containers are all one byte per level too.

Fix

Nesting depth is now tracked while processing and checked on every nested item — lists, maps, tags and indefinite length containers alike. Data nested deeper than the limit is rejected with an InvalidArgumentException instead of building the graph:

Cannot parse the data. Maximum nesting depth of 1000 exceeded.

The limit defaults to Decoder::DEFAULT_MAX_DEPTH (1000 levels) and can be lowered — recommended when the data comes from an untrusted source:

// Accept at most 32 levels of nested arrays, maps and tags
$decoder = Decoder::create(null, null, 32);

Backward compatible: the depth is a new optional third argument of Decoder::__construct() / Decoder::create(), DecoderInterface is unchanged, and 1000 levels is far beyond what COSE, CWT or WebAuthn data ever reaches.

Tests

tests/MaxDepthTest.php covers the limit being reached, exceeded, and configured, each of the four nesting vectors, and the rejection of a limit lower than 1.

Full suite: 497 tests, 1103 assertions, ECS and parallel-lint clean. PHPStan reports 4 errors on NegativeIntegerObject / UnsignedIntegerObject, all pre-existing on 3.3.x and unrelated to this change; Decoder.php is clean.

Reported by Ivan Tse.

The decoder processed nested data items recursively without any bound. A
payload made only of nested arrays (one byte per level) was enough to build an
object graph deep enough to crash the process when it was released, after
decode() had returned successfully.

Nested lists, maps, tags and indefinite length containers now count towards a
maximum nesting depth. Data nested deeper than that limit is rejected with an
InvalidArgumentException. The limit defaults to Decoder::DEFAULT_MAX_DEPTH
(1000 levels) and can be changed with the third argument of Decoder::create().

Reported by Ivan Tse (https://github.com/ivantsepp).
@Spomky Spomky self-assigned this Aug 31, 2026
@Spomky Spomky added this to the 3.3.3 milestone Aug 31, 2026
@Spomky
Spomky merged commit 693f6ed into 3.3.x Aug 31, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant