termivar_scanner/payload_strategies/
xss_attribute_boundary_query_pair.rs1use crate::payload_strategy::{
8 PayloadArtifact, PayloadSeed, PayloadStrategy, PayloadStrategyError, PayloadStrategyLimits,
9 PayloadStrategyRef, PayloadVariantRole,
10};
11
12pub const XSS_ATTRIBUTE_BOUNDARY_QUERY_PAIR_ID: &str =
13 "web.review.xss.attribute-boundary-query-pair";
14pub const XSS_ATTRIBUTE_BOUNDARY_QUERY_PAIR_REVISION: u32 = 1;
15
16#[derive(Debug, Clone)]
17pub struct XssAttributeBoundaryQueryPairStrategy {
18 reference: PayloadStrategyRef,
19}
20
21impl XssAttributeBoundaryQueryPairStrategy {
22 pub fn new() -> Self {
23 Self {
24 reference: PayloadStrategyRef::new(
25 XSS_ATTRIBUTE_BOUNDARY_QUERY_PAIR_ID,
26 XSS_ATTRIBUTE_BOUNDARY_QUERY_PAIR_REVISION,
27 )
28 .expect("the XSS attribute-boundary strategy identity is static and valid"),
29 }
30 }
31
32 fn derive_value(seed: &[u8], role: PayloadVariantRole) -> Option<String> {
33 let seed = std::str::from_utf8(seed).ok()?;
34 let mut parts = seed.split(':');
35 let family = parts.next()?;
36 let quote_mode = parts.next()?;
37 let identity = parts.next()?;
38 if parts.next().is_some()
39 || !matches!(family, "attribute" | "uri" | "handler")
40 || !matches!(quote_mode, "double-quoted" | "single-quoted" | "unquoted")
41 || identity.len() != 32
42 || !identity
43 .bytes()
44 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
45 {
46 return None;
47 }
48 if role == PayloadVariantRole::Control {
49 return Some(format!("venom-xss-attribute-control-{identity}-end"));
50 }
51 match quote_mode {
52 "double-quoted" => Some(format!(
53 "\" data-venom-xss-boundary-token=\"{identity}\" data-venom-xss-tail-token=\"{identity}"
54 )),
55 "single-quoted" => Some(format!(
56 "' data-venom-xss-boundary-token='{identity}' data-venom-xss-tail-token='{identity}"
57 )),
58 "unquoted" => Some(format!(
59 "venom-xss-inert-{identity} data-venom-xss-boundary-token={identity} data-venom-xss-tail-token={identity}"
60 )),
61 _ => None,
62 }
63 }
64}
65
66impl Default for XssAttributeBoundaryQueryPairStrategy {
67 fn default() -> Self {
68 Self::new()
69 }
70}
71
72impl PayloadStrategy for XssAttributeBoundaryQueryPairStrategy {
73 fn strategy_ref(&self) -> &PayloadStrategyRef {
74 &self.reference
75 }
76
77 fn derive_one(
78 &self,
79 role: PayloadVariantRole,
80 seed: &PayloadSeed,
81 limits: PayloadStrategyLimits,
82 ) -> Result<PayloadArtifact, PayloadStrategyError> {
83 let value = Self::derive_value(seed.as_bytes(), role)
84 .ok_or(PayloadStrategyError::DerivationFailed)?;
85 PayloadArtifact::new(self.reference.clone(), role, value.into_bytes(), limits)
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 const IDENTITY: &str = "0123456789abcdef0123456789abcdef";
94
95 fn limits() -> PayloadStrategyLimits {
96 PayloadStrategyLimits::new(256, 256).unwrap()
97 }
98
99 fn derive(family: &str, quote: &str, role: PayloadVariantRole) -> String {
100 let strategy = XssAttributeBoundaryQueryPairStrategy::new();
101 let seed = PayloadSeed::new(
102 format!("{family}:{quote}:{IDENTITY}").into_bytes(),
103 limits(),
104 )
105 .unwrap();
106 String::from_utf8(
107 strategy
108 .derive_one(role, &seed, limits())
109 .unwrap()
110 .as_bytes()
111 .to_vec(),
112 )
113 .unwrap()
114 }
115
116 #[test]
117 fn quote_variants_have_exact_inert_bounded_wire_shapes() {
118 assert_eq!(
119 derive("attribute", "double-quoted", PayloadVariantRole::Candidate),
120 format!(
121 "\" data-venom-xss-boundary-token=\"{IDENTITY}\" data-venom-xss-tail-token=\"{IDENTITY}"
122 )
123 );
124 assert_eq!(
125 derive("uri", "single-quoted", PayloadVariantRole::Candidate),
126 format!(
127 "' data-venom-xss-boundary-token='{IDENTITY}' data-venom-xss-tail-token='{IDENTITY}"
128 )
129 );
130 assert_eq!(
131 derive("handler", "unquoted", PayloadVariantRole::Candidate),
132 format!(
133 "venom-xss-inert-{IDENTITY} data-venom-xss-boundary-token={IDENTITY} data-venom-xss-tail-token={IDENTITY}"
134 )
135 );
136
137 for family in ["attribute", "uri", "handler"] {
138 for quote in ["double-quoted", "single-quoted", "unquoted"] {
139 let control = derive(family, quote, PayloadVariantRole::Control);
140 let candidate = derive(family, quote, PayloadVariantRole::Candidate);
141 assert_ne!(control, candidate);
142 assert!(candidate.len() <= 256);
143 for forbidden in [
144 "alert",
145 "prompt",
146 "confirm",
147 "javascript:",
148 "data:",
149 "fetch",
150 "document.",
151 "window.",
152 "http://",
153 "https://",
154 "//",
155 ] {
156 assert!(!candidate.contains(forbidden), "{forbidden} in {candidate}");
157 }
158 }
159 }
160 }
161
162 #[test]
163 fn unknown_family_quote_and_noncanonical_identity_fail_closed() {
164 let strategy = XssAttributeBoundaryQueryPairStrategy::new();
165 for value in [
166 format!("unknown:double-quoted:{IDENTITY}"),
167 format!("attribute:unknown:{IDENTITY}"),
168 "attribute:double-quoted:short".to_owned(),
169 "attribute:double-quoted:0123456789ABCDEF0123456789ABCDEF".to_owned(),
170 format!("attribute:double-quoted:{IDENTITY}:extra"),
171 ] {
172 let seed = PayloadSeed::new(value.into_bytes(), limits()).unwrap();
173 for role in [PayloadVariantRole::Control, PayloadVariantRole::Candidate] {
174 assert!(matches!(
175 strategy.derive_one(role, &seed, limits()),
176 Err(PayloadStrategyError::DerivationFailed)
177 ));
178 }
179 }
180 }
181}