xss-filters 0.2.0

Context-aware, just-sufficient output encoding to prevent XSS in HTML, attribute, URI, and CSS contexts
Documentation
//! Conformance tests for the CSS context filters.

mod common;

use common::test_yce;
use xss_filters::css;

/// Inputs for the CSS expression filters.
const CSS_PATTERNS: [&str; 26] = [
    "undefined",
    "null",
    "&",
    "1.1",
    "10%",
    "+10px",
    "-10px",
    "#fff",
    "\u{D7FF}",
    "<lone-high-surrogate>", // JS '\uD800'; no Rust &str analogue, skipped
    "<lone-low-surrogate>",  // JS '\uDFFF'; no Rust &str analogue, skipped
    "\u{1234}567",
    "\u{0}",
    " ",
    "\r\n\t\u{0C}\u{0B}",
    "\\",
    "\\n\\r\\f\\0\\9\\a\\f",
    "-ide_nt",
    "\"string\"",
    "'string'",
    "-  _ : ; ( ) \" ' / , % # ! * @ . { } []",
    "http://username:password@www.evil.com:8080/?k1=v1&k2=v2#hash",
    "url(https://www.evil.com)",
    "u\u{0}\u{0}rl(https://www.evil.com)",
    "\\u\\r\u{0}\u{0}\\l\u{0}\\((evil.com))",
    "expression(body.scrollTop + 50 + px)",
];

const CSS_PATTERNS_TAIL: &str = "(((()))) \\28 \\29";

/// JavaScript `'\uD800'` and `'\uDFFF'` are lone surrogates that a Rust `&str`
/// cannot hold. The JS test only checks that such values are not valid CSS terms
/// and get wrapped. We assert the wrapping shape on the non-surrogate cases and
/// skip the two surrogate-only rows, which have no Rust string analogue.
fn run_css(filter: impl Fn(&str) -> String, expected: &[&str], tail_expected: &str) {
    // indices 9 and 10 are lone surrogates in JS; drop them on both sides.
    let mut patterns: Vec<&str> = Vec::new();
    let mut exp: Vec<&str> = Vec::new();
    for (i, (p, e)) in CSS_PATTERNS.iter().zip(expected.iter()).enumerate() {
        if i == 9 || i == 10 {
            continue;
        }
        patterns.push(p);
        exp.push(e);
    }
    patterns.push(CSS_PATTERNS_TAIL);
    exp.push(tail_expected);
    test_yce(filter, &patterns, &exp);
}

#[test]
fn css_value_expression() {
    let expected = [
        "undefined",
        "null",
        ";-x:'&';-v:",
        "1.1",
        "10%",
        "+10px",
        "-10px",
        "#fff",
        ";-x:'\u{D7FF}';-v:",
        "", // surrogate row, skipped
        "", // surrogate row, skipped
        ";-x:'\u{1234}567';-v:",
        ";-x:'\u{FFFD}';-v:",
        ";-x:' ';-v:",
        ";-x:'\\d \\a \\9 \\c \\b ';-v:",
        ";-x:'\\5c ';-v:",
        ";-x:'\\5c n\\5c r\\5c f\\5c 0\\5c 9\\5c a\\5c f';-v:",
        "-ide_nt",
        ";-x:'\"string\"';-v:",
        ";-x:'\\27 string\\27 ';-v:",
        ";-x:'-  _ : ; ( ) \" \\27  / , % # ! * @ . \\7b  \\7d  \\5b \\5d ';-v:",
        ";-x:'http://username:password@www.evil.com:8080/?k1=v1&k2=v2#hash';-v:",
        ";-x:'-x-url(https://www.evil.com)';-v:",
        ";-x:'u\u{FFFD}\u{FFFD}rl(https://www.evil.com)';-v:",
        ";-x:'\\5c u\\5c r\u{FFFD}\u{FFFD}\\5c l\u{FFFD}\\5c ((evil.com))';-v:",
        ";-x:'expression(body.scrollTop + 50 + px)';-v:",
    ];
    run_css(
        css::css_value,
        &expected,
        ";-x:'(((()))) \\5c 28 \\5c 29';-v:",
    );
}

