Skip to main content

attackstr/
encoding.rs

1//! Encoding transforms  -  applied to payloads after template expansion.
2//!
3//! Built-in encodings cover the most common evasion techniques.
4//! Custom encodings can be registered via [`PayloadDb::register_encoding`].
5
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8use std::fmt::Write;
9
10/// A trait for encoding transforms.
11///
12/// Implement this trait to create custom encoders that can be used
13/// with the attackstr encoding system.
14///
15/// # Thread Safety
16/// This trait does not require `Send` or `Sync`. Thread-safety depends on the
17/// concrete implementing type.
18///
19/// # Example
20///
21/// ```rust
22/// use attackstr::Encoder;
23///
24/// struct Rot13Encoder;
25///
26/// impl Encoder for Rot13Encoder {
27///     fn encode(&self, input: &str) -> String {
28///         input.chars().map(|c| match c {
29///             'a'..='m' | 'A'..='M' => (c as u8 + 13) as char,
30///             'n'..='z' | 'N'..='Z' => (c as u8 - 13) as char,
31///             _ => c,
32///         }).collect()
33///     }
34/// }
35///
36/// let encoder = Rot13Encoder;
37/// assert_eq!(encoder.encode("hello"), "uryyb");
38/// ```
39pub trait Encoder {
40    /// Encode the input string.
41    fn encode(&self, input: &str) -> String;
42}
43
44impl<F> Encoder for F
45where
46    F: Fn(&str) -> String,
47{
48    fn encode(&self, input: &str) -> String {
49        self(input)
50    }
51}
52
53/// Errors returned by encoding operations.
54///
55/// # Thread Safety
56/// `EncodingError` is `Send` and `Sync`.
57#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
58#[non_exhaustive]
59pub enum EncodingError {
60    /// The requested encoding transform is not known.
61    #[error("unknown encoding transform '{transform}'. Fix: use a known built-in or register a custom encoding.")]
62    UnknownTransform {
63        /// Name of the unrecognized transform.
64        transform: String,
65    },
66}
67
68/// A custom encoder that wraps a callable.
69///
70/// This is useful for creating encoders from closures or function pointers
71/// without defining a new type. Closures may capture state.
72///
73/// # Thread Safety
74/// `CustomEncoder` is `Send` and `Sync`.
75///
76/// # Example
77///
78/// ```rust
79/// use attackstr::{CustomEncoder, Encoder};
80///
81/// let salt = "abc".to_string();
82/// let encoder = CustomEncoder::new(move |s: &str| format!("{salt}{s}"));
83/// assert_eq!(encoder.encode("hello"), "abchello");
84/// ```
85#[derive(Clone)]
86pub struct CustomEncoder {
87    func: Arc<dyn Fn(&str) -> String + Send + Sync>,
88}
89
90impl CustomEncoder {
91    /// Create a new `CustomEncoder` from a closure or function pointer.
92    ///
93    /// Example:
94    /// ```rust
95    /// use attackstr::{CustomEncoder, Encoder};
96    ///
97    /// let encoder = CustomEncoder::new(|value| value.to_uppercase());
98    /// assert_eq!(encoder.encode("xss"), "XSS");
99    /// ```
100    pub fn new<F>(func: F) -> Self
101    where
102        F: Fn(&str) -> String + Send + Sync + 'static,
103    {
104        Self {
105            func: Arc::new(func),
106        }
107    }
108
109    /// Apply the encoding to an input string.
110    ///
111    /// Example:
112    /// ```rust
113    /// use attackstr::CustomEncoder;
114    ///
115    /// let encoder = CustomEncoder::new(|value| format!("<{value}>"));
116    /// assert_eq!(encoder.encode("a"), "<a>");
117    /// ```
118    pub fn encode(&self, input: &str) -> String {
119        (self.func)(input)
120    }
121}
122
123impl std::fmt::Debug for CustomEncoder {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        f.debug_struct("CustomEncoder").finish_non_exhaustive()
126    }
127}
128
129impl Default for CustomEncoder {
130    fn default() -> Self {
131        Self::new(std::string::ToString::to_string)
132    }
133}
134
135impl Encoder for CustomEncoder {
136    fn encode(&self, input: &str) -> String {
137        self.encode(input)
138    }
139}
140
141impl std::fmt::Display for CustomEncoder {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        f.write_str("CustomEncoder(..)")
144    }
145}
146
147/// Apply a built-in encoding transform by name.
148///
149/// Returns [`EncodingError::UnknownTransform`] if the transform name is not
150/// recognized.
151///
152/// Example:
153/// ```rust
154/// use attackstr::apply_encoding;
155///
156/// assert_eq!(apply_encoding("a b", "url").unwrap(), "a%20b");
157/// ```
158pub fn apply_encoding(s: &str, transform: &str) -> Result<String, EncodingError> {
159    // The `BuiltinEncoding` enum is the single owner of the encoding-name set:
160    // parsing resolves the name (canonical or alias) and `apply` dispatches.
161    let encoding: BuiltinEncoding = transform.parse()?;
162    Ok(encoding.apply(s))
163}
164
165fn percent_hex_encode(s: &str) -> String {
166    s.bytes()
167        .fold(String::with_capacity(s.len() * 3), |mut acc, b| {
168            write!(&mut acc, "%{b:02x}").expect("writing to a String sink is infallible");
169            acc
170        })
171}
172
173fn unicode_escape(s: &str) -> String {
174    s.chars()
175        .fold(String::with_capacity(s.len() * 6), |mut acc, c| {
176            use std::fmt::Write;
177            let u = c as u32;
178            if u > 0xFFFF {
179                // Generate UTF-16 surrogate pair for JS compatibility.
180                let code = u - 0x1_0000;
181                let high = 0xD800 + (code >> 10);
182                let low = 0xDC00 + (code & 0x3FF);
183                write!(&mut acc, "\\u{:04x}\\u{:04x}", high, low)
184                    .expect("writing to a String sink is infallible");
185            } else {
186                write!(&mut acc, "\\u{:04x}", u)
187                    .expect("writing to a String sink is infallible");
188            }
189            acc
190        })
191}
192
193fn octal_escape(s: &str) -> String {
194    s.bytes()
195        .fold(String::with_capacity(s.len() * 4), |mut acc, b| {
196            write!(&mut acc, "\\{b:03o}").expect("writing to a String sink is infallible");
197            acc
198        })
199}
200
201fn js_charcode(s: &str) -> String {
202    let codes: Vec<String> = s.chars().map(|c| (c as u32).to_string()).collect();
203    format!("String.fromCodePoint({})", codes.join(","))
204}
205
206fn js_concat_split(s: &str) -> String {
207    // Each char becomes a single-quoted JS string literal joined with '+'.
208    // Characters with structural meaning inside a single-quoted literal
209    // ('\'' and '\\') and the whitespace controls must be escaped, or the
210    // generated JS is syntactically invalid (e.g. a literal ' yielded '''').
211    let parts: Vec<String> = s
212        .chars()
213        .map(|c| match c {
214            '\'' => "'\\''".to_string(),
215            '\\' => "'\\\\'".to_string(),
216            '\n' => "'\\n'".to_string(),
217            '\r' => "'\\r'".to_string(),
218            '\t' => "'\\t'".to_string(),
219            other => format!("'{other}'"),
220        })
221        .collect();
222    parts.join("+")
223}
224
225/// Alternating-case transform, Unicode-aware, keyed on char index plus
226/// `offset`. The single owner for both the `case_alternate` encoding and the
227/// `mutate_case`/tag-casing mutation paths; `offset` 0 lowercases even
228/// indices, `offset` 1 uppercases them.
229pub(crate) fn alternate_case(s: &str, offset: usize) -> String {
230    // Build in one buffer instead of allocating a `String` per char. Keeps the
231    // Unicode-aware case mapping (`char::to_lowercase`/`to_uppercase` can yield
232    // more than one char, e.g. 'İ'), so the result is byte-identical.
233    let mut out = String::with_capacity(s.len());
234    for (i, c) in s.chars().enumerate() {
235        if (i + offset) % 2 == 0 {
236            out.extend(c.to_lowercase());
237        } else {
238            out.extend(c.to_uppercase());
239        }
240    }
241    out
242}
243
244fn join_chars_with(s: &str, separator: &str) -> String {
245    // Build the result in one allocation instead of collecting N single-char
246    // `String`s into a `Vec` and re-joining (N+1 allocations).
247    let char_count = s.chars().count();
248    let mut out = String::with_capacity(s.len() + separator.len() * char_count.saturating_sub(1));
249    let mut chars = s.chars();
250    if let Some(first) = chars.next() {
251        out.push(first);
252        for c in chars {
253            out.push_str(separator);
254            out.push(c);
255        }
256    }
257    out
258}
259
260fn php_chr_concat(s: &str) -> String {
261    let parts: Vec<String> = s.bytes().map(|b| format!("chr({b})")).collect();
262    parts.join(".")
263}
264
265fn python_chr_join(s: &str) -> String {
266    let parts: Vec<String> = s.chars().map(|c| format!("chr({})", c as u32)).collect();
267    format!("\"\".join([{}])", parts.join(","))
268}
269
270fn sql_char_concat(s: &str) -> String {
271    let parts: Vec<String> = s.bytes().map(|b| format!("CHAR({b})")).collect();
272    format!("CONCAT({})", parts.join(","))
273}
274
275fn rot13_encode(s: &str) -> String {
276    s.chars()
277        .map(|c| match c {
278            'a'..='m' | 'A'..='M' => (c as u8 + 13) as char,
279            'n'..='z' | 'N'..='Z' => (c as u8 - 13) as char,
280            _ => c,
281        })
282        .collect()
283}
284
285fn css_escape(s: &str) -> String {
286    s.chars()
287        .fold(String::with_capacity(s.len() * 6), |mut acc, c| {
288            write!(&mut acc, "\\{:02x}", c as u32)
289                .expect("writing to a String sink is infallible");
290            acc
291        })
292}
293
294/// All built-in encoding names, for documentation and validation.
295///
296/// # Thread Safety
297/// `BuiltinEncoding` is `Send` and `Sync`.
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
299#[non_exhaustive]
300pub enum BuiltinEncoding {
301    /// No encoding.
302    Identity,
303    /// URL percent-encoding.
304    UrlEncode,
305    /// Double URL encoding.
306    DoubleUrl,
307    /// Hex percent-encoding.
308    Hex,
309    /// Unicode \uXXXX escapes.
310    Unicode,
311    /// HTML entity encoding.
312    HtmlEntities,
313    /// Append null byte.
314    NullByte,
315    /// Base64 encoding.
316    Base64,
317    /// Octal \NNN escapes.
318    Octal,
319    /// JavaScript `String.fromCodePoint()`.
320    JsCharCode,
321    /// JavaScript string concatenation.
322    JsConcat,
323    /// Alternating case.
324    CaseAlternate,
325    /// Tab-separated characters.
326    TabSplit,
327    /// Newline-separated characters.
328    NewlineSplit,
329    /// PHP `chr()` concatenation.
330    PhpChr,
331    /// Python `chr()` concatenation.
332    PythonChr,
333    /// SQL `CHAR()` function.
334    SqlChar,
335    /// CSS unicode escapes.
336    CssEscape,
337    /// ROT13 encoding.
338    Rot13,
339}
340
341impl std::fmt::Display for BuiltinEncoding {
342    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343        let value = match self {
344            Self::Identity => "identity",
345            Self::UrlEncode => "url_encode",
346            Self::DoubleUrl => "double_url",
347            Self::Hex => "hex",
348            Self::Unicode => "unicode",
349            Self::HtmlEntities => "html_entities",
350            Self::NullByte => "null_byte",
351            Self::Base64 => "base64",
352            Self::Octal => "octal",
353            Self::JsCharCode => "js_charcode",
354            Self::JsConcat => "js_concat",
355            Self::CaseAlternate => "case_alternate",
356            Self::TabSplit => "tab_split",
357            Self::NewlineSplit => "newline_split",
358            Self::PhpChr => "php_chr",
359            Self::PythonChr => "python_chr",
360            Self::SqlChar => "sql_char",
361            Self::CssEscape => "css_escape",
362            Self::Rot13 => "rot13",
363        };
364        f.write_str(value)
365    }
366}
367
368impl std::str::FromStr for BuiltinEncoding {
369    type Err = EncodingError;
370
371    /// Resolve an encoding name (canonical or alias) to its variant.
372    ///
373    /// This is the single owner of the encoding-name set: `apply_encoding`
374    /// dispatches through it, and [`BuiltinEncoding::ALL`] is validated
375    /// against it by the bidirectional completeness test.
376    fn from_str(name: &str) -> Result<Self, Self::Err> {
377        let variant = match name {
378            "identity" | "raw" => Self::Identity,
379            "url_encode" | "url" => Self::UrlEncode,
380            "double_url" => Self::DoubleUrl,
381            "hex" => Self::Hex,
382            "unicode" => Self::Unicode,
383            "html_entities" | "html" => Self::HtmlEntities,
384            "null_byte" => Self::NullByte,
385            "base64" => Self::Base64,
386            "octal" => Self::Octal,
387            "charcode" | "js_charcode" => Self::JsCharCode,
388            "concat_split" | "js_concat" => Self::JsConcat,
389            "case_alternate" => Self::CaseAlternate,
390            "tab_split" => Self::TabSplit,
391            "newline_split" => Self::NewlineSplit,
392            "php_chr" => Self::PhpChr,
393            "python_chr" => Self::PythonChr,
394            "sql_char" => Self::SqlChar,
395            "css_escape" => Self::CssEscape,
396            "rot13" => Self::Rot13,
397            other => {
398                return Err(EncodingError::UnknownTransform {
399                    transform: other.to_string(),
400                })
401            }
402        };
403        Ok(variant)
404    }
405}
406
407impl BuiltinEncoding {
408    /// Check whether `name` (canonical or alias) is a recognized builtin encoding.
409    pub fn is_builtin(name: &str) -> bool {
410        name.parse::<Self>().is_ok()
411    }
412
413    /// Apply this encoding to `s`.
414    fn apply(self, s: &str) -> String {
415        match self {
416            Self::Identity => s.to_string(),
417            Self::UrlEncode => urlencoding::encode(s).into_owned(),
418            Self::DoubleUrl => urlencoding::encode(&urlencoding::encode(s)).into_owned(),
419            Self::Hex => percent_hex_encode(s),
420            Self::Unicode => unicode_escape(s),
421            Self::HtmlEntities => html_encode(s),
422            Self::NullByte => format!("{s}%00"),
423            Self::Base64 => encodex::base64::encode(s.as_bytes()),
424            Self::Octal => octal_escape(s),
425            Self::JsCharCode => js_charcode(s),
426            Self::JsConcat => js_concat_split(s),
427            Self::CaseAlternate => alternate_case(s, 0),
428            Self::TabSplit => join_chars_with(s, "\t"),
429            Self::NewlineSplit => join_chars_with(s, "\n"),
430            Self::PhpChr => php_chr_concat(s),
431            Self::PythonChr => python_chr_join(s),
432            Self::SqlChar => sql_char_concat(s),
433            Self::CssEscape => css_escape(s),
434            Self::Rot13 => rot13_encode(s),
435        }
436    }
437
438    /// All builtin encoding names as strings.
439    pub const ALL: &'static [&'static str] = &[
440        "identity",
441        "raw",
442        "url_encode",
443        "url",
444        "double_url",
445        "hex",
446        "unicode",
447        "html_entities",
448        "html",
449        "null_byte",
450        "base64",
451        "octal",
452        "charcode",
453        "js_charcode",
454        "concat_split",
455        "js_concat",
456        "case_alternate",
457        "tab_split",
458        "newline_split",
459        "php_chr",
460        "python_chr",
461        "sql_char",
462        "css_escape",
463        "rot13",
464    ];
465}
466
467fn html_encode(s: &str) -> String {
468    let mut out = String::with_capacity(s.len() * 2);
469    for c in s.chars() {
470        match c {
471            '&' => out.push_str("&amp;"),
472            '<' => out.push_str("&lt;"),
473            '>' => out.push_str("&gt;"),
474            '"' => out.push_str("&quot;"),
475            '\'' => out.push_str("&#39;"),
476            '`' => out.push_str("&#96;"),
477            '/' => out.push_str("&#47;"),
478            _ => out.push(c),
479        }
480    }
481    out
482}