termivar_scanner/payload_strategies/
xss_structural_query_pair.rs1use crate::payload_strategy::{
7 PayloadArtifact, PayloadSeed, PayloadStrategy, PayloadStrategyError, PayloadStrategyLimits,
8 PayloadStrategyRef, PayloadVariantRole,
9};
10
11pub const XSS_STRUCTURAL_QUERY_PAIR_ID: &str = "web.review.xss.structural-query-pair";
12pub const XSS_STRUCTURAL_QUERY_PAIR_REVISION: u32 = 1;
13
14#[derive(Debug, Clone)]
15pub struct XssStructuralQueryPairStrategy {
16 reference: PayloadStrategyRef,
17}
18
19impl XssStructuralQueryPairStrategy {
20 pub fn new() -> Self {
21 Self {
22 reference: PayloadStrategyRef::new(
23 XSS_STRUCTURAL_QUERY_PAIR_ID,
24 XSS_STRUCTURAL_QUERY_PAIR_REVISION,
25 )
26 .expect("the XSS structural strategy identity is static and valid"),
27 }
28 }
29
30 fn derive_value(seed: &[u8], role: PayloadVariantRole) -> Option<String> {
31 let seed = std::str::from_utf8(seed).ok()?;
32 let (family, identity) = seed.split_once(':')?;
33 if identity.len() != 32
34 || !identity
35 .bytes()
36 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
37 {
38 return None;
39 }
40 if role == PayloadVariantRole::Control {
41 return Some(format!("venom-xss-control-{identity}-end"));
42 }
43 match family {
44 "html" => Some(format!(
45 "<span data-venom-xss-boundary-token=\"{identity}\"></span>"
46 )),
47 "uri" => Some(format!(
48 "venom-xss-{identity}/segment?probe={identity}#boundary"
49 )),
50 "handler" => Some(format!("/*venom-xss-handler-{identity}*/")),
51 "script" => Some(format!("/*venom-xss-script-{identity}*/")),
52 _ => None,
53 }
54 }
55}
56
57impl Default for XssStructuralQueryPairStrategy {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63impl PayloadStrategy for XssStructuralQueryPairStrategy {
64 fn strategy_ref(&self) -> &PayloadStrategyRef {
65 &self.reference
66 }
67
68 fn derive_one(
69 &self,
70 role: PayloadVariantRole,
71 seed: &PayloadSeed,
72 limits: PayloadStrategyLimits,
73 ) -> Result<PayloadArtifact, PayloadStrategyError> {
74 let value = Self::derive_value(seed.as_bytes(), role)
75 .ok_or(PayloadStrategyError::DerivationFailed)?;
76 PayloadArtifact::new(self.reference.clone(), role, value.into_bytes(), limits)
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 fn limits() -> PayloadStrategyLimits {
85 PayloadStrategyLimits::new(256, 256).unwrap()
86 }
87
88 #[test]
89 fn closed_families_create_distinct_bounded_non_networking_candidates() {
90 let strategy = XssStructuralQueryPairStrategy::new();
91 for family in ["html", "uri", "handler", "script"] {
92 let seed = PayloadSeed::new(
93 format!("{family}:0123456789abcdef0123456789abcdef").into_bytes(),
94 limits(),
95 )
96 .unwrap();
97 let control = strategy
98 .derive_one(PayloadVariantRole::Control, &seed, limits())
99 .unwrap();
100 let candidate = strategy
101 .derive_one(PayloadVariantRole::Candidate, &seed, limits())
102 .unwrap();
103 assert_ne!(control.as_bytes(), candidate.as_bytes());
104 assert!(candidate.as_bytes().len() <= 160);
105 let text = std::str::from_utf8(candidate.as_bytes()).unwrap();
106 for forbidden in [
107 "alert(",
108 "javascript:",
109 "data:",
110 "http://",
111 "https://",
112 "//",
113 "fetch(",
114 "document.",
115 "window.",
116 ] {
117 assert!(
118 !text.contains(forbidden),
119 "unsafe token {forbidden} in {text}"
120 );
121 }
122 }
123 }
124
125 #[test]
126 fn unknown_family_and_noncanonical_identity_fail_closed() {
127 let strategy = XssStructuralQueryPairStrategy::new();
128 for seed in [
129 "unknown:0123456789abcdef0123456789abcdef",
130 "html:short",
131 "html:0123456789ABCDEF0123456789ABCDEF",
132 ] {
133 let seed = PayloadSeed::new(seed.as_bytes().to_vec(), limits()).unwrap();
134 assert!(matches!(
135 strategy.derive_one(PayloadVariantRole::Candidate, &seed, limits()),
136 Err(PayloadStrategyError::DerivationFailed)
137 ));
138 }
139 }
140}