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
39 changes: 39 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,45 @@ mod tests {
}
}

fn nested_at_rules(rule: &str, n: usize) -> String {
format!("{}a{{color:red}}{}", format!("{rule}{{").repeat(n), "}".repeat(n))
}

#[test]
fn nesting_depth_limit() {
// Well-formed CSS nested below the limit still parses.
assert!(StyleSheet::parse(&nested_at_rules("@media (min-width:1px)", 16), ParserOptions::default()).is_ok());

// Deep attacker-controlled nesting is rejected instead of overflowing the stack.
error_test(
&nested_at_rules("@media (min-width:1px)", 200),
ParserError::MaximumNestingDepth,
);
error_test(
&nested_at_rules("@supports (color:red)", 200),
ParserError::MaximumNestingDepth,
);
error_test(&nested_at_rules(".a", 200), ParserError::MaximumNestingDepth);

// The sanitizer path parses with error recovery on; recovery must not overflow either.
error_recovery_test(&nested_at_rules("@media (min-width:1px)", 200));

// Prove the guard prevents a stack overflow on a small (512 KiB) thread stack.
// Only meaningful for release frame sizes; debug frames are far larger and unrepresentative.
#[cfg(not(debug_assertions))]
std::thread::Builder::new()
.stack_size(512 * 1024)
.spawn(|| {
error_test(
&nested_at_rules("@media (min-width:1px)", 500),
ParserError::MaximumNestingDepth,
);
})
.unwrap()
.join()
.unwrap();
}

fn css_modules_error_test(source: &str, error: ParserError) {
let res = StyleSheet::parse(
&source,
Expand Down
20 changes: 20 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ use cssparser::*;
use parcel_selectors::parser::{NestingRequirement, ParseErrorRecovery};
use std::sync::{Arc, RwLock};

/// Maximum at-rule / style-rule nesting depth accepted by the parser.
///
/// Nested rules (`@media`, `@supports`, nested style rules, ...) are parsed by
/// recursive descent, so a `<style>` block with attacker-controlled nesting (e.g. a
/// hostile email) could otherwise recurse until the thread stack overflows and the
/// process aborts.
/// Legitimate CSS never nests at-rules this deep.
const MAX_NESTING_DEPTH: usize = 32;

bitflags! {
/// Parser feature flags to enable.
#[derive(Clone, Debug, Default)]
Expand Down Expand Up @@ -165,6 +174,7 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> TopLevelRuleParser<'a,
rules: &mut self.rules,
is_in_style_rule: false,
allow_declarations: false,
depth: 0,
}
}
}
Expand Down Expand Up @@ -444,6 +454,7 @@ pub struct NestedRuleParser<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> {
rules: &'a mut CssRuleList<'i, T::AtRule>,
is_in_style_rule: bool,
allow_declarations: bool,
depth: usize,
}

impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> NestedRuleParser<'a, 'o, 'i, T> {
Expand All @@ -452,6 +463,12 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> NestedRuleParser<'a, 'o
input: &mut Parser<'i, 't>,
is_style_rule: bool,
) -> Result<(DeclarationBlock<'i>, CssRuleList<'i, T::AtRule>), ParseError<'i, ParserError<'i>>> {
// Every deeply-recursive parse path funnels through here, so guarding it bounds the
// overall nesting depth and prevents a stack overflow on hostile input.
if self.depth >= MAX_NESTING_DEPTH {
return Err(input.new_custom_error(ParserError::MaximumNestingDepth));
}

let mut rules = CssRuleList(vec![]);
let mut nested_parser = NestedRuleParser {
options: self.options,
Expand All @@ -461,6 +478,7 @@ impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> NestedRuleParser<'a, 'o
rules: &mut rules,
is_in_style_rule: self.is_in_style_rule || is_style_rule,
allow_declarations: self.allow_declarations || self.is_in_style_rule || is_style_rule,
depth: self.depth + 1,
};

let parse_declarations = nested_parser.parse_declarations();
Expand Down Expand Up @@ -1125,6 +1143,7 @@ pub fn parse_rule_list<'a, 'o, 'i, 't, T: crate::traits::AtRuleParser<'i>>(
rules: &mut CssRuleList(Vec::new()),
is_in_style_rule: false,
allow_declarations: false,
depth: 0,
};

let (_, rules) = parser.parse_nested(input, false)?;
Expand All @@ -1145,6 +1164,7 @@ pub fn parse_style_block<'a, 'o, 'i, 't, T: crate::traits::AtRuleParser<'i>>(
rules: &mut CssRuleList(Vec::new()),
is_in_style_rule: is_nested,
allow_declarations: true,
depth: 0,
};

parser.parse_style_block(input)
Expand Down