Bump quick-xml to 0.41: the namespace-declaration DoS was reachable (RUSTSEC-2026-0195) (#363)

`crates/docx/src/xml.rs` parses untrusted document XML through
`quick_xml::reader::NsReader`. In 0.38.4 that is RUSTSEC-2026-0195 (CVSS 7.5): the
namespace resolver pushes a binding for every `xmlns` / `xmlns:*` declaration on a
start tag into an unbounded `Vec`, with no limit on the number of declarations.

None of the reader's existing caps bound that path - MAX_PART (256 MiB), MAX_TOTAL
(512 MiB), MAX_ENTRIES, MAX_DEPTH and MAX_ELEMENTS bound part size, entry count,
nesting and element count, not declarations per tag.

Measured before this change, `wordcraft-cli convert` on a crafted .docx whose
`word/document.xml` is one start tag with 21,845,333 `xmlns:a="u"` declarations:

  508 KB on disk -> 262 MB decompressed (under MAX_PART) -> 1.19 GB peak RSS,
  exit 0: the document opened and converted without any warning.

At the MAX_PART/MAX_TOTAL ceiling that is roughly 2.4 GB of resident memory from
about a megabyte of download, i.e. an OOM kill with unsaved work open. Nothing
panics, so the `Session::run` guard does not catch it.

0.41.0 caps namespace declarations per start tag at 256
(`DEFAULT_MAX_DECLARATIONS_PER_ELEMENT`, raisable through
`NamespaceResolver::set_max_declarations_per_element`) and returns
`NamespaceError::TooManyDeclarations` beyond it. The same fixture now fails in
about a second with a clear error and no namespace allocation:

  wordcraft-cli: malformed XML: start tag declares more than 256 namespace
  bindings; raise the limit with NamespaceResolver::set_max_declarations_per_element

Two API adaptations for 0.41 (0.x bumps are allowed to break):
- `resolve_attribute` moved off `NsReader` on to the resolver:
  `r.resolve_attribute(key)` -> `r.resolver_mut().resolve_attribute(key)`.
- `Attribute::unescape_value` is deprecated in favour of `normalized_value`, which
  also applies XML attribute-value normalization as the spec requires; attribute
  values in a Word part are UTF-8 here (UTF-16 parts are transcoded first).

Regression test: `crates/docx/src/xml.rs::too_many_namespace_declarations_is_error`
asserts a tag with 10,000 declarations is an error. It fails on 0.38.4 and passes
on 0.41.0.

This also clears the second quick-xml advisory, RUSTSEC-2026-0194
(quadratic duplicate-attribute scan). That one was not reachable here - the reader
already passes `with_checks(false)` at xml.rs:257, and a 60,000-attribute tag parses
in the same 65 ms as a 20,000-attribute one - but `cargo audit` is clean afterwards.

Verified: `cargo xtask ci`; `cargo audit` (no vulnerabilities left, the two
unmaintained-crate warnings are `krilla`'s `rustybuzz`/`ttf-parser` and are
unrelated); 32 docx tests plus the malformed-input suite; and 21 crafted hostile
documents through `wordcraft-cli convert` with zero panics before and after.

One residual, unchanged by this commit: the fixture above still costs about 509 MB,
because MAX_PART lets one part decompress to 256 MiB and every part is held in memory
while the package is read. That is the existing, intended bound; what this commit
removes is the namespace allocation on top of it.
This commit is contained in:
pkibiego
2026-10-10 18:04:11 -04:00
committed by GitHub
parent 15f2f01475
commit 5c970f90bd
4 changed files with 22 additions and 16 deletions
Generated
+3 -12
View File
@@ -3096,15 +3096,6 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.41.0"
@@ -4404,7 +4395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
dependencies = [
"proc-macro2",
"quick-xml 0.41.0",
"quick-xml",
"quote",
]
@@ -5079,7 +5070,7 @@ dependencies = [
"image",
"log",
"proptest",
"quick-xml 0.38.4",
"quick-xml",
"sha1",
"sha2",
"thiserror 2.0.21",
@@ -5133,7 +5124,7 @@ dependencies = [
"image",
"log",
"proptest",
"quick-xml 0.38.4",
"quick-xml",
"wordcraft-doc",
"wordcraft-geom",
"zip",