Skip to main content

termivar_scanner/payload_strategies/
mod.rs

1//! Native, built-in [`crate::payload_strategy::PayloadStrategy`]
2//!
3//! ## Runtime scope
4//!
5//! - **Build:** always/default.
6//! - **Execution:** Surface B (concrete planner-selected payload strategies).
7//! - **Default `termivar scan`:** no.
8//! - **Support:** implemented and tested.
9//!
10//! See `docs/internals/runtime-map.md`.
11//! implementations and their registry.
12//!
13//! The contract module [`crate::payload_strategy`] defines the deterministic,
14//! bounded, redacted boundary a planner-selected strategy must honor. This
15//! module holds the concrete implementations of that contract and the builder
16//! that registers the strategies a standard profile may resolve.
17//!
18//! Implementations here are pure functions of `(role, seed, limits)`. Adding a
19//! strategy requires repeat and concurrency conformance tests before it is
20//! registered by [`standard_payload_strategies`].
21
22use std::sync::Arc;
23
24use crate::payload_strategy::{PayloadStrategyError, PayloadStrategyRegistry};
25
26pub mod api_authorization_context_pair;
27pub mod cors_origin_pair;
28pub mod encoding;
29pub mod external_url_query_pair;
30pub mod http_header_control_pair;
31#[cfg(feature = "normalization-resilience")]
32pub(crate) mod normalization_resilience_query_pair;
33pub mod reflection_marker_query_pair;
34pub mod sql_quote_balance_query_pair;
35pub mod ssti_arithmetic_expression_pair;
36#[cfg(feature = "template-evaluation-review")]
37pub mod template_jinja_uppercase_expression_pair;
38pub mod xss_attribute_boundary_query_pair;
39pub mod xss_javascript_lexical_boundary_query_pair;
40pub mod xss_structural_query_pair;
41
42pub use api_authorization_context_pair::{
43    ApiAuthorizationContextPairStrategy, API_AUTHORIZATION_CONTEXT_PAIR_HEADER_NAME,
44    API_AUTHORIZATION_CONTEXT_PAIR_ID, API_AUTHORIZATION_CONTEXT_PAIR_REVISION,
45};
46pub use cors_origin_pair::{
47    CorsOriginPairStrategy, CORS_ORIGIN_PAIR_HEADER_NAME, CORS_ORIGIN_PAIR_ID,
48    CORS_ORIGIN_PAIR_REVISION,
49};
50pub use encoding::{encode_into_artifact, hex_encode, percent_encode, PayloadEncoding};
51pub use external_url_query_pair::{
52    ExternalUrlQueryPairStrategy, EXTERNAL_URL_QUERY_PAIR_ID, EXTERNAL_URL_QUERY_PAIR_REVISION,
53};
54pub use http_header_control_pair::{
55    HttpHeaderControlPairStrategy, HTTP_HEADER_CONTROL_PAIR_HEADER_NAME,
56    HTTP_HEADER_CONTROL_PAIR_ID, HTTP_HEADER_CONTROL_PAIR_REVISION,
57};
58#[cfg(feature = "normalization-resilience")]
59pub(crate) use normalization_resilience_query_pair::NormalizationResilienceQueryPairStrategy;
60pub use reflection_marker_query_pair::{
61    ReflectionMarkerQueryPairStrategy, REFLECTION_MARKER_QUERY_PAIR_ID,
62    REFLECTION_MARKER_QUERY_PAIR_REVISION,
63};
64pub use sql_quote_balance_query_pair::{
65    SqlQuoteBalanceQueryPairStrategy, SQL_QUOTE_BALANCE_QUERY_PAIR_ID,
66    SQL_QUOTE_BALANCE_QUERY_PAIR_REVISION,
67};
68pub use ssti_arithmetic_expression_pair::{
69    SstiArithmeticExpressionPairStrategy, SSTI_ARITHMETIC_EXPRESSION_PAIR_ID,
70    SSTI_ARITHMETIC_EXPRESSION_PAIR_REVISION,
71};
72#[cfg(feature = "template-evaluation-review")]
73pub use template_jinja_uppercase_expression_pair::{
74    TemplateJinjaUppercaseExpressionPairStrategy, TEMPLATE_JINJA_UPPERCASE_EXPRESSION_PAIR_ID,
75    TEMPLATE_JINJA_UPPERCASE_EXPRESSION_PAIR_REVISION,
76};
77pub use xss_attribute_boundary_query_pair::{
78    XssAttributeBoundaryQueryPairStrategy, XSS_ATTRIBUTE_BOUNDARY_QUERY_PAIR_ID,
79    XSS_ATTRIBUTE_BOUNDARY_QUERY_PAIR_REVISION,
80};
81pub(crate) use xss_javascript_lexical_boundary_query_pair::XssJavascriptLexicalProbeTokens;
82pub use xss_javascript_lexical_boundary_query_pair::{
83    XssJavascriptLexicalBoundaryQueryPairStrategy, XSS_JAVASCRIPT_LEXICAL_BOUNDARY_QUERY_PAIR_ID,
84    XSS_JAVASCRIPT_LEXICAL_BOUNDARY_QUERY_PAIR_REVISION,
85};
86pub use xss_structural_query_pair::{
87    XssStructuralQueryPairStrategy, XSS_STRUCTURAL_QUERY_PAIR_ID,
88    XSS_STRUCTURAL_QUERY_PAIR_REVISION,
89};
90
91/// Builds the registry of payload strategies a standard profile may resolve.
92///
93/// Every entry is a native, conformance-tested implementation. Registration is
94/// deterministic and order-independent, and a duplicate identity is a
95/// programmer error surfaced as [`PayloadStrategyError::StrategyIdentityConflict`].
96pub fn standard_payload_strategies() -> Result<PayloadStrategyRegistry, PayloadStrategyError> {
97    let mut registry = PayloadStrategyRegistry::new();
98    registry.register(Arc::new(HttpHeaderControlPairStrategy::new()))?;
99    #[cfg(feature = "normalization-resilience")]
100    registry.register(Arc::new(NormalizationResilienceQueryPairStrategy::new()))?;
101    registry.register(Arc::new(ApiAuthorizationContextPairStrategy::new()))?;
102    registry.register(Arc::new(CorsOriginPairStrategy::new()))?;
103    registry.register(Arc::new(ExternalUrlQueryPairStrategy::new()))?;
104    registry.register(Arc::new(ReflectionMarkerQueryPairStrategy::new()))?;
105    registry.register(Arc::new(SqlQuoteBalanceQueryPairStrategy::new()))?;
106    registry.register(Arc::new(SstiArithmeticExpressionPairStrategy::new()))?;
107    #[cfg(feature = "template-evaluation-review")]
108    registry.register(Arc::new(TemplateJinjaUppercaseExpressionPairStrategy::new()))?;
109    registry.register(Arc::new(XssAttributeBoundaryQueryPairStrategy::new()))?;
110    registry.register(Arc::new(
111        XssJavascriptLexicalBoundaryQueryPairStrategy::new(),
112    ))?;
113    registry.register(Arc::new(XssStructuralQueryPairStrategy::new()))?;
114    Ok(registry)
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::payload_strategy::{
121        PayloadSeed, PayloadStrategyLimits, PayloadStrategyRef, PayloadVariantRole,
122    };
123
124    #[test]
125    fn standard_registry_registers_every_built_in_strategy() {
126        let registry = standard_payload_strategies().unwrap();
127        let header_pair = PayloadStrategyRef::new(
128            HTTP_HEADER_CONTROL_PAIR_ID,
129            HTTP_HEADER_CONTROL_PAIR_REVISION,
130        )
131        .unwrap();
132        let authorization_pair = PayloadStrategyRef::new(
133            API_AUTHORIZATION_CONTEXT_PAIR_ID,
134            API_AUTHORIZATION_CONTEXT_PAIR_REVISION,
135        )
136        .unwrap();
137        let cors_origin_pair =
138            PayloadStrategyRef::new(CORS_ORIGIN_PAIR_ID, CORS_ORIGIN_PAIR_REVISION).unwrap();
139        let external_url_query_pair =
140            PayloadStrategyRef::new(EXTERNAL_URL_QUERY_PAIR_ID, EXTERNAL_URL_QUERY_PAIR_REVISION)
141                .unwrap();
142        let reflection_marker_pair = PayloadStrategyRef::new(
143            REFLECTION_MARKER_QUERY_PAIR_ID,
144            REFLECTION_MARKER_QUERY_PAIR_REVISION,
145        )
146        .unwrap();
147
148        let sql_quote_pair = PayloadStrategyRef::new(
149            SQL_QUOTE_BALANCE_QUERY_PAIR_ID,
150            SQL_QUOTE_BALANCE_QUERY_PAIR_REVISION,
151        )
152        .unwrap();
153
154        let ssti_arithmetic_pair = PayloadStrategyRef::new(
155            SSTI_ARITHMETIC_EXPRESSION_PAIR_ID,
156            SSTI_ARITHMETIC_EXPRESSION_PAIR_REVISION,
157        )
158        .unwrap();
159
160        let xss_structural_pair = PayloadStrategyRef::new(
161            XSS_STRUCTURAL_QUERY_PAIR_ID,
162            XSS_STRUCTURAL_QUERY_PAIR_REVISION,
163        )
164        .unwrap();
165
166        let xss_attribute_boundary_pair = PayloadStrategyRef::new(
167            XSS_ATTRIBUTE_BOUNDARY_QUERY_PAIR_ID,
168            XSS_ATTRIBUTE_BOUNDARY_QUERY_PAIR_REVISION,
169        )
170        .unwrap();
171
172        let xss_javascript_lexical_boundary_pair = PayloadStrategyRef::new(
173            XSS_JAVASCRIPT_LEXICAL_BOUNDARY_QUERY_PAIR_ID,
174            XSS_JAVASCRIPT_LEXICAL_BOUNDARY_QUERY_PAIR_REVISION,
175        )
176        .unwrap();
177
178        let expected_len = 10
179            + cfg!(feature = "normalization-resilience") as usize
180            + cfg!(feature = "template-evaluation-review") as usize;
181        assert_eq!(registry.len(), expected_len);
182        #[cfg(feature = "normalization-resilience")]
183        {
184            use normalization_resilience_query_pair::{
185                NORMALIZATION_RESILIENCE_QUERY_PAIR_ID,
186                NORMALIZATION_RESILIENCE_QUERY_PAIR_REVISION,
187            };
188
189            let normalization = PayloadStrategyRef::new(
190                NORMALIZATION_RESILIENCE_QUERY_PAIR_ID,
191                NORMALIZATION_RESILIENCE_QUERY_PAIR_REVISION,
192            )
193            .unwrap();
194            assert!(registry.contains(&normalization));
195        }
196        #[cfg(feature = "template-evaluation-review")]
197        {
198            let template = PayloadStrategyRef::new(
199                TEMPLATE_JINJA_UPPERCASE_EXPRESSION_PAIR_ID,
200                TEMPLATE_JINJA_UPPERCASE_EXPRESSION_PAIR_REVISION,
201            )
202            .unwrap();
203            assert!(registry.contains(&template));
204        }
205        assert!(registry.contains(&header_pair));
206        assert!(registry.contains(&authorization_pair));
207        assert!(registry.contains(&cors_origin_pair));
208        assert!(registry.contains(&external_url_query_pair));
209        assert!(registry.contains(&reflection_marker_pair));
210        assert!(registry.contains(&sql_quote_pair));
211        assert!(registry.contains(&ssti_arithmetic_pair));
212        assert!(registry.contains(&xss_attribute_boundary_pair));
213        assert!(registry.contains(&xss_javascript_lexical_boundary_pair));
214        assert!(registry.contains(&xss_structural_pair));
215    }
216
217    #[test]
218    fn standard_registry_can_derive_a_pair_for_its_strategy() {
219        let registry = standard_payload_strategies().unwrap();
220        let reference = PayloadStrategyRef::new(
221            HTTP_HEADER_CONTROL_PAIR_ID,
222            HTTP_HEADER_CONTROL_PAIR_REVISION,
223        )
224        .unwrap();
225        let limits = PayloadStrategyLimits::default();
226        let seed = PayloadSeed::new(b"application/json".to_vec(), limits).unwrap();
227
228        let control = registry
229            .derive_one(&reference, PayloadVariantRole::Control, &seed, limits)
230            .unwrap();
231        let candidate = registry
232            .derive_one(&reference, PayloadVariantRole::Candidate, &seed, limits)
233            .unwrap();
234
235        assert_eq!(control.as_bytes(), b"*/*");
236        assert_eq!(candidate.as_bytes(), b"*/*, application/json");
237        assert_ne!(control.receipt().sha256(), candidate.receipt().sha256());
238    }
239}