use super::ProbeContext;
use std::collections::HashMap;
pub fn extract_xss_features(ctx: &ProbeContext, features: &mut HashMap<String, f64>) {
let body = &ctx.response.body;
let body_lower = body.to_lowercase();
let probe = &ctx.probe_payload;
let reflected = body.contains(probe);
let reflected_lower = body_lower.contains(&probe.to_lowercase());
let encoded_probe = probe
.to_lowercase()
.replace('<', "<")
.replace('>', ">");
let has_encoded_reflection =
!reflected && body_lower.contains(&encoded_probe) && !encoded_probe.is_empty();
if has_encoded_reflection {
features.insert("xss:reflection_is_url_encoded".into(), 1.0);
}
if !reflected && !reflected_lower && !has_encoded_reflection {
return; }
if reflected {
features.insert("xss:reflection_unencoded".into(), 1.0);
}
if body_lower.contains("<script") && reflected {
features.insert("xss:script_tag_reflected".into(), 0.95);
}
for handler in &[
"onerror=",
"onload=",
"onclick=",
"onmouseover=",
"onfocus=",
] {
if body_lower.contains(handler) && reflected {
features.insert("xss:event_handler_reflected".into(), 0.95);
break;
}
}
if body_lower.contains("javascript:") && reflected {
features.insert("xss:javascript_uri_reflected".into(), 0.9);
}
if let Some(pos) = body.find(probe) {
let before = &body[..pos];
let in_script = before.rfind("<script").map_or(false, |script_pos| {
!before[script_pos..].contains("</script>")
});
if in_script {
features.insert("xss:reflection_in_script_block".into(), 1.0);
}
let in_attr = before.rfind('=').map_or(false, |eq_pos| {
let after_eq = &before[eq_pos..];
(after_eq.contains('"') && after_eq.matches('"').count() % 2 == 1)
|| (after_eq.contains('\'') && after_eq.matches('\'').count() % 2 == 1)
});
if in_attr {
features.insert("xss:reflection_in_attribute".into(), 1.0);
}
if !in_script && !in_attr {
features.insert("xss:reflection_in_html_body".into(), 1.0);
}
}
if !ctx.response.headers.contains_key("content-security-policy") {
features.insert("xss:no_csp_header".into(), 1.0);
} else if let Some(csp) = ctx.response.headers.get("content-security-policy") {
if csp.contains("unsafe-inline") {
features.insert("xss:csp_allows_unsafe_inline".into(), 1.0);
}
if csp.contains("unsafe-eval") {
features.insert("xss:csp_allows_unsafe_eval".into(), 1.0);
}
if !csp.contains("unsafe-inline") && !csp.contains("unsafe-eval") {
features.insert("xss:csp_blocks_execution".into(), 1.0);
}
}
if let Some(ct) = ctx.response.headers.get("content-type") {
if !ct.contains("text/html") && !ct.contains("application/xhtml") {
features.insert("xss:content_type_not_html".into(), 1.0);
}
}
if let Some(cookie) = ctx.response.headers.get("set-cookie") {
if cookie.to_lowercase().contains("httponly") {
features.insert("xss:httponly_cookie_set".into(), 1.0);
}
}
if let Some(ct) = ctx.response.headers.get("content-type") {
if ct.contains("json") {
features.insert("xss:reflection_in_json_string".into(), 1.0);
}
}
if ctx
.response
.headers
.get("x-content-type-options")
.map(|v| v.contains("nosniff"))
.unwrap_or(false)
{
features.insert("xss:x_content_type_nosniff".into(), 1.0);
}
}
#[cfg(test)]
mod tests {
use super::super::tests::*;
use super::*;
#[test]
fn test_reflected_xss_basic() {
let response = make_response(
"<html><body><script>alert(1)</script></body></html>",
200,
);
let ctx = make_ctx("xss", "<script>alert(1)</script>", response);
let mut features = HashMap::new();
extract_xss_features(&ctx, &mut features);
assert!(features.contains_key("xss:reflection_unencoded"));
assert!(features.contains_key("xss:script_tag_reflected"));
}
#[test]
fn test_no_reflection_no_features() {
let response = make_response("<html><body>Safe content</body></html>", 200);
let ctx = make_ctx("xss", "<script>alert(1)</script>", response);
let mut features = HashMap::new();
extract_xss_features(&ctx, &mut features);
assert!(features.is_empty());
}
#[test]
fn test_event_handler_detection() {
let response = make_response(
"<img src=x onerror=alert(1)>",
200,
);
let ctx = make_ctx("xss", "<img src=x onerror=alert(1)>", response);
let mut features = HashMap::new();
extract_xss_features(&ctx, &mut features);
assert!(features.contains_key("xss:event_handler_reflected"));
}
#[test]
fn test_no_csp_header() {
let response = make_response("<script>alert(1)</script>", 200);
let ctx = make_ctx("xss", "<script>alert(1)</script>", response);
let mut features = HashMap::new();
extract_xss_features(&ctx, &mut features);
assert!(features.contains_key("xss:no_csp_header"));
}
#[test]
fn test_strict_csp_suppression() {
let mut response = make_response("<script>alert(1)</script>", 200);
response.headers.insert(
"content-security-policy".to_string(),
"default-src 'self'; script-src 'self'".to_string(),
);
let ctx = make_ctx("xss", "<script>alert(1)</script>", response);
let mut features = HashMap::new();
extract_xss_features(&ctx, &mut features);
assert!(features.contains_key("xss:csp_blocks_execution"));
assert!(!features.contains_key("xss:no_csp_header"));
}
#[test]
fn test_unsafe_inline_csp() {
let mut response = make_response("<script>alert(1)</script>", 200);
response.headers.insert(
"content-security-policy".to_string(),
"default-src 'self'; script-src 'unsafe-inline'".to_string(),
);
let ctx = make_ctx("xss", "<script>alert(1)</script>", response);
let mut features = HashMap::new();
extract_xss_features(&ctx, &mut features);
assert!(features.contains_key("xss:csp_allows_unsafe_inline"));
}
#[test]
fn test_reflection_in_script_block() {
let response = make_response(
"<html><script>var x = 'PAYLOAD';</script></html>",
200,
);
let ctx = make_ctx("xss", "PAYLOAD", response);
let mut features = HashMap::new();
extract_xss_features(&ctx, &mut features);
assert!(features.contains_key("xss:reflection_in_script_block"));
}
#[test]
fn test_content_type_not_html() {
let mut response = make_response("{\"value\": \"<script>alert(1)</script>\"}", 200);
response.headers.insert(
"content-type".to_string(),
"application/json".to_string(),
);
let ctx = make_ctx("xss", "<script>alert(1)</script>", response);
let mut features = HashMap::new();
extract_xss_features(&ctx, &mut features);
assert!(features.contains_key("xss:content_type_not_html"));
}
}