#[test]
fn css_double_quoted_string() {
    let expected = [
        "undefined",
        "null",
        "&",
        "1.1",
        "10%",
        "+10px",
        "-10px",
        "#fff",
        "\u{D7FF}",
        "",
        "",
        "\u{1234}567",
        "\u{FFFD}",
        " ",
        "\\d \\a \\9 \\c \\b ",
        "\\5c ",
        "\\5c n\\5c r\\5c f\\5c 0\\5c 9\\5c a\\5c f",
        "-ide_nt",
        "\\22 string\\22 ",
        "'string'",
        "-  _ : ; ( ) \\22  ' / , % # ! * @ . \\7b  \\7d  \\5b \\5d ",
        "http://username:password@www.evil.com:8080/?k1=v1&k2=v2#hash",
        "-x-url(https://www.evil.com)",
        "u\u{FFFD}\u{FFFD}rl(https://www.evil.com)",
        "\\5c u\\5c r\u{FFFD}\u{FFFD}\\5c l\u{FFFD}\\5c ((evil.com))",
        "expression(body.scrollTop + 50 + px)",
    ];
    run_css(
        css::css_double_quoted,
        &expected,
        "(((()))) \\5c 28 \\5c 29",
    );
}

#[test]
fn css_single_quoted_string() {
    let expected = [
        "undefined",
        "null",
        "&",
        "1.1",
        "10%",
        "+10px",
        "-10px",
        "#fff",
        "\u{D7FF}",
        "",
        "",
        "\u{1234}567",
        "\u{FFFD}",
        " ",
        "\\d \\a \\9 \\c \\b ",
        "\\5c ",
        "\\5c n\\5c r\\5c f\\5c 0\\5c 9\\5c a\\5c f",
        "-ide_nt",
        "\"string\"",
        "\\27 string\\27 ",
        "-  _ : ; ( ) \" \\27  / , % # ! * @ . \\7b  \\7d  \\5b \\5d ",
        "http://username:password@www.evil.com:8080/?k1=v1&k2=v2#hash",
        "-x-url(https://www.evil.com)",
        "u\u{FFFD}\u{FFFD}rl(https://www.evil.com)",
        "\\5c u\\5c r\u{FFFD}\u{FFFD}\\5c l\u{FFFD}\\5c ((evil.com))",
        "expression(body.scrollTop + 50 + px)",
    ];
    run_css(
        css::css_single_quoted,
        &expected,
        "(((()))) \\5c 28 \\5c 29",
    );
}

// --- CSS url filters ---

/// Inputs for the CSS url filters.
const URL_PATTERNS: [&str; 25] = [
    "undefined",
    "null",
    "&",
    "1.1",
    "10%",
    "+10px",
    "-10px",
    "#fff",
    "\\a",
    "\u{D7FF}",
    "\u{1234}567",
    "\u{0}",
    " ",
    "\r\n\t\u{0C}\u{0B}",
    "\\",
    "\\n\\r\\f\\0\\9\\a\\f",
    "-ide_nt",
    "\"string\"",
    "'string'",
    "-  _ : ; ( ) \" ' / , % # ! * @ . { } [ ]",
    "http://username:password@www.evil.com:8080/?k1=v1&k2=v2#hash",
    "\u{0}\u{8}\u{B}\u{7F}\u{E}-\u{1F}",
    "&rpar;&#x00029;&#41;&lpar;&#x00028;&#40;&apos;&#x00027;&#39;&quot;&QUOT;&#x00022;&#34",
    "javascript:alert(1)",
    "(((()))) \\28 \\29",
];

