xss-filters 0.2.0

Context-aware, just-sufficient output encoding to prevent XSS in HTML, attribute, URI, and CSS contexts
Documentation
//! Context-aware output encoders that prevent cross-site scripting.
//!
//! Each filter encodes only the characters needed to keep an untrusted string
//! from breaking out of a specific HTML output context. The supported contexts
//! are HTML data, HTML comments, single/double/unquoted attribute values, and
//! URIs placed inside any of those. URI filters normalize IPv6 hosts and block
//! dangerous protocols such as `javascript:` and `data:`.
//!
//! The design encodes the minimal set of characters required to thwart a
//! context break, rather than entity-escaping everything. This is faster and
//! avoids double-encoding.
//!
//! # Choosing a filter
//!
//! Match the filter to the exact place the value lands in the document.
//!
//! ```
//! use xss_filters::{in_html_data, in_double_quoted_attr, uri_in_double_quoted_attr};
//!
//! // <div>{{ value }}</div>
//! assert_eq!(in_html_data("<script>"), "&lt;script>");
//!
//! // <input value="{{ value }}">
//! assert_eq!(in_double_quoted_attr("a\"b"), "a&quot;b");
//!
//! // <a href="{{ value }}">
//! assert_eq!(
//!     uri_in_double_quoted_attr("javascript:alert(1)"),
//!     "x-javascript:alert(1)"
//! );
//! ```
//!
//! # Input contract
//!
//! All public functions take `&str` and return `String`. JavaScript-style
//! coercion of non-string values is the caller's job. Apply these filters only
//! to UTF-8 documents. Do not apply them inside scriptable contexts such as
//! `<script>`, `<style>`, `<object>`, `<embed>`, `<svg>`, or inside `style` and
//! `on*` attributes.
//!
//! # URI encoding and surrogates
//!
//! The URI filters build on encoders equivalent to JavaScript `encodeURI` and
//! `encodeURIComponent`. Those throw on a lone UTF-16 surrogate. A Rust `&str`
//! is always well-formed UTF-8, so the `&str` entry points cannot reach that
//! case. [`uri::encode_uri_units`] reproduces the throw for raw UTF-16 input.

#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]
#![warn(missing_docs)]

extern crate alloc;
use alloc::string::String;

#[cfg(test)]
extern crate std;

mod core;
pub mod uri;

#[cfg(any(feature = "css", test))]
pub mod css;

// ---------------------------------------------------------------------------
// Generic context filters
// ---------------------------------------------------------------------------

/// Encode `<` as `&lt;` for HTML data context.
///
/// Use inside element content such as `<div>{{ s }}</div>`. Only `<` is
/// encoded. `>`, `&`, `"`, and `'` pass through.
///
/// ```
/// assert_eq!(xss_filters::in_html_data("foo&<>'\""), "foo&&lt;>'\"");
/// ```
pub fn in_html_data(s: &str) -> String {
    core::yd(s)
}

/// Neutralize HTML comment closers for HTML comment context.
///
/// Use inside `<!-- {{ s }} -->`. Sequences that could close the comment
/// (`-->`, `--!>`, a leading `-*>`), collaborate to close it (a trailing `--!`,
/// `--`, or `-`), or trigger IE conditional comments (`]>` or a trailing `]`)
/// gain a space. NUL becomes the replacement character.
///
/// ```
/// assert_eq!(xss_filters::in_html_comment("foo-->bar"), "foo-- >bar");
/// ```
pub fn in_html_comment(s: &str) -> String {
    core::yc(s)
}

/// Encode `'` as `&#39;` for single-quoted attribute context.
///
/// Use inside `<input value='{{ s }}'>`. Not for `on*` attributes. For URIs use
/// the matching `uri*_in_single_quoted_attr` filter.
///
/// ```
/// assert_eq!(xss_filters::in_single_quoted_attr("it's"), "it&#39;s");
/// ```
pub fn in_single_quoted_attr(s: &str) -> String {
    core::yavs(s)
}

/// Encode `"` as `&quot;` for double-quoted attribute context.
///
/// Use inside `<input value="{{ s }}">`. Not for `on*` attributes. For URIs use
/// the matching `uri*_in_double_quoted_attr` filter.
///
/// ```
/// assert_eq!(xss_filters::in_double_quoted_attr("a\"b"), "a&quot;b");
/// ```
pub fn in_double_quoted_attr(s: &str) -> String {
    core::yavd(s)
}

/// Encode attribute-breaking characters for unquoted attribute context.
///
/// Use inside `<input value={{ s }}>`. Whitespace, quotes, backtick, `=`, `<`,
/// `>`, and NUL are encoded. An empty input yields a single replacement
/// character so the value cannot silently swallow the next attribute.
///
/// ```
/// assert_eq!(xss_filters::in_unquoted_attr(""), "\u{FFFD}");
/// assert_eq!(xss_filters::in_unquoted_attr("a b"), "a&#32;b");
/// ```
pub fn in_unquoted_attr(s: &str) -> String {
    core::yavu(s)
}

// ---------------------------------------------------------------------------
// URI matrix: helpers
// ---------------------------------------------------------------------------

/// Compose a full or path URI attribute filter: encode, attr-escape, then guard.
fn uri_in_attr(s: &str, attr: fn(&str) -> String, encode: fn(&str) -> String) -> String {
    core::yubl(&attr(&encode(s)))
}

