@live-codes/css-sanitizer
v0.2.0
Published
A zero-dependency, environment-agnostic CSS sanitizer.
Readme
@live-codes/css-sanitizer
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.
Why?
Because untrusted CSS can be dangerous!
Why not regex?
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.
Installation
npm install @live-codes/css-sanitizerUsage
import { 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;}Options
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.
What it strips
| 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.
API
interface SanitizeOptions {
blocklistAtRules?: string[];
blocklistProperties?: string[];
}
function sanitizeCSS(css: string, options?: SanitizeOptions): string;Development
- Install dependencies:
vp install- Run the unit tests:
vp test- Format, lint, and type check:
vp check- Build the library:
vp packLicense
MIT