#[test]
fn css_unquoted_url_filter() {
    let expected = [
        "undefined",
        "null",
        "&",
        "1.1",
        "10%25",
        "+10px",
        "-10px",
        "#fff",
        "%5Ca",
        "%ED%9F%BF",
        "%E1%88%B4567",
        "%EF%BF%BD",
        "%20",
        "%0D%0A%09%0C%0B",
        "%5C",
        "%5Cn%5Cr%5Cf%5C0%5C9%5Ca%5Cf",
        "-ide_nt",
        "%22string%22",
        "\\27 string\\27 ",
        "-%20%20_%20:%20;%20%28%20%29%20%22%20\\27 %20/%20,%20%25%20#%20!%20*%20@%20.%20%7B%20%7D%20%5B%20%5D",
        "http://username:password@www.evil.com:8080/?k1=v1&k2=v2#hash",
        "%EF%BF%BD%08%0B%7F%0E-%1F",
        "%29%29%29%28%28%28\\27 \\27 \\27 %22%22%22%22",
        "##javascript:alert%281%29",
        "%28%28%28%28%29%29%29%29%20%5C28%20%5C29",
    ];
    test_yce(css::css_unquoted_url, &URL_PATTERNS, &expected);
}

#[test]
fn css_double_quoted_url_filter() {
    let expected = [
        "undefined",
        "null",
        "&",
        "1.1",
        "10%25",
        "+10px",
        "-10px",
        "#fff",
        "%5Ca",
        "%ED%9F%BF",
        "%E1%88%B4567",
        "%EF%BF%BD",
        "%20",
        "%0D%0A%09%0C%0B",
        "%5C",
        "%5Cn%5Cr%5Cf%5C0%5C9%5Ca%5Cf",
        "-ide_nt",
        "%22string%22",
        "'string'",
        "-%20%20_%20:%20;%20(%20)%20%22%20'%20/%20,%20%25%20#%20!%20*%20@%20.%20%7B%20%7D%20%5B%20%5D",
        "http://username:password@www.evil.com:8080/?k1=v1&k2=v2#hash",
        "%EF%BF%BD%08%0B%7F%0E-%1F",
        ")))((('''%22%22%22%22",
        "##javascript:alert(1)",
        "(((())))%20%5C28%20%5C29",
    ];
    test_yce(css::css_double_quoted_url, &URL_PATTERNS, &expected);
}

#[test]
fn css_single_quoted_url_filter() {
    let expected = [
        "undefined",
        "null",
        "&",
        "1.1",
        "10%25",
        "+10px",
        "-10px",
        "#fff",
        "%5Ca",
        "%ED%9F%BF",
        "%E1%88%B4567",
        "%EF%BF%BD",
        "%20",
        "%0D%0A%09%0C%0B",
        "%5C",
        "%5Cn%5Cr%5Cf%5C0%5C9%5Ca%5Cf",
        "-ide_nt",
        "%22string%22",
        "\\27 string\\27 ",
        "-%20%20_%20:%20;%20(%20)%20%22%20\\27 %20/%20,%20%25%20#%20!%20*%20@%20.%20%7B%20%7D%20%5B%20%5D",
        "http://username:password@www.evil.com:8080/?k1=v1&k2=v2#hash",
        "%EF%BF%BD%08%0B%7F%0E-%1F",
        ")))(((\\27 \\27 \\27 %22%22%22%22",
        "##javascript:alert(1)",
        "(((())))%20%5C28%20%5C29",
    ];
    test_yce(css::css_single_quoted_url, &URL_PATTERNS, &expected);
}

/// The CSS-string filters escape `<` so a value cannot close a `<style>` block.
#[test]
fn css_string_filters_escape_angle_bracket() {
    for out in [
        css::css_value("a</style>b"),
        css::css_double_quoted("a</style>b"),
        css::css_single_quoted("a</style>b"),
    ] {
        assert!(!out.contains('<'), "leaked < in {out:?}");
    }
}