iotaledger / iotaledger/identity

RUSTSEC-2026-0194: Quadratic run time when checking a start tag for duplicate attribute names

Open Beginner friendly
#1,832 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
346
Forks
100
Avg merge
13h 41m
Merged PRs (30d)
2

Description

> Quadratic run time when checking a start tag for duplicate attribute names

| Details | |
| ------------------- | ---------------------------------------------- |
| Package | `quick-xml` |
| Version | `0.39.4` |
| URL | [https://github.com/tafia/quick-xml/issues/969](https://github.com/tafia/quick-xml/issues/969) |
| Date | 2026-06-29 |
| Patched versions | `>=0.41.0` |

`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).

See [advisory page](https://rustsec.org/advisories/RUSTSEC-2026-0194.html) for additional details.

Contributor guide

Open the contributing guide

Research direction

Start by locating the quick-xml dependency in the repository's dependency manifests and lockfile, then check where XML parsing reaches BytesStart::attributes(), BytesStart::try_get_attribute(), or NsReader. Upgrade quick-xml to >=0.41.0 and run the repository's test suite; done means the patched dependency is selected and existing tests pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
security
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
64/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.