Hardening PHP YAML parsing against untrusted input

The parser does not fail politely when you hand it hostile YAML

Deeply nested documents can drive the parser into recursive descent until PHP runs out of room and throws a fatal error. The recursion sits in the block parsing path, so a document that looks small at the top can still be enough to burn through stack frames if the indentation keeps going.

Anchors and aliases are just as awkward. A small YAML payload can expand into a much larger in-memory structure once aliases start repeating, and that can turn a parse into a memory problem rather than a syntax problem. In practice, the process may fall over later than the parser itself, which makes the failure mode look messier than it should.

Cleanup code deserves attention too. Header stripping that relies on greedy regular expressions can be pushed into catastrophic backtracking, which means the parser stops being slow and becomes stuck. In an open source audit, that sort of branch is usually the one that looks harmless right up to the point it is not.

Keep unsafe features out of the untrusted path

Yaml::PARSE_OBJECT and Yaml::PARSE_CONSTANT need to be treated as explicit trust boundaries, not as normal parser toggles. Yaml::PARSE_OBJECT can route !php/object payloads into unserialize(), which gives attacker-controlled input a path into PHP object injection. Yaml::PARSE_CONSTANT resolves !php/const values through \constant(), which can expose built-in or application-defined constants if untrusted data reaches it.

That is the sort of feature that belongs behind a deliberate decision, not hidden in a default loader. If a code path accepts user input, the safe boundary is to reject those tags before parsing or to run with those features disabled altogether. Syntax checks alone do not make that path safe.

Test parsing, dumping YAML, and validation as separate behaviours. The dumper can produce legal YAML from PHP data, while the parser still behaves badly on hostile input, and the validator can say a file is syntactically fine without matching the exact runtime path. Different parsers can also disagree on meaning, which is another reason not to treat “it passes lint” as a security control.

A sensible audit starts with Parser::cleanup(), then doParse() and parseBlock(), then any code path that turns on object or constant resolution. That order catches the parts most likely to break under untrusted input, and it avoids spending time admiring the safe bits while the dangerous ones sit in plain sight.

Related posts

Immich | v3.2.2

Immich v3 2 2: small patch fixes cross user face reassign bug, skips faces owned by other accounts, recommended update for users relying on face reassign

Nextcloud | v35.0.0

Nextcloud v35: polished UI, Files and sharing upgrades, better client parity, security hardening, performance and admin gains, developer notes and upgrade tips

Talos Linux | v1.14.1

Talos Linux v1 14 1: Linux 6 18 51, containerd 2 3 5, Go 1 26 8, robustness and edge fixes for BGP, VRF, WireGuard, kubelet, LVM, USB, images published