A zero-dependency, environment-agnostic CSS sanitizer for stripping dangerous patterns from untrusted CSS. Designed primarily for browser use, but works in any JavaScript environment.
Because untrusted CSS can be dangerous!
Naive regex stripping is bypassable. This library uses a single-pass character scanner that tracks string/comment/paren/brace context and decodes CSS escapes, closing three common bypass holes:
- Nested parens —
expression(if(1>0){alert(1)})defeatsurl\([^)]*\). The scanner skips balanced parens. - CSS escapes —
ur\6c(...),@\69 mport,beh\61 viorbypass literal matching. The scanner decodes escapes before comparison. - String context —
content: "url(javascript:alert(1))"is just text inside a string, not a function call. The scanner preserves it by tracking string context.
npm install @live-codes/css-sanitizerimport { sanitizeCSS } from "@live-codes/css-sanitizer";
const untrusted = `
@import url(evil.css);
.card {
background: url("javascript:alert(1)") no-repeat;
color: red;
behavior: url(evil.htc);
}
`;
const safe = sanitizeCSS(untrusted);
// .card { background: no-repeat; color: red;}sanitizeCSS(css, {
// Additional at-rule names to strip (without the @ prefix)
blocklistAtRules: ["font-face"],
// Additional property names to strip
blocklistProperties: ["position"],
});Options extend the default blocklist rather than replacing it.
| Category | Patterns | Behavior |
|---|---|---|
| At-rules | @import, @charset, @namespace |
Removed entirely (prelude + block) |
| Properties | behavior, -moz-binding |
Entire declaration removed |
| Functions | url() (non-data:), expression() |
Removed from values; declaration removed if left empty |
| Functions | url() with data: scheme |
Allowed — inline resources make no external request |
| Comments | /* ... */ |
All comments stripped |
All other CSS is preserved. At-rules like @media, @keyframes, @supports, and @font-face are kept — their contents are recursively sanitized.
data: URLs are allowed in url() (e.g. background: url(data:image/png;base64,...)) because they are inline resources that don't make external network requests and can't be used for data exfiltration. However, @import is stripped entirely regardless of URL scheme — @import url(data:text/css,...) would import unsanitized CSS, bypassing the sanitizer.
All comparisons are case-insensitive and escape-aware, so URL(, ur\6c(, BEHAVIOR, beh\61 vior, and @\69 mport are all caught.
interface SanitizeOptions {
blocklistAtRules?: string[];
blocklistProperties?: string[];
}
function sanitizeCSS(css: string, options?: SanitizeOptions): string;- Install dependencies:
vp install- Run the unit tests:
vp test- Format, lint, and type check:
vp check- Build the library:
vp packMIT