diff --git a/docs/untrusted-input.md b/docs/untrusted-input.md index 6dbdeaa..536fc1d 100644 --- a/docs/untrusted-input.md +++ b/docs/untrusted-input.md @@ -1,16 +1,16 @@ # Untrusted input YAML from the network, user uploads, or a CI job is hostile until proven -otherwise. saneyaml applies structural resource limits by default, and lets you -tune them per call site. +otherwise. saneyaml applies structural resource limits by default while parsing +YAML, and lets you tune them per call site. > Snippets elide the enclosing function; assume a function returning > `saneyaml::Result<()>`. ## Defaults -Every parser, loader, streaming, lossless, and Serde entry point enforces these -out of the box: +Parser-backed entry points enforce these limits while constructing YAML trees or +streams: | Limit | Default | Rejects | |---|---|---| @@ -24,6 +24,25 @@ out of the box: The defaults accept real-world config (Kubernetes CRDs, OpenAPI, Compose) while rejecting compact bombs that sit under the byte ceiling. +These guarantees apply to input-based APIs such as `parse_*`, `stream_*`, and +lossless parsing, as well as the `LoadOptions` loading and Serde methods. The +top-level `from_str`, `from_slice`, `from_reader`, and document-loading entry +points also inherit these guarantees. A `Node` or `Value` returned by one of +those parser-backed APIs has already been checked when it was constructed. + +## Caller-built trees + +`from_node` and `from_value` deserialize trees supplied by the caller. They do +not accept `LoadOptions` and do not re-run the parser resource limits. The same +applies to direct Serde deserialization from a `Node` or `Value`. If you build or +receive a tree independently, bound or validate its size, nesting, scalar sizes, +collection widths, and any alias or merge expansion before deserializing it. + +Serde still has a separate guard for excessively deep `<<` merge expansion, but +that guard is not a general resource-limit validation pass. saneyaml does not +currently provide an options-aware validator for caller-built trees; such +validation remains the caller's responsibility. + ## Tighten for a specific call Lower a limit when you know your inputs are small: diff --git a/src/de.rs b/src/de.rs index c3e3570..f4fe701 100644 --- a/src/de.rs +++ b/src/de.rs @@ -93,6 +93,13 @@ where } /// Deserializes from an already parsed spanful [`Node`]. +/// +/// This function deserializes the caller-supplied tree directly; it does not +/// re-run the parser resource limits configured by [`LoadOptions`]. A `Node` +/// returned by a parser-backed API has already been checked when it was +/// constructed. If you build or receive the node independently, bound or +/// validate it before calling this function. The separate guard for deeply +/// nested `<<` merge expansion is not a general resource-limit validation pass. pub fn from_node<'de, T>(node: &'de Node) -> crate::Result where T: serde::Deserialize<'de>, @@ -109,11 +116,16 @@ where /// Deserializes from a spanless YAML [`Value`]. /// -/// Unlike parser-backed entrypoints such as [`from_str`], [`from_slice`], and -/// [`from_node`], this path cannot recover the original scalar source spelling -/// for typed `String` or `&str` targets. Non-string [`Value`] scalars therefore -/// stay typed and reject string targets instead of being coerced to their YAML -/// source text. +/// This function deserializes the caller-supplied tree directly; it does not +/// re-run the parser resource limits configured by [`LoadOptions`]. Caller-built +/// values must be bounded or validated independently before deserialization. The +/// separate guard for deeply nested `<<` merge expansion is not a general +/// resource-limit validation pass. +/// +/// Unlike parser-backed entrypoints such as [`from_str`] and [`from_slice`], +/// this path cannot recover the original scalar source spelling for typed +/// `String` or `&str` targets. Non-string [`Value`] scalars therefore stay typed +/// and reject string targets instead of being coerced to their YAML source text. pub fn from_value(value: Value) -> crate::Result where T: DeserializeOwned, diff --git a/tests/dos_hardening.rs b/tests/dos_hardening.rs index cd802c1..a5265c7 100644 --- a/tests/dos_hardening.rs +++ b/tests/dos_hardening.rs @@ -1,4 +1,4 @@ -use saneyaml::{Error, Event, LoadOptions, Node, Value}; +use saneyaml::{Error, Event, LoadOptions, Node, NodeValue, Span, Value}; use serde::Deserialize; use std::fs; use std::io::Cursor; @@ -171,6 +171,32 @@ fn collection_limit_rejects_wide_sequences_and_mappings_with_spans() { } } +#[test] +fn caller_built_trees_do_not_reapply_parser_collection_limits() { + let input = "[null, null]"; + let error = LoadOptions::new() + .max_collection_items(1) + .parse_str(input) + .expect_err("parser-backed input must honor the collection limit"); + assert_limit_error(input, &error, "YAML collection exceeds configured limit"); + + let expected = Value::Sequence(vec![Value::Null, Value::Null]); + let from_value: Value = + saneyaml::from_value(expected.clone()).expect("from_value accepts caller-built trees"); + assert_eq!(from_value, expected); + + let node = Node::new( + NodeValue::Sequence(vec![ + Node::null(Span::default()), + Node::null(Span::default()), + ]), + Span::default(), + ); + let from_node: Value = + saneyaml::from_node(&node).expect("from_node accepts caller-built trees"); + assert_eq!(from_node, expected); +} + #[test] fn alias_bomb_rejects_semantic_loaders_but_raw_events_do_not_expand() { let options = LoadOptions::new().max_alias_expansion_nodes(8);