Skip to content

live-codes/css-sanitizer

Repository files navigation

@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 parensexpression(if(1>0){alert(1)}) defeats url\([^)]*\). The scanner skips balanced parens.
  • CSS escapesur\6c(...), @\69 mport, beh\61 vior bypass literal matching. The scanner decodes escapes before comparison.
  • String contextcontent: "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-sanitizer

Usage

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 pack

License

MIT

About

A zero-dependency, environment-agnostic CSS sanitizer.

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

 
 
 

Contributors