use std::sync::OnceLock;
use regex::RegexSet;
static XSS_PATTERNS: OnceLock<RegexSet> = OnceLock::new();
static XSS_DESCRIPTIONS: &[&str] = &[
"Script tag injection",
"Script close tag",
"Event handler: onload",
"Event handler: onerror",
"Event handler: onclick/onmouseover/onfocus",
"Event handler: onmouseenter/onkeydown/onkeyup/onkeypress",
"javascript: URI scheme",
"data: URI scheme (text/html)",
"SVG onload injection",
"IFRAME injection",
"IMG src with script",
"CSS expression() injection",
"vbscript: URI scheme",
"HTML entity encoded script",
"Event handler with whitespace evasion (on[ws]load)",
"UTF-7 encoded script tag (+ADw-script)",
"Backslash hex-encoded tag (\\x3c)",
"Script tag with whitespace break (<scri pt>)",
"Unicode fullwidth angle bracket script tag",
];
fn patterns() -> &'static RegexSet {
XSS_PATTERNS.get_or_init(|| {
RegexSet::new([
r"(?i)<\s*script\b[^>]*>",
r"(?i)<\s*/\s*script\s*>",
r"(?i)\bon(load)\s*=",
r"(?i)\bon(error)\s*=",
r"(?i)\bon(click|mouseover|focus)\s*=",
r"(?i)\bon(mouseenter|keydown|keyup|keypress|change|submit|blur|input|begin|end|abort|animationend|animationstart|toggle)\s*=",
r"(?i)javascript\s*:",
r"(?i)data\s*:\s*text/html",
r"(?i)<\s*svg\b[^>]*\bon\w+\s*=",
r"(?i)<\s*iframe\b",
r"(?i)<\s*img\b[^>]*\bon\w+\s*=",
r"(?i)expression\s*\(",
r"(?i)vbscript\s*:",
r"(?i)(<|<|<)\s*script",
r"(?i)\bon[\t\n\r\x0c]+(load|error|click|mouseover|focus|mouseenter|keydown|begin)\s*=",
r"(?i)\+ADw-\s*script",
r"(?i)(\\x3c|\\u003c)\s*script",
r"(?i)<\s*scri[\s\x00]+pt\b",
r"(?i)\x{ff1c}\s*script",
])
.expect("XSS regex patterns must compile")
})
}
pub fn check_xss(input: &str) -> Option<String> {
let set = patterns();
let matches: Vec<_> = set.matches(input).into_iter().collect();
if matches.is_empty() {
None
} else {
let idx = matches[0];
Some(
XSS_DESCRIPTIONS
.get(idx)
.unwrap_or(&"XSS attack")
.to_string(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_script_tag() {
assert!(check_xss("<script>alert('xss')</script>").is_some());
assert!(check_xss("<SCRIPT SRC=http://evil.com/xss.js></SCRIPT>").is_some());
}
#[test]
fn detects_script_with_attributes() {
assert!(check_xss(r#"<script type="text/javascript">alert(1)</script>"#).is_some());
}
#[test]
fn detects_onload() {
assert!(check_xss(r#"<body onload=alert('XSS')>"#).is_some());
}
#[test]
fn detects_onerror() {
assert!(check_xss(r#"<img src=x onerror=alert(1)>"#).is_some());
}
#[test]
fn detects_onclick() {
assert!(check_xss(r#"<div onclick=alert(1)>click</div>"#).is_some());
}
#[test]
fn detects_onmouseover() {
assert!(check_xss(r#"<a onmouseover=alert(1)>hover</a>"#).is_some());
}
#[test]
fn detects_onfocus() {
assert!(check_xss(r#"<input onfocus=alert(1) autofocus>"#).is_some());
}
#[test]
fn detects_javascript_uri() {
assert!(check_xss("javascript:alert(document.cookie)").is_some());
assert!(check_xss("JAVASCRIPT : void(0)").is_some());
}
#[test]
fn detects_data_uri() {
assert!(check_xss("data:text/html,<script>alert(1)</script>").is_some());
}
#[test]
fn detects_svg_onload() {
assert!(check_xss(r#"<svg onload=alert(1)>"#).is_some());
}
#[test]
fn detects_iframe() {
assert!(check_xss(r#"<iframe src="http://evil.com"></iframe>"#).is_some());
}
#[test]
fn detects_img_event() {
assert!(check_xss(r#"<img src=x onerror=alert(1)>"#).is_some());
}
#[test]
fn detects_css_expression() {
assert!(check_xss("background: expression(alert(1))").is_some());
}
#[test]
fn detects_vbscript() {
assert!(check_xss("vbscript:MsgBox(1)").is_some());
}
#[test]
fn detects_entity_encoded() {
assert!(check_xss("<script>alert(1)</script>").is_some());
assert!(check_xss("<script>alert(1)</script>").is_some());
}
#[test]
fn detects_polyglot() {
let payload = r#"jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e"#;
assert!(check_xss(payload).is_some());
}
#[test]
fn allows_normal_html_content() {
assert!(check_xss("This is a <b>bold</b> statement").is_none());
}
#[test]
fn allows_normal_text() {
assert!(check_xss("Hello world, welcome to our site!").is_none());
}
#[test]
fn allows_css_properties() {
assert!(check_xss("color: red; font-size: 14px").is_none());
}
#[test]
fn allows_normal_url() {
assert!(check_xss("https://example.com/page?q=test").is_none());
}
#[test]
fn allows_normal_json() {
assert!(check_xss(r#"{"key": "value", "count": 42}"#).is_none());
}
#[test]
fn allows_angle_brackets_in_math() {
assert!(check_xss("a < b && c > d").is_none());
}
}