Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

### New Features

- [#978]: Add `Deserializer::recursion_limit` to bound the recursion depth
of the serde `Deserializer` (defaults to 128, matching `serde_json`).

### Bug Fixes

- [#977]: `NamespaceResolver::push` (and hence every `NsReader` `Start`/`Empty`
Expand All @@ -24,10 +27,21 @@
`u16` depth counter. Previously the unguarded `nesting_level += 1` panicked
under `overflow-checks` builds and silently wrapped in release, corrupting
namespace-scope bookkeeping on deeply nested untrusted input.
- [#978]: The serde `Deserializer` now returns the new
`DeError::TooDeeplyNested` instead of overflowing the native call stack
and aborting the process when deserializing a struct, a `Vec<T>`-shaped
sequence of structs, or an enum variant nested deeper than the configured
recursion limit. Previously none of `deserialize_struct`, `deserialize_seq`,
or `deserialize_enum` bounded the recursion depth of their respective
re-entrant `MapAccess`/`SeqAccess`/`EnumAccess`/`VariantAccess`
implementations, so a maliciously deeply nested (but otherwise
well-formed) untrusted XML document could crash the whole process in a
way that could not be caught as a `Result::Err`.

### Misc Changes

[#977]: https://github.com/tafia/quick-xml/issues/977
[#978]: https://github.com/tafia/quick-xml/issues/978


## 0.41.0 -- 2026-06-29
Expand Down
34 changes: 25 additions & 9 deletions src/de/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
de::resolver::EntityResolver,
de::simple_type::SimpleTypeDeserializer,
de::text::TextDeserializer,
de::{DeEvent, Deserializer, XmlRead, TEXT_KEY, VALUE_KEY},
de::{DeEvent, DepthGuard, Deserializer, XmlRead, TEXT_KEY, VALUE_KEY},
errors::serialize::DeError,
errors::Error,
events::attributes::IterState,
Expand Down Expand Up @@ -173,7 +173,13 @@ where
{
/// Tag -- owner of attributes
start: BytesStart<'de>,
de: &'d mut Deserializer<'de, R, E>,
/// A guard that both grants access to the deserializer and bounds the
/// recursion depth for as long as this element's map/struct scope is
/// alive (dropping it, e.g. when this `ElementMapAccess` is dropped,
/// releases one level of the recursion-depth budget). See
/// [`Deserializer::recursion_limit`] and
/// <https://github.com/tafia/quick-xml/issues/978>.
de: DepthGuard<'d, 'de, R, E>,
/// State of the iterator over attributes. Contains the next position in the
/// inner `start` slice, from which next attribute should be parsed.
iter: IterState,
Expand All @@ -200,21 +206,29 @@ where
R: XmlRead<'de>,
E: EntityResolver,
{
/// Create a new ElementMapAccess
/// Create a new ElementMapAccess.
///
/// Returns [`DeError::TooDeeplyNested`] instead of constructing the
/// accessor if doing so would exceed the deserializer's configured
/// recursion-depth limit -- every struct/map scope opened here can, via
/// a recursive user type, re-enter this constructor, and each level adds
/// unbounded native call-stack frames otherwise (see
/// <https://github.com/tafia/quick-xml/issues/978>).
pub fn new(
de: &'d mut Deserializer<'de, R, E>,
start: BytesStart<'de>,
fields: &'static [&'static str],
) -> Self {
Self {
) -> Result<Self, DeError> {
let de = de.enter_depth()?;
Ok(Self {
de,
iter: IterState::new(start.name().as_ref().len(), false),
start,
source: ValueSource::Unknown,
fields,
has_value_field: fields.contains(&VALUE_KEY),
has_text_field: fields.contains(&TEXT_KEY),
}
})
}

/// Determines if subtree started with the specified event should be skipped.
Expand Down Expand Up @@ -800,7 +814,9 @@ where
V: Visitor<'de>,
{
match self.map.de.next()? {
DeEvent::Start(e) => visitor.visit_map(ElementMapAccess::new(self.map.de, e, fields)),
DeEvent::Start(e) => {
visitor.visit_map(ElementMapAccess::new(&mut *self.map.de, e, fields)?)
}
DeEvent::Text(e) => {
SimpleTypeDeserializer::from_text_content(e).deserialize_struct("", fields, visitor)
}
Expand Down Expand Up @@ -997,7 +1013,7 @@ where
DeEvent::Start(start) => seed
.deserialize(ElementDeserializer {
start,
de: self.map.de,
de: &mut *self.map.de,
})
.map(Some),
// SAFETY: we just checked that the next event is Start
Expand Down Expand Up @@ -1143,7 +1159,7 @@ where
where
V: Visitor<'de>,
{
visitor.visit_map(ElementMapAccess::new(self.de, self.start, fields))
visitor.visit_map(ElementMapAccess::new(self.de, self.start, fields)?)
}

fn deserialize_enum<V>(
Expand Down
128 changes: 125 additions & 3 deletions src/de/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2128,7 +2128,7 @@ use std::io::BufRead;
use std::mem::replace;
#[cfg(feature = "overlapped-lists")]
use std::num::NonZeroUsize;
use std::ops::{Deref, Range};
use std::ops::{Deref, DerefMut, Range};

/// Data represented by a text node or a CDATA node. XML markup is not expected
pub(crate) const TEXT_KEY: &str = "$text";
Expand Down Expand Up @@ -2574,6 +2574,63 @@ where

/// Buffer to store attribute name as a field name exposed to serde consumers
key_buf: String,

/// Current recursion depth of nested structs, sequences-of-structs and
/// enum variants. Every level of such nesting adds native call-stack
/// frames that are not otherwise bounded (see [`Self::max_depth`] and
/// <https://github.com/tafia/quick-xml/issues/978>).
depth: usize,
/// Maximum allowed value of [`Self::depth`] before [`DeError::TooDeeplyNested`]
/// is returned. Configurable via [`Self::recursion_limit`].
max_depth: usize,
}

/// Default value of [`Deserializer::max_depth`], matching `serde_json`'s
/// default recursion limit.
const DEFAULT_RECURSION_LIMIT: usize = 128;

/// RAII guard that decrements [`Deserializer::depth`] when dropped. Ensures
/// the recursion-depth counter stays balanced across early returns (`?`) and
/// panics unwinding through the guarded recursive call, not just the
/// successful-return path.
struct DepthGuard<'a, 'de, R, E>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that type necessary? It seems to me that ElementMapAccess can fulfill its goals. It is created for each element, so nesting should be possible only via it.

where
R: XmlRead<'de>,
E: EntityResolver,
{
de: &'a mut Deserializer<'de, R, E>,
}

impl<'a, 'de, R, E> Drop for DepthGuard<'a, 'de, R, E>
where
R: XmlRead<'de>,
E: EntityResolver,
{
fn drop(&mut self) {
self.de.depth -= 1;
}
}

impl<'a, 'de, R, E> Deref for DepthGuard<'a, 'de, R, E>
where
R: XmlRead<'de>,
E: EntityResolver,
{
type Target = Deserializer<'de, R, E>;

fn deref(&self) -> &Self::Target {
self.de
}
}

impl<'a, 'de, R, E> DerefMut for DepthGuard<'a, 'de, R, E>
where
R: XmlRead<'de>,
E: EntityResolver,
{
fn deref_mut(&mut self) -> &mut Self::Target {
self.de
}
Comment on lines +2614 to +2633

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If DepthGuard will kept, it seems to me, that it would better to use de field directly instead of via traits.

Suggested change
impl<'a, 'de, R, E> Deref for DepthGuard<'a, 'de, R, E>
where
R: XmlRead<'de>,
E: EntityResolver,
{
type Target = Deserializer<'de, R, E>;
fn deref(&self) -> &Self::Target {
self.de
}
}
impl<'a, 'de, R, E> DerefMut for DepthGuard<'a, 'de, R, E>
where
R: XmlRead<'de>,
E: EntityResolver,
{
fn deref_mut(&mut self) -> &mut Self::Target {
self.de
}

}

impl<'de, R, E> Deserializer<'de, R, E>
Expand Down Expand Up @@ -2602,7 +2659,26 @@ where
peek: None,

key_buf: String::new(),

depth: 0,
max_depth: DEFAULT_RECURSION_LIMIT,
}
}

/// Checks and increments the recursion-depth counter, returning a
/// [`DepthGuard`] that decrements it again when dropped. Returns
/// [`DeError::TooDeeplyNested`] (without incrementing) if the configured
/// [`Self::max_depth`] (see [`Self::recursion_limit`]) would be exceeded.
///
/// Must be called once per level of struct/sequence/enum nesting that
/// re-enters the deserializer, so that sibling (as opposed to nested)
/// elements do not spuriously accumulate depth.
fn enter_depth(&mut self) -> Result<DepthGuard<'_, 'de, R, E>, DeError> {
if self.depth >= self.max_depth {
return Err(DeError::TooDeeplyNested(self.max_depth));
}
self.depth += 1;
Ok(DepthGuard { de: self })
}

/// Returns `true` if all events was consumed.
Expand Down Expand Up @@ -2716,6 +2792,46 @@ where
self
}

/// Set the maximum recursion depth allowed while deserializing nested
/// structs, sequences of structs (`Vec<T>` where `T` is a struct), and
/// enum variants.
///
/// Each level of such nesting re-enters the deserializer and adds native
/// call-stack frames that are not otherwise bounded. Deserializing a
/// maliciously deeply nested untrusted XML document can overflow the
/// native stack, which aborts the process -- this is a [DoS] that cannot
/// be caught as a [`Result::Err`], unlike ordinary parse errors. This
/// limit turns that abort into a clean [`DeError::TooDeeplyNested`].
///
/// Defaults to 128, matching `serde_json`'s default recursion limit.
///
/// # Examples
///
/// ```
/// use quick_xml::de::Deserializer;
/// use quick_xml::DeError;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Node {
/// #[serde(default, rename = "Node")]
/// node: Vec<Node>,
/// }
///
/// let xml = "<Node>".repeat(1000) + &"</Node>".repeat(1000);
/// let mut de = Deserializer::from_str(&xml);
/// de.recursion_limit(100);
///
/// let result = Node::deserialize(&mut de);
/// assert!(matches!(result, Err(DeError::TooDeeplyNested(100))));
Comment on lines +2821 to +2826

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, that 10 and 11 are enough for demonstration purposes

Suggested change
/// let xml = "<Node>".repeat(1000) + &"</Node>".repeat(1000);
/// let mut de = Deserializer::from_str(&xml);
/// de.recursion_limit(100);
///
/// let result = Node::deserialize(&mut de);
/// assert!(matches!(result, Err(DeError::TooDeeplyNested(100))));
/// let xml = "<Node>".repeat(11) + &"</Node>".repeat(11);
/// let mut de = Deserializer::from_str(&xml);
///
/// de.recursion_limit(11);
/// let _ = Node::deserialize(&mut de).unwrap();
///
/// de.recursion_limit(10);
/// let result = Node::deserialize(&mut de);
/// assert!(matches!(result, Err(DeError::TooDeeplyNested(10))));

/// ```
///
/// [DoS]: https://en.wikipedia.org/wiki/Denial-of-service_attack
pub fn recursion_limit(&mut self, limit: usize) -> &mut Self {
self.max_depth = limit;
self
}

#[cfg(feature = "overlapped-lists")]
fn peek(&mut self) -> Result<&DeEvent<'de>, DeError> {
if self.read.is_empty() {
Expand Down Expand Up @@ -3230,7 +3346,7 @@ where
// When document is pretty-printed there could be whitespaces before the root element
self.skip_whitespaces()?;
match self.next()? {
DeEvent::Start(e) => visitor.visit_map(ElementMapAccess::new(self, e, fields)),
DeEvent::Start(e) => visitor.visit_map(ElementMapAccess::new(self, e, fields)?),
// SAFETY: The reader is guaranteed that we don't have unmatched tags
// If we here, then our deserializer has a bug
DeEvent::End(e) => unreachable!("{:?}", e),
Expand Down Expand Up @@ -3305,7 +3421,13 @@ where
// which represents the enum variant
// Checked by `top_level::list_of_enum` test in serde-de-seq
self.skip_whitespaces()?;
visitor.visit_enum(var::EnumAccess::new(self))
// Bounds recursion depth: a newtype variant wrapping the same enum
// type (`enum E { Wrap(Box<E>) }`) re-enters this method through
// `VariantAccess::newtype_variant_seed`, adding native call-stack
// frames per level of XML nesting with no other bound.
// See https://github.com/tafia/quick-xml/issues/978
let mut guard = self.enter_depth()?;
visitor.visit_enum(var::EnumAccess::new(&mut *guard))
}

fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, DeError>
Expand Down
2 changes: 1 addition & 1 deletion src/de/var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ where
V: Visitor<'de>,
{
match self.de.next()? {
DeEvent::Start(e) => visitor.visit_map(ElementMapAccess::new(self.de, e, fields)),
DeEvent::Start(e) => visitor.visit_map(ElementMapAccess::new(self.de, e, fields)?),
DeEvent::Text(e) => {
SimpleTypeDeserializer::from_text_content(e).deserialize_struct("", fields, visitor)
}
Expand Down
17 changes: 17 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,18 @@ pub mod serialize {
/// exceeded. The limit was provided as an argument
#[cfg(feature = "overlapped-lists")]
TooManyEvents(NonZeroUsize),
/// The document nests structs, sequences-of-structs, or enum variants
/// more deeply than the configured recursion limit allows.
///
/// Each level of such nesting adds native call-stack frames while
/// deserializing (through `MapAccess`/`EnumAccess`/`VariantAccess`
/// re-entering the deserializer), which are not otherwise bounded.
/// Without this limit, a sufficiently deeply nested untrusted document
/// can overflow the native stack, aborting the process in a way that
/// cannot be caught as a [`Result::Err`]. The contained value is the
/// limit that was exceeded; see
/// [`Deserializer::recursion_limit`](crate::de::Deserializer::recursion_limit).
TooDeeplyNested(usize),
}

impl fmt::Display for DeError {
Expand All @@ -365,6 +377,11 @@ pub mod serialize {
Self::UnexpectedEof => f.write_str("unexpected `Event::Eof`"),
#[cfg(feature = "overlapped-lists")]
Self::TooManyEvents(s) => write!(f, "deserializer buffered {} events, limit exceeded", s),
Self::TooDeeplyNested(limit) => write!(
f,
"input document is nested too deeply, recursion limit ({}) exceeded",
limit
),
}
}
}
Expand Down
Loading
Loading