Policy-driven CSS sanitization on top of lightningcss.
This crate exposes lightningcss directly and lets you sanitize rules, selectors,
properties, and descriptors through a custom policy trait. It is an AST policy engine,
not a built-in preset sanitizer.
[dependencies]
css-sanitizer = "0.1.4"cargo run --example sanitize_stringsCssSanitizationPolicyis the main extension point.clean_declaration_list_with_policy()andclean_stylesheet_with_policy()parse, sanitize, and serialize strings.sanitize_declaration_block_ast()andsanitize_stylesheet_ast()mutate parsedlightningcssASTs in place.lightningcssis re-exported so callers can work against the same AST types.
Default trait methods are fail-open. If you want a strict sanitizer, your policy must
explicitly return NodeAction::Drop for anything you do not want to keep.
use css_sanitizer::{
clean_stylesheet_with_policy, CssSanitizationPolicy, NodeAction, PropertyContext,
RuleContext,
};
use css_sanitizer::lightningcss::rules::CssRule;
struct StyleColorOnly;
impl CssSanitizationPolicy for StyleColorOnly {
fn visit_rule(
&self,
rule: &mut CssRule<'_>,
_ctx: RuleContext,
) -> NodeAction {
match rule {
CssRule::Style(_) => NodeAction::Continue,
_ => NodeAction::Drop,
}
}
fn visit_property(
&self,
property: &mut css_sanitizer::lightningcss::properties::Property<'_>,
_ctx: PropertyContext,
) -> NodeAction {
if property.property_id().name() == "color" {
NodeAction::Continue
} else {
NodeAction::Drop
}
}
}
let safe = clean_stylesheet_with_policy(
"@import url('evil.css'); .card { color: red; position: fixed }",
&StyleColorOnly,
);
assert!(!safe.contains("@import"));
assert!(safe.contains("color"));
assert!(!safe.contains("position"));use css_sanitizer::{
sanitize_stylesheet_ast, CssSanitizationPolicy, NodeAction, RuleContext,
};
use css_sanitizer::lightningcss::rules::CssRule;
use css_sanitizer::lightningcss::stylesheet::{ParserOptions, StyleSheet};
struct NoImports;
impl CssSanitizationPolicy for NoImports {
fn visit_rule(
&self,
rule: &mut CssRule<'_>,
_ctx: RuleContext,
) -> NodeAction {
if matches!(rule, CssRule::Import(_)) {
NodeAction::Drop
} else {
NodeAction::Continue
}
}
}
let mut stylesheet =
StyleSheet::parse("@import url('evil.css'); .card { color: blue }", ParserOptions::default())
.expect("stylesheet should parse");
sanitize_stylesheet_ast(&mut stylesheet, &NoImports);
let output = stylesheet
.to_css(Default::default())
.expect("stylesheet should serialize")
.code;
assert!(!output.contains("@import"));
assert!(output.contains(".card"));The built-in walker already handles:
- full stylesheet rule lists
- nested style rules
@media,@supports,@container,@scope,@starting-style@keyframes@font-face@font-palette-values@font-feature-valuesand its sub-rules@pageand page margin rules@counter-style@viewport- selector lists on style-like rules
- normal properties and
!importantdeclarations - descriptor-style nodes exposed by
lightningcss
Empty rules created by filtering are removed during traversal.
CssSanitizationPolicyNodeActionRuleContextSelectorContextPropertyContextDescriptorContextsanitize_declaration_block_ast()sanitize_stylesheet_ast()clean_declaration_list_with_policy()clean_stylesheet_with_policy()pub use lightningcss
- Sevenmark - a wiki markup language that uses this crate to sanitize CSS blocks and inline styles. The policy implementation lives in
crates/sevenmark_html/src/render/sanitize.rsand is a good reference for a fail-closed, production-style policy.
- This crate does not ship a safe default policy.
- Selector scoping,
@import, remote URLs,!important,var(), and unknown rules are all policy decisions. var(--x)still cannot be resolved statically across external cascade boundaries unless your own policy or environment model provides that information.
cargo xtask publish-dry
cargo xtask publishcargo bench --bench sanitizeThe Criterion benchmark suite measures:
- declaration-list parse + sanitize + serialize
- stylesheet parse + sanitize + serialize
- stylesheet AST API parse + sanitize
lightningcssparse/serialize round-trips as a baseline next to sanitizer runs
The built-in fixtures are synthetic but intentionally stress nested rules, descriptor rules,
URLs, var(), and pruning behavior. If you later want real-world corpora, prefer fetching
official distributed CSS from upstream projects during benchmarking rather than vendoring
large third-party CSS blobs into this repository.
Apache-2.0