security_rust/injection/
xss.rs1use crate::{regex_detect, AttackCategory, DetectionResult, Detector, Severity};
4use regex::Regex;
5use std::sync::LazyLock;
6
7static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
8 vec![
9 Regex::new(r"(?i)<script[\s/>]").unwrap(),
10 Regex::new(r"(?i)on(?:error|load|click|mouse(?:over|out|down|up|move)|key(?:down|up|press)|focus|blur|change|submit|reset|scroll|resize|abort|select|start|drag|drop|play|pause|ended|volumechange|animationstart|animationend|transitionend|touchstart|touchend|pointerdown|pointerup|wheel|auxclick|canplay|canplaythrough|close|cuechange|dblclick|durationchange|emptied|fullscreenchange|gotpointercapture|input|invalid|loadeddata|loadedmetadata|loadstart|lostpointercapture|offline|online|pagehide|pageshow|popstate|progress|ratechange|securitypolicyviolation|seeked|seeking|show|stalled|suspend|timeupdate|toggle|waiting)\s*=").unwrap(),
11 Regex::new(r"(?i)javascript\s*:").unwrap(),
12 Regex::new(r"(?i)<svg[\s/>]").unwrap(),
13 Regex::new(r"(?i)expression\s*\(").unwrap(),
14 Regex::new(r"(?i)<iframe[\s/>]").unwrap(),
15 Regex::new(r"(?i)<embed[\s/>]").unwrap(),
16 Regex::new(r"(?i)<object[\s/>]").unwrap(),
17 Regex::new(r"(?i)vbscript\s*:").unwrap(),
18 Regex::new(r"(?i)data\s*:\s*text/html").unwrap(),
19 Regex::new(r"(?i)<link[\s/>]").unwrap(),
20 Regex::new(r"(?i)<meta[\s/>]").unwrap(),
21 Regex::new(r"(?i)eval\s*\(").unwrap(),
22 Regex::new(r"(?i)fromCharCode\s*\(").unwrap(),
23 Regex::new(r"(?i)document\.cookie").unwrap(),
24 Regex::new(r"(?i)document\.write\s*\(").unwrap(),
25 Regex::new(r"(?i)window\.location").unwrap(),
26 ]
27});
28
29pub struct XssDetector;
30
31impl Detector for XssDetector {
32 fn name(&self) -> &'static str {
33 "xss"
34 }
35
36 fn detect(&self, input: &str) -> Option<DetectionResult> {
37 regex_detect(&PATTERNS, self.name(), AttackCategory::Injection, Severity::Critical, "XSS cross-site scripting detected", input)
38 }
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44
45 fn det() -> XssDetector {
46 XssDetector
47 }
48
49 fn assert_hit(input: &str) {
50 crate::test_helpers::assert_detected(
51 &det(),
52 input,
53 AttackCategory::Injection,
54 Severity::Critical,
55 );
56 }
57
58 #[test]
59 fn name_is_xss() {
60 assert_eq!(det().name(), "xss");
61 }
62
63 #[test]
64 fn detects_common_payloads() {
65 for input in [
66 "<script>alert(1)</script>",
67 "<img src=x onerror=alert(1)>",
68 "javascript:alert(document.cookie)",
69 "<svg onload=alert(1)>",
70 "eval('alert(1)')",
71 "<iframe src=\"//evil.com\"></iframe>",
72 "data:text/html,<script>alert(1)</script>",
73 ] {
74 assert_hit(input);
75 }
76 }
77
78 #[test]
79 fn benign_inputs_not_detected() {
80 for input in [
81 "Hello, this is a normal text input. Nothing suspicious here.",
82 "The weather today is sunny with a high of 25 degrees.",
83 "Please call the office at 555-1234 for assistance.",
84 "Welcome to our website, please enjoy your stay.",
85 ] {
86 assert!(det().detect(input).is_none(), "false positive: {input}");
87 }
88 }
89
90 #[test]
91 fn edge_cases() {
92 assert!(det().detect("").is_none());
93 assert!(det().detect(" \t\n ").is_none());
94 assert!(det().detect("こんにちは世界 你好").is_none());
95 assert!(det().detect("script alert(1)").is_none());
97 assert!(det().detect("javascript alert(1)").is_none());
98 assert!(det().detect("evaluate this expression carefully").is_none());
99 }
100
101 #[test]
102 fn obfuscated_variants_detected() {
103 for input in [
104 "<SCRIPT>alert(1)</SCRIPT>",
105 "<img src=x OnErRoR=alert(1)>",
106 "JaVaScRiPt:alert(1)",
107 "<SVG/onload=alert(1)>",
108 ] {
109 assert_hit(input);
110 }
111 }
112}