termivar_scanner/payload_strategies/
sql_quote_balance_query_pair.rs1use crate::payload_strategy::{
9 PayloadArtifact, PayloadSeed, PayloadStrategy, PayloadStrategyError, PayloadStrategyLimits,
10 PayloadStrategyRef, PayloadVariantRole,
11};
12
13pub const SQL_QUOTE_BALANCE_QUERY_PAIR_ID: &str = "web.review.sql.quote-balance-query-pair";
14pub const SQL_QUOTE_BALANCE_QUERY_PAIR_REVISION: u32 = 1;
15
16#[derive(Debug, Clone)]
17pub struct SqlQuoteBalanceQueryPairStrategy {
18 reference: PayloadStrategyRef,
19}
20
21impl SqlQuoteBalanceQueryPairStrategy {
22 pub fn new() -> Self {
23 Self {
24 reference: PayloadStrategyRef::new(
25 SQL_QUOTE_BALANCE_QUERY_PAIR_ID,
26 SQL_QUOTE_BALANCE_QUERY_PAIR_REVISION,
27 )
28 .expect("the SQL quote-balance strategy identity is static and valid"),
29 }
30 }
31
32 fn valid_seed(seed: &[u8]) -> bool {
33 (8..=64).contains(&seed.len())
34 && seed.iter().all(|byte| {
35 byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~')
36 })
37 }
38}
39
40impl Default for SqlQuoteBalanceQueryPairStrategy {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl PayloadStrategy for SqlQuoteBalanceQueryPairStrategy {
47 fn strategy_ref(&self) -> &PayloadStrategyRef {
48 &self.reference
49 }
50
51 fn derive_one(
52 &self,
53 role: PayloadVariantRole,
54 seed: &PayloadSeed,
55 limits: PayloadStrategyLimits,
56 ) -> Result<PayloadArtifact, PayloadStrategyError> {
57 if !Self::valid_seed(seed.as_bytes()) {
58 return Err(PayloadStrategyError::DerivationFailed);
59 }
60 let mut bytes = seed.as_bytes().to_vec();
61 if role == PayloadVariantRole::Candidate {
62 bytes.push(b'\'');
63 }
64 PayloadArtifact::new(self.reference.clone(), role, bytes, limits)
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 fn limits() -> PayloadStrategyLimits {
73 PayloadStrategyLimits::new(64, 65).unwrap()
74 }
75
76 #[test]
77 fn catalog_is_exactly_one_quote_and_contains_no_sql_control_syntax() {
78 let strategy = SqlQuoteBalanceQueryPairStrategy::new();
79 let seed = PayloadSeed::new(b"venom-review-a1b2c3".to_vec(), limits()).unwrap();
80 let control = strategy
81 .derive_one(PayloadVariantRole::Control, &seed, limits())
82 .unwrap();
83 let candidate = strategy
84 .derive_one(PayloadVariantRole::Candidate, &seed, limits())
85 .unwrap();
86 assert_eq!(control.as_bytes(), b"venom-review-a1b2c3");
87 assert_eq!(candidate.as_bytes(), b"venom-review-a1b2c3'");
88 for forbidden in [b";".as_slice(), b"--", b"/*", b"=", b"(", b")"] {
89 assert!(!candidate
90 .as_bytes()
91 .windows(forbidden.len())
92 .any(|w| w == forbidden));
93 }
94 }
95
96 #[test]
97 fn unsafe_or_ambiguous_seeds_fail_closed() {
98 let strategy = SqlQuoteBalanceQueryPairStrategy::new();
99 for value in [
100 b"short".as_slice(),
101 b"contains space",
102 b"contains'quote",
103 b"contains;statement",
104 b"contains=operator",
105 ] {
106 let seed = PayloadSeed::new(value.to_vec(), limits()).unwrap();
107 for role in [PayloadVariantRole::Control, PayloadVariantRole::Candidate] {
108 assert!(matches!(
109 strategy.derive_one(role, &seed, limits()),
110 Err(PayloadStrategyError::DerivationFailed)
111 ));
112 }
113 }
114 }
115
116 #[test]
117 fn repeat_is_byte_deterministic_and_bounded() {
118 let strategy = SqlQuoteBalanceQueryPairStrategy::new();
119 let seed = PayloadSeed::new(b"venom-review-repeat".to_vec(), limits()).unwrap();
120 let first = strategy
121 .derive_one(PayloadVariantRole::Candidate, &seed, limits())
122 .unwrap();
123 let second = strategy
124 .derive_one(PayloadVariantRole::Candidate, &seed, limits())
125 .unwrap();
126 assert_eq!(first, second);
127 assert_eq!(first.strategy(), strategy.strategy_ref());
128 }
129}