From 40a20325e17dae44fc8567b7d113eca7964649a1 Mon Sep 17 00:00:00 2001 From: Wojciech Polak Date: Fri, 3 Jul 2026 12:29:23 +0200 Subject: [PATCH] Fix: Nested rule parser Nested at-rules and style rules are parsed by recursive descent with no depth bound, so untrusted input with deep nesting overflows the thread stack and aborts the process. Enforce MAX_NESTING_DEPTH (32) in NestedRuleParser::parse_nested, returning ParserError::MaximumNestingDepth once exceeded (or skipping the rule under error recovery). The limit fits within a 512 KiB stack in release builds; real CSS never nests this deep. This is not a real problem for parcel bundler but may be for using `lightningcss` in other applications like email processing --- src/lib.rs | 39 +++++++++++++++++++++++++++++++++++++++ src/parser.rs | 20 ++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index bcd47649..3fd59a2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, diff --git a/src/parser.rs b/src/parser.rs index aea80b12..9b9b98cb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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 `