// ---------------------------------------------------------------------------
// Full URI (encodeURI + IPv6 restore, protocol guard on attrs)
// ---------------------------------------------------------------------------

/// Full URI inside a single-quoted attribute.
///
/// `encodeURI` plus IPv6 restore, then single-quote escape, then protocol guard.
pub fn uri_in_single_quoted_attr(s: &str) -> String {
    uri_in_attr(s, core::yavs, core::yufull)
}

/// Full URI inside a double-quoted attribute.
pub fn uri_in_double_quoted_attr(s: &str) -> String {
    uri_in_attr(s, core::yavd, core::yufull)
}

/// Full URI inside an unquoted attribute.
pub fn uri_in_unquoted_attr(s: &str) -> String {
    uri_in_attr(s, core::yavu, core::yufull)
}

/// Full URI in HTML data context. Only the URI encoder runs, since `<` is
/// already `%3C`.
pub fn uri_in_html_data(s: &str) -> String {
    core::yufull(s)
}

/// Full URI in HTML comment context.
pub fn uri_in_html_comment(s: &str) -> String {
    core::yc(&core::yufull(s))
}

// ---------------------------------------------------------------------------
// URI path / query (encodeURI, no IPv6 restore, protocol guard on attrs)
// ---------------------------------------------------------------------------

/// URI path or query inside a single-quoted attribute.
pub fn uri_path_in_single_quoted_attr(s: &str) -> String {
    uri_in_attr(s, core::yavs, core::yu)
}

/// URI path or query inside a double-quoted attribute.
pub fn uri_path_in_double_quoted_attr(s: &str) -> String {
    uri_in_attr(s, core::yavd, core::yu)
}

/// URI path or query inside an unquoted attribute.
pub fn uri_path_in_unquoted_attr(s: &str) -> String {
    uri_in_attr(s, core::yavu, core::yu)
}

/// URI path or query in HTML data context.
pub fn uri_path_in_html_data(s: &str) -> String {
    core::yu(s)
}

/// URI path or query in HTML comment context.
pub fn uri_path_in_html_comment(s: &str) -> String {
    core::yc(&core::yu(s))
}

// URI query filters are exact aliases of the path filters.

/// URI query inside a single-quoted attribute. Alias of the path filter.
pub fn uri_query_in_single_quoted_attr(s: &str) -> String {
    uri_path_in_single_quoted_attr(s)
}

/// URI query inside a double-quoted attribute. Alias of the path filter.
pub fn uri_query_in_double_quoted_attr(s: &str) -> String {
    uri_path_in_double_quoted_attr(s)
}

/// URI query inside an unquoted attribute. Alias of the path filter.
pub fn uri_query_in_unquoted_attr(s: &str) -> String {
    uri_path_in_unquoted_attr(s)
}

/// URI query in HTML data context. Alias of the path filter.
pub fn uri_query_in_html_data(s: &str) -> String {
    uri_path_in_html_data(s)
}

/// URI query in HTML comment context. Alias of the path filter.
pub fn uri_query_in_html_comment(s: &str) -> String {
    uri_path_in_html_comment(s)
}

// ---------------------------------------------------------------------------
// URI component (encodeURIComponent, no protocol guard)
// ---------------------------------------------------------------------------

/// URI component inside a single-quoted attribute.
///
/// No protocol guard runs because `encodeURIComponent` already percent-encodes
/// the `:` that a `javascript:` scheme needs.
pub fn uri_component_in_single_quoted_attr(s: &str) -> String {
    core::yavs(&core::yuc(s))
}

/// URI component inside a double-quoted attribute.
pub fn uri_component_in_double_quoted_attr(s: &str) -> String {
    core::yavd(&core::yuc(s))
}

/// URI component inside an unquoted attribute.
pub fn uri_component_in_unquoted_attr(s: &str) -> String {
    core::yavu(&core::yuc(s))
}

/// URI component in HTML data context.
pub fn uri_component_in_html_data(s: &str) -> String {
    core::yuc(s)
}

/// URI component in HTML comment context.
pub fn uri_component_in_html_comment(s: &str) -> String {
    core::yc(&core::yuc(s))
}

// ---------------------------------------------------------------------------
// URI fragment (encodeURIComponent; protocol guard on attrs)
// ---------------------------------------------------------------------------

/// URI fragment inside a single-quoted attribute.
///
/// Like the component filter but with the protocol guard, since a fragment
/// pasted into `href` could carry `javascript:`.
pub fn uri_fragment_in_single_quoted_attr(s: &str) -> String {
    core::yubl(&core::yavs(&core::yuc(s)))
}

/// URI fragment inside a double-quoted attribute.
pub fn uri_fragment_in_double_quoted_attr(s: &str) -> String {
    core::yubl(&core::yavd(&core::yuc(s)))
}

/// URI fragment inside an unquoted attribute.
pub fn uri_fragment_in_unquoted_attr(s: &str) -> String {
    core::yubl(&core::yavu(&core::yuc(s)))
}

/// URI fragment in HTML data context. Alias of the component filter.
pub fn uri_fragment_in_html_data(s: &str) -> String {
    uri_component_in_html_data(s)
}

/// URI fragment in HTML comment context. Alias of the component filter.
pub fn uri_fragment_in_html_comment(s: &str) -> String {
    uri_component_in_html_comment(s)
}