Skip to content

Unbounded u16 depth counter in NamespaceResolver::push → panic / namespace-scope corruption on deeply nested XML #977

Description

@scadastrangelove

NamespaceResolver::push (src/name.rs) increments its depth counter with an unguarded self.nesting_level += 1; on a u16 field (nesting_level, declared near src/name.rs:514). push is called once per Start/Empty event by NsReader::process_event (src/reader/ns_reader.rs), and there is no depth cap anywhere on the read_event path — so a document with 65 536 nested elements overflows the counter. Note the asymmetry: the matching pop() already saturates (self.set_level(self.nesting_level.saturating_sub(1));, src/name.rs:763), but the increment does not. Reachable from the public NsReader::read_resolved_event* on untrusted XML.

Impact — two facets, depending on build profile:

  • overflow-checks builds (all debug builds; release profiles that opt in): the 65 536-th push panics attempt to add with overflow → remote, pre-auth DoS from a ~490 KB document.
  • Default --release (overflow-checks off): the u16 silently wraps 65535 → 0, after which set_level's scope-truncation (rposition(|n| n.level <= level)) runs on non-monotonic levels and corrupts namespace resolution — bindings from closed scopes leak, in-scope bindings vanish. For any consumer that trusts resolved namespaces (XML-DSig, SOAP action routing, element-by-namespace access decisions) this is a security-relevant correctness bug, not just a crash.

This is live on master (src/name.rs still self.nesting_level += 1; with pop saturating_sub), and is distinct from the closed #970 / #972, which capped declarations per element (max_declarations_per_element / TooManyDeclarations) — this is the orthogonal depth counter.

How to reproduce

cargo new repro && cd repro, add quick-xml = "0.41" to Cargo.toml, drop this into src/main.rs:

use quick_xml::reader::NsReader;
use quick_xml::events::Event;

fn main() {
    let depth = 70_000;
    let xml = "<a>".repeat(depth) + &"</a>".repeat(depth); // ~490 KB
    let mut r = NsReader::from_str(&xml);
    let mut n = 0u64;
    loop {
        match r.read_resolved_event() {
            Ok((_, Event::Eof)) => break,
            Ok(_) => n += 1,
            Err(e) => { eprintln!("Err after {n} events: {e:?}"); break; }
        }
    }
    eprintln!("done, {n} events");
}
  • RUSTFLAGS="-Coverflow-checks=on" cargo run --releasethread 'main' panicked at src/name.rs:...: attempt to add with overflow.

  • Default cargo run --release → no panic, but the resolver is corrupted. Nesting one xmlns:p outside and a different xmlns:p inside a nested <x>, then resolving the same prefix after popping back out:

    depth inner <p:e> (expect BBB) outer <p:f> (expect AAA)
    3 (control) BBB AAA
    65534 BBB BBB ✗ (closed-scope binding leaked)
    ≥65535 BBB Unknown("p") ✗ (in-scope binding vanished)

Suggested fix

A saturating_add (mirroring pop) removes the panic but not the misresolution — at saturation all levels ≥ u16::MAX collapse to one and set_level still over-truncates (verified). The correct fix is a depth cap that returns a clean error at the u16 boundary, in the same spirit as the existing TooManyDeclarations guard. I have a patch (a NamespaceError::TooDeeplyNested variant + checked_add, all existing tests pass incl. --features serialize) and am happy to open a PR.

Found by the rust-in-peace security pipeline.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugnamespacesIssues related to namespaces support

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions