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
5 changes: 3 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ The default loader posture is bounded for untrusted YAML inputs:
- `LoadOptions` applies a default 64 MiB input byte ceiling.
- Callers can tune `max_input_bytes()` or explicitly opt out with
`without_input_limit()` only after bounding the source themselves.
- Alias expansion uses an input-derived budget by default and can be tuned with
`max_alias_expansion_nodes()`.
- Alias expansion uses an input-derived aggregate budget by default and can be
tuned with `max_alias_expansion_nodes()`. The budget accumulates clone work,
scalar bytes, and collection-item work across the complete input stream.
- Recursive aliases are rejected.
- Default nesting, scalar-size, and collection-item limits protect parser,
loader, Serde, and lossless entrypoints from unbounded structural work:
Expand Down
2 changes: 1 addition & 1 deletion docs/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ The defended input is untrusted YAML at every load entrypoint. With default
`LoadOptions`, the crate rejects:

- input above **64 MiB** before parsing,
- alias-expansion bombs (input-derived budget) and recursive aliases,
- alias-expansion bombs (input-derived aggregate work budget) and recursive aliases,
- nesting beyond **128**, scalars above **1 MiB**, and collections above
**16,384** entries,

Expand Down
6 changes: 4 additions & 2 deletions docs/untrusted-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ out of the box:
| Nesting depth | 128 | Deeply nested block/flow bombs |
| Scalar size | 1 MiB | Single giant scalars |
| Collection size | 16,384 entries | Wide sequence/mapping bombs |
| Alias expansion | input-derived budget | Billion-laughs alias bombs |
| Alias expansion | input-derived aggregate work budget | Billion-laughs alias bombs and oversized alias materialization |
| Recursive aliases | — | always rejected |

The defaults accept real-world config (Kubernetes CRDs, OpenAPI, Compose) while
Expand All @@ -39,7 +39,9 @@ let cfg: Config = LoadOptions::new()
```

All knobs: `max_input_bytes`, `max_alias_expansion_nodes`, `max_nesting_depth`,
`max_scalar_bytes`, `max_collection_items`.
`max_scalar_bytes`, `max_collection_items`. The alias setting applies to the
aggregate stream work across clone operations, scalar bytes, and collection
items; it is not reset between documents.

## Relax — only when you've bounded the source yourself

Expand Down
24 changes: 12 additions & 12 deletions src/event_de/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,29 @@ use crate::{
Event, EventMeta, ScalarStyle, merge_policy_for_schema, parse_scalar_with_schema,
schema_for_directives,
},
schema::{LoadOptions, Schema},
schema::{AliasExpansionBudget, LoadOptions, Schema},
};
use serde::de::{
self, DeserializeOwned, DeserializeSeed, EnumAccess, IntoDeserializer, MapAccess, SeqAccess,
VariantAccess, Visitor,
};
use std::{collections::HashMap, io::Read, marker::PhantomData};
use std::{cell::Cell, collections::HashMap, io::Read, marker::PhantomData};

#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn from_str_with_options<'de, T>(input: &'de str, options: LoadOptions) -> Result<T>
where
T: serde::Deserialize<'de>,
{
let configured_schema = options.selected_schema();
let replay_budget = options.alias_expansion_budget(input.len());
let alias_budget = Cell::new(options.alias_expansion_budget(input.len()));
let max_nesting_depth = options.selected_max_nesting_depth();
let events = crate::parse::EventStream::from_str_with_options(input, options)?
.collect::<Result<Vec<_>>>()?;
let mut source = EventSource::new(
input,
events,
configured_schema,
replay_budget,
&alias_budget,
max_nesting_depth,
);
source.enter_stream()?;
Expand Down Expand Up @@ -69,13 +69,13 @@ where
T: serde::Deserialize<'de>,
{
let configured_schema = options.selected_schema();
let replay_budget = options.alias_expansion_budget(input.len());
let alias_budget = Cell::new(options.alias_expansion_budget(input.len()));
let max_nesting_depth = options.selected_max_nesting_depth();
Ok(EventDocumentIter {
input,
frames: EventDocumentFrames::from_str_with_options(input, options)?,
configured_schema,
replay_budget,
alias_budget,
max_nesting_depth,
_marker: PhantomData,
})
Expand Down Expand Up @@ -111,14 +111,14 @@ where
)
})?;
let configured_schema = options.selected_schema();
let replay_budget = options.alias_expansion_budget(input.len());
let alias_budget = Cell::new(options.alias_expansion_budget(input.len()));
let max_nesting_depth = options.selected_max_nesting_depth();
let frames = EventDocumentFrames::from_str_with_options(&input, options)?;
Ok(OwnedEventDocumentIter {
input,
frames,
configured_schema,
replay_budget,
alias_budget,
max_nesting_depth,
_marker: PhantomData,
})
Expand All @@ -128,7 +128,7 @@ pub(crate) struct EventDocumentIter<'de, T> {
input: &'de str,
frames: EventDocumentFrames,
configured_schema: Schema,
replay_budget: usize,
alias_budget: Cell<AliasExpansionBudget>,
max_nesting_depth: Option<usize>,
_marker: PhantomData<T>,
}
Expand All @@ -148,7 +148,7 @@ where
self.input,
events,
self.configured_schema,
self.replay_budget,
&self.alias_budget,
self.max_nesting_depth,
)
})
Expand All @@ -161,7 +161,7 @@ pub(crate) struct OwnedEventDocumentIter<T> {
input: String,
frames: EventDocumentFrames,
configured_schema: Schema,
replay_budget: usize,
alias_budget: Cell<AliasExpansionBudget>,
max_nesting_depth: Option<usize>,
_marker: PhantomData<T>,
}
Expand All @@ -181,7 +181,7 @@ where
&self.input,
events,
self.configured_schema,
self.replay_budget,
&self.alias_budget,
self.max_nesting_depth,
)
})
Expand Down
25 changes: 12 additions & 13 deletions src/event_de/serde_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ use super::prepared::*;
use super::source::EventSource;
use super::*;

pub(super) struct EventNodeDeserializer<'a, 'de> {
pub(super) source: &'a mut EventSource<'de>,
pub(super) struct EventNodeDeserializer<'a, 'de, 'budget> {
pub(super) source: &'a mut EventSource<'de, 'budget>,
}

impl<'de> EventNodeDeserializer<'_, 'de> {
impl<'de, 'budget> EventNodeDeserializer<'_, 'de, 'budget> {
fn deserialize_prepared_current_node<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
Expand Down Expand Up @@ -38,7 +38,7 @@ impl<'de> EventNodeDeserializer<'_, 'de> {
}
}

impl<'de> de::Deserializer<'de> for EventNodeDeserializer<'_, 'de> {
impl<'de, 'budget> de::Deserializer<'de> for EventNodeDeserializer<'_, 'de, 'budget> {
type Error = Error;

fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
Expand Down Expand Up @@ -397,7 +397,7 @@ impl<'de> de::Deserializer<'de> for EventNodeDeserializer<'_, 'de> {
}
}

impl EventSource<'_> {
impl<'de, 'budget> EventSource<'de, 'budget> {
fn peek_has_yaml_core_tag(&self, suffixes: &[&str]) -> bool {
match self.peek() {
Some(Event::SequenceStart { meta, .. }) | Some(Event::MappingStart { meta, .. }) => {
Expand All @@ -424,12 +424,12 @@ impl EventSource<'_> {
}
}

struct EventSeqAccess<'a, 'de> {
source: &'a mut EventSource<'de>,
struct EventSeqAccess<'a, 'de, 'budget> {
source: &'a mut EventSource<'de, 'budget>,
index: usize,
}

impl<'de> SeqAccess<'de> for EventSeqAccess<'_, 'de> {
impl<'de, 'budget> SeqAccess<'de> for EventSeqAccess<'_, 'de, 'budget> {
type Error = Error;

fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>>
Expand All @@ -450,12 +450,12 @@ impl<'de> SeqAccess<'de> for EventSeqAccess<'_, 'de> {
}
}

struct EventMapAccess<'a, 'de> {
source: &'a mut EventSource<'de>,
struct EventMapAccess<'a, 'de, 'budget> {
source: &'a mut EventSource<'de, 'budget>,
value: Option<ErrorPathSegment>,
}

impl<'de> MapAccess<'de> for EventMapAccess<'_, 'de> {
impl<'de, 'budget> MapAccess<'de> for EventMapAccess<'_, 'de, 'budget> {
type Error = Error;

fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>>
Expand All @@ -469,10 +469,9 @@ impl<'de> MapAccess<'de> for EventMapAccess<'_, 'de> {
let depth = self.source.depth;
let (events, pos) = self.source.current_events_and_pos();
let mut scan_anchors = self.source.anchors.clone();
let mut replayed_events = 0usize;
let segment = self
.source
.mapping_key_at(events, pos, &mut scan_anchors, &mut replayed_events, depth)?
.mapping_key_at(events, pos, &mut scan_anchors, depth)?
.map(|(node, _)| path_segment_for_node(&node))
.unwrap_or(ErrorPathSegment::ComplexKey);
self.value = Some(segment.clone());
Expand Down
Loading
Loading