Sanitizer API

repository·main·Indexed 43 days ago

https://github.com/wicg/sanitizer-api

A proposed web platform API for browser-native HTML sanitization to prevent XSS and injection attacks. It provides safe methods like Element.setHTML and Document.parseHTML, as well as unsafe variants, allowing developers to configure allow-lists and remove-lists via the Sanitizer constructor to securely handle user-generated content.

Tokens
4.2K
Snippets
10
Records
14
Agent score
55%

What's inside Sanitizer API

  1. Overview of the Sanitizer API

    main

    The Sanitizer API is a browser-native API designed to provide a safe, easy-to-use, and browser-maintained way to sanitize HTML strings. Its primary purpose is to prevent accidental JavaScript execution when rendering user-generated content or client-side templates. By being part of the web platform, the API is updated alongside the browser's parser, ensuring protection against new attack vectors without requiring manual library updates.

    Key Characteristics:

    • User Input Sanitization: Converts potentially unsafe HTML strings into safe versions that do not trigger inadvertent script execution.
    • Browser-Maintained: As an "ever-green" part of the web platform, it benefits from automatic updates to the browser's security and parsing logic.
    • Safe Defaults: Designed with a small API surface and sensible defaults to make common sanitization tasks easy and secure by default.

    Note on Status: The API is currently being incubated and is not yet finalized. It is moving toward being part of the WHATWG HTML specification.

  2. Compare Sanitizer API with DOMPurify

    main

    The Sanitizer API proposal differs from DOMPurify in several architectural ways:

    • Configuration: DOMPurify uses a modifiable default config and accepts an options dictionary on every call. The Sanitizer API allows creating sanitizer objects from an options dictionary and provides read-only sanitizers for easy-to-use defaults.
    • Return Types: DOMPurify uses a single method with different return types based on the options set. The Sanitizer API uses different methods for different return types.
    • Extensibility: DOMPurify supports a system of hooks (e.g., beforeSanitizeElements, uponSanitizeElement) which currently has no equivalent in the Sanitizer API proposal.
    • Feature Set: DOMPurify currently offers a significantly larger number of configuration options than the Sanitizer API proposal.
  3. Use the basic Sanitizer API methods

    main

    The Sanitizer API provides two sets of methods for handling HTML: 'safe' methods and 'unsafe' methods.

    • Safe methods (setHTML, parseHTML): These remove all script-y content defined by the platform while letting the rest pass. They are designed to prevent XSS.
    • Unsafe methods (setHTMLUnsafe, parseHTMLUnsafe): These do not perform sanitization and allow all content, including scripts, to pass through.

    Context Awareness:

    • setHTML and parseHTML observe the context of the node they are called on. For example, parsing a <tr> inside a <table> element will correctly wrap it in a <tbody>, whereas parsing it on a <div> will result in the row being stripped or flattened depending on the context.
    • Security Note: If you call setHTML on a <script> element, it is treated as a no-op to prevent the accidental creation of new script content.
    const example = `<b onclick="alert(1)">hello world</b>`;
    const element = document.createElement("div");
    
    // Safe: removes the onclick attribute
    element.setHTML(example);  // <div><b style="...">hello world</b></div>
    
    // Unsafe: keeps the onclick attribute
    element.setHTMLUnsafe(example); // <div><b onclick="alert(1)">hello world</b></div>
    
    // Return a Document instance:
    Document.parseHTML(example);  // <html>...<body><b...>...</body></html>
  4. Check Sanitizer API implementation status

    main

    Because the Sanitizer API is still in incubation, its availability varies by browser. Before integrating it into your project, check the following resources for the current implementation status:

    • Browser Positions: Check the status for Mozilla, WebKit, and Chrome.
    • Can I Use: Check caniuse.com for browser compatibility data.
    • Web Platform Tests: View the results of Web Platform Tests to see how implementations are performing against the spec.
  5. Configure the Sanitizer API via options

    main

    All Sanitizer API methods (setHTML, setHTMLUnsafe, parseHTML, and parseHTMLUnsafe) accept an optional configuration object.

    Currently, the primary configuration key is filter (the exact name is subject to change). This dictionary allows developers to define which elements, attributes, or styles are permitted during the sanitization process.

    Note that the behavior of the filter configuration differs depending on the method used:

    • In safe methods, the browser enforces a security baseline regardless of the configuration.
    • In unsafe methods, the configuration is applied exactly as written, and no default security baseline is enforced.
    element.setHTML(htmlString, {
      filter: { /* filter configuration object */ }
    });
  6. Report issues and bugs for the Sanitizer API

    main

    The Sanitizer API is being transitioned to the WHATWG HTML specification. Follow these guidelines for reporting:

    • Specification Issues: Report issues regarding the API definition or specification itself in the WHATWG HTML issue tracker.
    • Implementation Bugs: Report bugs found in specific browser implementations (e.g., Chrome, Firefox, Safari) directly to the corresponding browser vendor's security reporting channels.
  7. Replace elements with their children

    main

    If you want to remove specific HTML tags but preserve the text content inside them, use the replaceWithChildrenElements option. This is useful for stripping formatting while keeping user input readable.

    Note that replaceWithChildrenElements only applies to the immediate children (one level deep).

    const config = new Sanitizer({
      elements: ["b", "i"],
      replaceWithChildrenElements: ["span"]
    });
    
    element.setHTML(
      "Fancy <span style='color:blue'>text with <b style='...'>pizzazz</b></span>.",
      { sanitizer: config 
    });
    // Result: <div...>Fancy text with <b>pizzazz</b>.</div>
  8. Modify an existing Sanitizer object

    main

    The Sanitizer object is mutable. You can create a base configuration and then use specialized methods to add or remove rules. This is more performant than recreating a new Sanitizer object in a loop.

    Available Methods:

    • allowElement(x): Adds an element to the allow-list. If x is a dictionary, you can also specify attributes or removeAttributes for that element.
    • removeElement(x): Adds an element to the remove-list.
    • replaceElementWithChildren(x): Adds an element to the replaceWithChildrenElements list.
    • allowAttribute(x): Adds an attribute to the allow-list.
    • removeAttribute(x): Adds an attribute to the remove-list.
    • setComments(bool): Enables or disables HTML comment nodes.
    • setDataAttributes(bool): Enables or disables data attributes.

    Note: Adding an item to an allow-list automatically removes it from any existing remove-lists for that type.

    const s = new Sanitizer({ elements: ["div", "p", "b"] });
    s.allowElement("span");
    s.removeElement("b");
    
    // To set element-specific attributes (overwrites previous settings for that element):
    s.allowElement({name: "div", attributes: ["id", "class"]});
  9. Configure the Sanitizer with an allow-list or remove-list

    main

    You can control what the sanitizer allows or removes using the Sanitizer constructor. There are two primary strategies:

    1. Allow-list approach: Specify exactly which elements and attributes are permitted. Everything else is removed.
    2. Remove-list approach: Specify which removeElements and removeAttributes should be stripped. Everything else is kept (based on platform defaults).

    Important Rules:

    • You cannot mix an allow-list and a remove-list for the same category (e.g., you cannot have both elements and removeElements in the same config; doing so throws a TypeError).
    • You can combine allow-lists with replaceWithChildrenElements lists.
    // Allow-list: Only these 5 elements and the 'class' attribute are allowed
    const config_allow_some_formatting = new Sanitizer({
      elements: [ "div", "p", "em", "b", "img" ],
      attributes: [ "class" ]
    });
    
    // Remove-list: Allows defaults, but strips <style> and 'class'/'style' attributes
    const config_disallow_style_definitions = new Sanitizer({
      removeElements: [ "style" ],
      removeAttributes: [ "class", "style" ]
    });
  10. Use the Sanitizer API to set HTML safely

    main

    The Sanitizer API provides methods to parse and filter HTML strings to prevent XSS (Cross-Site Scripting) attacks. The API is divided into 'safe' methods that enforce a security baseline and 'unsafe' methods that allow arbitrary content if configured to do so.

    Safe Methods (Enforce XSS Baseline)

    These methods apply a filter and ensure a secure baseline is maintained (e.g., dropping javascript: URLs even if not explicitly configured).

    • Element.setHTML(string, {options}): Parses the string using the current element as the context (similar to innerHTML) and replaces the element's children with the sanitized results.
    • Document.parseHTML(string, {options}): A static method that creates a new Document instance, parses the string, applies a filter, and returns the document.

    Unsafe Methods (No Default Baseline)

    These methods do not enforce a security baseline. If no filter is provided in the options, no filtering occurs.

    • Element.setHTMLUnsafe(string, {options}): Similar to setHTML, but allows elements like <script> if the filter is configured to permit them.
    • Document.parseHTMLUnsafe(string, {options}): A static method that parses the string into a new document without enforcing a baseline or applying a default filter.
    // Example of safe usage on an element
    const container = document.querySelector('#container');
    container.setHTML('<img src=x onerror=alert(1)>', { /* options */ });
    
    // Example of parsing into a new document safely
    const doc = Document.parseHTML('<div>Hello</div>', { /* options */ });
    
    // Example of unsafe usage (allows scripts if configured)
    const unsafeContainer = document.querySelector('#unsafe');
    unsafeContainer.setHTMLUnsafe('<script>alert(1)</script>', { /* options */ });
  11. Query a Sanitizer configuration

    main

    You can inspect a Sanitizer object to see its canonical configuration using the get() and getUnsafe() methods.

    • get(): Returns a dictionary representing the configuration as it would be applied by a safe method. It resolves names into explicit name/namespace pairs and converts remove-lists into their equivalent allow-lists based on platform defaults.
    • getUnsafe(): Returns a dictionary representing the configuration as it would be applied by an unsafe method (e.g., it will include elements like <script> if they were in the original config).
    const a_simple_config = new Sanitizer({ elements: [ "div", "p", "span", "script" ] });
    
    // Returns the safe version (script is removed because it's unsafe)
    a_simple_config.get();
    
    // Returns the unsafe version (script is included)
    a_simple_config.getUnsafe();
  12. Configure attributes for specific elements

    main

    You can define attribute rules that apply only to specific elements. This allows for fine-grained control, such as allowing src on <img> tags but not on <input> tags.

    In the configuration object, an element can be defined as a string (for the default HTML namespace) or as a dictionary containing name, namespace, attributes (an allow-list), or removeAttributes (a deny-list).

    // Allow 'class' everywhere, but 'src' only on <img>
    const config = new Sanitizer({
      elements: [
        "div", "p", "em", "b",
        { name: "img", attributes: [ "src" ] }
      ],
      attributes: ["class"],
    });
    
    // Remove 'src' from <input> but keep it elsewhere
    const remove_src_from_input = new Sanitizer({
      elements: [{ name: "input", removeAttributes: ["src"] }],
    });