diff --git a/crates/quick-xml/RUSTSEC-0000-0000.md b/crates/quick-xml/RUSTSEC-0000-0000.md new file mode 100644 index 000000000..cdc801aea --- /dev/null +++ b/crates/quick-xml/RUSTSEC-0000-0000.md @@ -0,0 +1,74 @@ +```toml +[advisory] +id = "RUSTSEC-0000-0000" +package = "quick-xml" +date = "2026-06-29" +url = "https://github.com/tafia/quick-xml/issues/969" +references = [ + "https://github.com/tafia/quick-xml/pull/971", + "https://github.com/tafia/quick-xml/commit/07f3db8343cf152f5bc3483ef5b3164582489bea", +] +categories = ["denial-of-service"] +keywords = ["xml", "parser", "dos", "algorithmic-complexity", "quadratic"] +cvss = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + +[versions] +patched = [">= 0.41.0"] +``` + +# Quadratic run time when checking a start tag for duplicate attribute names + +`BytesStart::attributes()` returns an `Attributes` iterator which, by default +(`with_checks(true)`), rejects a start tag that repeats an attribute name. For +each attribute yielded, the iterator compared the new name against every name +seen so far in the same tag using a linear scan, so a start tag with `N` +distinct attribute names cost `O(N²)` byte comparisons. There was no bound on +`N` other than the size of the buffered start tag. + +## Impact + +Any code that parses untrusted XML and iterates a start tag's attributes with +the default duplicate check enabled can be made to spend CPU time quadratic in +the number of attributes on a single tag. Because the check is pure computation +with no `.await`/I/O, an I/O-based timeout on the consumer (for example a read +or request timeout) cannot interrupt it while it runs. + +Measured cost of a single start tag, release build: + +| Attributes on one tag | Time | +|---|---| +| 80,000 | ~6 s | +| 800,000 | ~10 min | + +The cost grows with the square of the attribute count, so a start tag of a few +tens of megabytes can stall a parsing thread for hours. No memory is exhausted +and the parser does not crash; the effect is CPU exhaustion on the thread doing +the parsing: a single crafted start tag can pin a CPU core for minutes to hours, +denying service to that worker. A deployment that places a wall-clock bound on +parsing, or confines it to a non-critical thread, may consider the availability +impact lower. + +## Affected code paths + +* `BytesStart::attributes()` / `Attributes` iterated with checks enabled (the + default), and `BytesStart::try_get_attribute`. +* `NsReader`, which resolves namespaces by iterating a tag's attributes and so + reaches the same check internally. + +Consumers that iterate attributes with `.attributes().with_checks(false)` and do +not use `NsReader` are not affected. + +This was reported as reachable by a remote, unauthenticated attacker in a +real-world RPKI relying party (NLnet Labs Routinator) via a crafted RRDP +`snapshot.xml`. + +## Remediation + +Upgrade to `quick-xml >= 0.41.0`, where the duplicate check keeps the linear +scan for start tags with a small number of attributes and switches to an `O(1)` +hash pre-filter above a threshold, making the whole tag `O(N)`. The reported +`AttrError::Duplicated` positions are unchanged. + +If upgrading is not possible and duplicate-name detection is not required, +disable it with `.attributes().with_checks(false)` (this does not help +`NsReader` consumers, which have no equivalent opt-out before 0.41.0).