rustsec-2026-0194
Vulnerability from osv_rustsec
Published
2026-06-29 12:00
Modified
2026-07-02 07:59
Summary
Quadratic run time when checking a start tag for duplicate attribute names
Details

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


{
  "affected": [
    {
      "database_specific": {
        "categories": [
          "denial-of-service"
        ],
        "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
        "informational": null
      },
      "ecosystem_specific": {
        "affected_functions": null,
        "affects": {
          "arch": [],
          "functions": [],
          "os": []
        }
      },
      "package": {
        "ecosystem": "crates.io",
        "name": "quick-xml",
        "purl": "pkg:cargo/quick-xml"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.0.0-0"
            },
            {
              "fixed": "0.41.0"
            }
          ],
          "type": "SEMVER"
        }
      ],
      "versions": []
    }
  ],
  "aliases": [],
  "database_specific": {
    "license": "CC0-1.0"
  },
  "details": "`BytesStart::attributes()` returns an `Attributes` iterator which, by default\n(`with_checks(true)`), rejects a start tag that repeats an attribute name. For\neach attribute yielded, the iterator compared the new name against every name\nseen so far in the same tag using a linear scan, so a start tag with `N`\ndistinct attribute names cost `O(N\u00b2)` byte comparisons. There was no bound on\n`N` other than the size of the buffered start tag.\n\n## Impact\n\nAny code that parses untrusted XML and iterates a start tag\u0027s attributes with\nthe default duplicate check enabled can be made to spend CPU time quadratic in\nthe number of attributes on a single tag. Because the check is pure computation\nwith no `.await`/I/O, an I/O-based timeout on the consumer (for example a read\nor request timeout) cannot interrupt it while it runs.\n\nMeasured cost of a single start tag, release build:\n\n| Attributes on one tag | Time |\n|---|---|\n| 80,000  | ~6 s   |\n| 800,000 | ~10 min |\n\nThe cost grows with the square of the attribute count, so a start tag of a few\ntens of megabytes can stall a parsing thread for hours. No memory is exhausted\nand the parser does not crash; the effect is CPU exhaustion on the thread doing\nthe parsing: a single crafted start tag can pin a CPU core for minutes to hours,\ndenying service to that worker. A deployment that places a wall-clock bound on\nparsing, or confines it to a non-critical thread, may consider the availability\nimpact lower.\n\n## Affected code paths\n\n* `BytesStart::attributes()` / `Attributes` iterated with checks enabled (the\n  default), and `BytesStart::try_get_attribute`.\n* `NsReader`, which resolves namespaces by iterating a tag\u0027s attributes and so\n  reaches the same check internally.\n\nConsumers that iterate attributes with `.attributes().with_checks(false)` and do\nnot use `NsReader` are not affected.\n\nThis was reported as reachable by a remote, unauthenticated attacker in a\nreal-world RPKI relying party (NLnet Labs Routinator) via a crafted RRDP\n`snapshot.xml`.\n\n## Remediation\n\nUpgrade to `quick-xml \u003e= 0.41.0`, where the duplicate check keeps the linear\nscan for start tags with a small number of attributes and switches to an `O(1)`\nhash pre-filter above a threshold, making the whole tag `O(N)`. The reported\n`AttrError::Duplicated` positions are unchanged.\n\nIf upgrading is not possible and duplicate-name detection is not required,\ndisable it with `.attributes().with_checks(false)` (this does not help\n`NsReader` consumers, which have no equivalent opt-out before 0.41.0).",
  "id": "RUSTSEC-2026-0194",
  "modified": "2026-07-02T07:59:25Z",
  "published": "2026-06-29T12:00:00Z",
  "references": [
    {
      "type": "PACKAGE",
      "url": "https://crates.io/crates/quick-xml"
    },
    {
      "type": "ADVISORY",
      "url": "https://rustsec.org/advisories/RUSTSEC-2026-0194.html"
    },
    {
      "type": "REPORT",
      "url": "https://github.com/tafia/quick-xml/issues/969"
    },
    {
      "type": "WEB",
      "url": "https://github.com/tafia/quick-xml/pull/971"
    },
    {
      "type": "WEB",
      "url": "https://github.com/tafia/quick-xml/commit/07f3db8343cf152f5bc3483ef5b3164582489bea"
    }
  ],
  "related": [],
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Quadratic run time when checking a start tag for duplicate attribute names"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…