use crate::traits::PayloadOracle;
use serde::Deserialize;
use std::sync::OnceLock;
pub struct XssOracle;
#[derive(Deserialize)]
struct XssRules {
tag: Vec<TagPrefix>,
event: Vec<NamedEntry>,
exec_sink: Vec<NamedEntry>,
js_uri_scheme: Vec<UriPrefix>,
dangerous_sink: Vec<NamedEntry>,
}
#[derive(Deserialize)]
struct TagPrefix {
prefix: String,
}
#[derive(Deserialize)]
struct UriPrefix {
prefix: String,
}
#[derive(Deserialize)]
struct NamedEntry {
name: String,
}
fn xss_rules() -> &'static XssRules {
static RULES: OnceLock<XssRules> = OnceLock::new();
RULES.get_or_init(|| {
let raw = include_str!("../rules/xss/structure.toml");
toml::from_str(raw).expect("rules/xss/structure.toml must parse")
})
}
fn xss_tags() -> &'static [TagPrefix] {
&xss_rules().tag
}
fn xss_events() -> &'static [NamedEntry] {
&xss_rules().event
}
fn xss_exec_sinks() -> &'static [NamedEntry] {
&xss_rules().exec_sink
}
fn js_uri_schemes() -> &'static [UriPrefix] {
&xss_rules().js_uri_scheme
}
fn dangerous_sinks() -> &'static [NamedEntry] {
&xss_rules().dangerous_sink
}
fn has_xss_structure(payload: &str) -> bool {
let lower = payload.to_ascii_lowercase();
let has_tag = xss_tags().iter().any(|t| lower.contains(&t.prefix));
let has_event = xss_events().iter().any(|e| lower.contains(&e.name));
let has_exec = xss_exec_sinks().iter().any(|s| lower.contains(&s.name));
let has_uri = js_uri_schemes()
.iter()
.any(|s| lower.contains(&s.prefix));
let has_dangerous_sink = dangerous_sinks().iter().any(|s| lower.contains(&s.name));
has_uri
|| (has_tag && has_exec && has_dangerous_sink)
|| (has_tag && has_event && has_dangerous_sink)
}
impl PayloadOracle for XssOracle {
fn is_semantically_valid(&self, _original: &str, transformed: &str) -> bool {
has_xss_structure(transformed)
}
fn name(&self) -> &'static str {
"XSS"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classic_script_tag_valid() {
let oracle = XssOracle;
assert!(
oracle.is_semantically_valid("<script>alert(1)</script>", "<script>alert(1)</script>",)
);
}
#[test]
fn img_onerror_valid() {
let oracle = XssOracle;
assert!(oracle.is_semantically_valid(
"<img src=x onerror=alert(1)>",
"<img src=x onerror=alert(1)>",
));
}
#[test]
fn svg_onload_valid() {
let oracle = XssOracle;
assert!(oracle.is_semantically_valid("<svg onload=alert(1)>", "<svg onload=alert(1)>",));
}
#[test]
fn javascript_uri_valid() {
let oracle = XssOracle;
assert!(oracle.is_semantically_valid("javascript:alert(1)", "javascript:alert(1)",));
}
#[test]
fn data_uri_valid() {
let oracle = XssOracle;
assert!(oracle.is_semantically_valid(
"data:text/html,<script>alert(1)</script>",
"data:text/html,<script>alert(1)</script>",
));
}
#[test]
fn broken_tag_invalid() {
let oracle = XssOracle;
assert!(!oracle.is_semantically_valid(
"<script>alert(1)</script>",
"%3Cscript%3Ealert%281%29%3C/script%3E",
));
}
#[test]
fn case_alternation_preserves_structure() {
let oracle = XssOracle;
assert!(
oracle.is_semantically_valid("<script>alert(1)</script>", "<ScRiPt>alert(1)</sCrIpT>",)
);
}
#[test]
fn empty_string_invalid() {
let oracle = XssOracle;
assert!(!oracle.is_semantically_valid("<script>alert(1)</script>", ""));
}
#[test]
fn plain_text_invalid() {
let oracle = XssOracle;
assert!(!oracle.is_semantically_valid("<script>alert(1)</script>", "hello world",));
}
#[test]
fn event_handler_without_explicit_exec_valid() {
let oracle = XssOracle;
assert!(oracle.is_semantically_valid(
"<img src=x onerror=alert(1)>",
"<details open ontoggle=alert(1)>",
));
}
#[test]
fn dom_clobber_valid() {
let oracle = XssOracle;
assert!(oracle.is_semantically_valid(
"<script>alert(1)</script>",
"<form name=body><input name=innerHTML value='<img src=x onerror=alert(1)>'></form>",
));
}
#[test]
fn constructor_chain_valid() {
let oracle = XssOracle;
assert!(oracle.is_semantically_valid(
"<script>alert(1)</script>",
"<img src=x onerror=constructor.constructor('alert(1)')()>",
));
}
}