agentsec-core 0.3.0

AgentSec core library — scan / web / paste logic, pure Rust
Documentation
//! Obfuscation-aware decode chain for paste detection.
//!
//! Attackers wrap injection payloads in percent / HTML / base64 / Unicode
//! disguise so a naive substring scan misses them. [`decode_chain`] applies
//! all four layers in sequence so the downstream detector
//! ([`crate::paste::detector::scan`]) sees the normalized form.

use unicode_normalization::UnicodeNormalization;

/// Apply the full decode chain: percent → HTML → base64 → Unicode NFKC.
///
/// Each layer is best-effort:
///
/// - percent and HTML decoding pass non-matching bytes through unchanged
/// - base64 decoding requires the *entire* input to be a valid base64
///   token; otherwise the layer returns the input unchanged (no partial
///   decode)
/// - NFKC always succeeds
///
/// # Examples
///
/// ```
/// use agentsec_core::paste::obfuscation::decode_chain;
///
/// // Plain text passes through.
/// assert_eq!(decode_chain("hello"), "hello");
///
/// // Percent-encoded text is decoded.
/// assert_eq!(decode_chain("hello%20world"), "hello world");
/// ```
pub fn decode_chain(input: &str) -> String {
    let s1 = decode_percent(input);
    let s2 = decode_html(&s1);
    let s3 = decode_base64(&s2);
    normalize_unicode(&s3)
}

fn decode_percent(input: &str) -> String {
    percent_encoding::percent_decode_str(input)
        .decode_utf8_lossy()
        .into_owned()
}

fn decode_html(input: &str) -> String {
    html_escape::decode_html_entities(input).into_owned()
}

fn decode_base64(input: &str) -> String {
    // Best-effort: only decode if the whole token looks base64-like.
    match data_encoding::BASE64.decode(input.as_bytes()) {
        Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
        Err(_) => input.to_string(),
    }
}

fn normalize_unicode(input: &str) -> String {
    input.nfkc().collect()
}