Skip to content
Draft
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
27 changes: 23 additions & 4 deletions docs/untrusted-input.md
Original file line number Diff line number Diff line change
@@ -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 |
|---|---|---|
Expand All @@ -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:
Expand Down
22 changes: 17 additions & 5 deletions src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>
where
T: serde::Deserialize<'de>,
Expand All @@ -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<T>(value: Value) -> crate::Result<T>
where
T: DeserializeOwned,
Expand Down
28 changes: 27 additions & 1 deletion tests/dos_hardening.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Loading