Affected versions
Symfony YAML versions <5.4.56, >=6.0.0, <6.4.50, >=7.0.0, <7.4.23, >=8.0.0, <8.1.11 are affected by this security issue.
The issue has been fixed in Symfony YAML 5.4.56, 6.4.50, 7.4.23, 8.1.11.
Description
Parsing some YAML documents took time that grew much faster than their size. A small document could keep a PHP process busy for seconds or minutes:
- Since 6.4.34, 7.4.6 and 8.0.6, an embedded mapping in a flow sequence, such as
[a: [a: [a: x, x]]], was parsed again for every scalar appended to it. The collections nested in it did the same, so the parse time grew exponentially with the nesting depth. A document of 93 bytes took 15 seconds. - The patterns matching a mapping key or a tag scanned a run of spaces once per character of the run. A single line of 128 KB, such as
a, a run of spaces andb: c, took 6 to 12 seconds. The PCRE backtracking limit does not stop this. - Each scalar of a flow collection was matched against a copy of the rest of the collection. A JSON-like document of 200 KB took about 9 seconds. Since 6.4.49, 7.4.22 and 8.1.10, each scalar of a flow sequence is copied a second time. A JSON array of numbers of 730 KB then took 10 seconds.
- An empty sequence item followed by a blank or comment line turned the next item into a nested sequence. Each item then parsed the rest of the document again, up to the nesting limit. A document of 200 KB took about 7 seconds.
- An anchor named
0was never removed from the list of anchors being parsed. That list grew with each such anchor, and every nested block that declares an anchor copied it. A document of 512 KB took 4 seconds. - Each nesting level copied its block into a new parser, blank and comment lines included. With 128 nested mappings, 128 KB of blank lines took 4 to 5 seconds and nearly 300 MB.
- Without PCRE JIT, stripping a trailing comment scanned a run of spaces once per character of the run. Chomping a block scalar scanned its blank lines once per blank line, so 32,000 blank lines took 9 seconds.
Any code parsing untrusted YAML is affected, with the default flags. On 7.4 and later, that includes #[MapRequestPayload] when the Yaml component is installed and the endpoint does not restrict acceptFormat. A single unauthenticated request with a YAML content type keeps a PHP worker busy until max_execution_time. JSON documents are valid YAML, so a JSON payload sent with a YAML content type is enough for the flow collection case. On 6.4, #[MapRequestPayload] accepts YAML only when the application registers a yaml request format.
Resolution
The end of an embedded mapping is now found first and the mapping is parsed once. The patterns matching keys, tags and comments consume each run of spaces once, and still match the same lines. Scalars of flow collections are matched at their offset instead of in a copy of the rest of the collection. Block scalars are now chomped with rtrim() instead of a pattern. A nested block that has a long run of blank or comment lines is read in place instead of being copied at each nesting level.
An empty sequence item followed by a blank or comment line no longer turns the next item into a nested sequence. For example, "-\n\n- x" now gives [null, "x"] instead of [["x"]]. An anchor named 0 is now handled like any other anchor, so *0 no longer reports a circular reference.
In addition, #[MapRequestPayload] no longer decodes YAML and CSV payloads unless acceptFormat lists them. These formats had been accepted by default since 7.4.0, as a side effect of new media types in Request.
The patches for this issue are available here for branch 5.4 and here for branch 6.4 (and forward-ported to 7.4, 8.1 and 8.2). The #[MapRequestPayload] change is available here for branch 7.4 (and forward-ported to 8.1 and 8.2).
Credits
We would like to thank Sebastian Bergmann for reporting the issue and Nicolas Grekas for finding the related ones and providing the fix.