stemedb/applications/aphoria/src/extractors/jwt_config.rs
jordan b3e8a9a058 feat: Multi-application expansion with chaos testing and community UI
Major additions:
- Community Next.js app (port 18187) for browsing claims with API docs
- stemedb-chaos crate: Fault injection, chaos testing, CRDT properties
- Latent ingestion system: Reddit/FDA ingesters with ADK-Go agents
- Disputed claims handling: Manual review workflows and validation
- Aphoria security scanner: New extractors (SQL injection, command
  injection, weak crypto, TLS version), policy-based ignores, UAT reports
- Docker infrastructure: Dockerfile, docker-compose.yml for full stack
- VulnBank demo: Intentionally vulnerable multi-language test corpus

SDK & API enhancements:
- Source registry handlers for tracking data provenance
- Metrics endpoint
- Skeptic filtering improvements

Code quality:
- Split 14 large files (>500 lines) into focused modules
- All files now under 500-line limit per project guidelines

Documentation:
- Chaos testing guide, circuit breakers, observability docs
- Phase 7 UAT documentation updates
- Martin Kleppmann technical writer agent

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 01:24:14 -07:00

401 lines
13 KiB
Rust

//! JWT configuration extractor.
//!
//! Detects patterns where JWT validation is misconfigured,
//! violating RFC 7519 security requirements.
use regex::Regex;
use stemedb_core::types::ObjectValue;
use super::Extractor;
use crate::types::{ExtractedClaim, Language};
/// Extractor for JWT validation configuration.
pub struct JwtConfigExtractor {
/// Audience validation disabled
aud_disabled: Regex,
/// Algorithm none allowed
alg_none: Regex,
/// Signature verification skipped
sig_skip: Regex,
/// Expiry validation disabled
exp_disabled: Regex,
/// Go jwt.Parse without algorithm check (heuristic)
go_parse_insecure: Regex,
}
impl Default for JwtConfigExtractor {
fn default() -> Self {
Self::new()
}
}
impl JwtConfigExtractor {
/// Create a new JWT config extractor with compiled regexes.
///
/// # Panics
/// Panics if any regex pattern is invalid (programmer error).
#[allow(clippy::expect_used)]
pub fn new() -> Self {
Self {
// Match JWT audience validation disabled patterns:
// - set_audience([]) or set_audience(vec![]) - empty audience list
// - validate_aud = false or validate_audience = false
// - aud = None or audience = None (direct assignment)
// - ValidateAudience = false (.NET style)
// NOTE: \baud\b ensures we don't match "audit" in "audit_log_path"
aud_disabled: Regex::new(
r"(?i)(set_audience\s*\(\s*(?:vec!)?\s*\[\s*\]\s*\)|validate_aud\w*\s*[:=]\s*false|\baud(?:ience)?\s*[:=]\s*None|ValidateAudience\s*[:=]\s*false)",
)
.expect("valid regex"),
// Match JWT algorithm none patterns (signature bypass vulnerability):
// - Algorithm::None (Rust enum variant)
// - alg: none, alg = none, alg: "none" (config assignment)
// - allow_none = true or allow_none_algorithm = true
// - SigningMethodNone (Go jwt library)
// - YAML list item: "- none" or '- "none"' in algorithms list
// - WithValidMethods([]string{"none"...}) in Go
// NOTE: Post-processing filters out documentation like "(no alg: none)"
alg_none: Regex::new(
r#"(?i)(Algorithm::None|\balg(?:orithm)?s?\b\s*[:=]\s*['"]?none|allow_none\w*\s*[:=]\s*true|SigningMethodNone|-\s*['"]?none['"]?\s*(?:#|$)|WithValidMethods.*none)"#,
)
.expect("valid regex"),
// Match signature verification disabled patterns:
// - dangerous_insecure_* functions
// - skip_signature settings
// - verify_signature = false, signature_verify = false
// - RequireSignedTokens = false
// NOTE: Excludes patterns like "skip_verify = false" (that's secure!)
// and UI state like "isVerifying = false"
sig_skip: Regex::new(
r"(?i)(dangerous_insecure|skip_signature|(?:verify_signature|signature_verify)\s*[:=]\s*false|RequireSignedTokens\s*[:=]\s*false)",
)
.expect("valid regex"),
exp_disabled: Regex::new(
r"(?i)(validate_exp.*false|RequireExpirationTime.*false|IgnoreExpiration)",
)
.expect("valid regex"),
go_parse_insecure: Regex::new(
r"jwt\.Parse\([^,]+,\s*func\s*\([^)]*\*jwt\.Token\)\s*\([^)]*,\s*error\)\s*\{[^}]*return\s+[^,]+,\s*nil",
)
.expect("valid regex"),
}
}
#[allow(clippy::too_many_arguments)]
fn extract_claim(
&self,
path_segments: &[String],
file: &str,
line: usize,
matched_text: &str,
leaf: &str,
predicate: &str,
value: ObjectValue,
description: &str,
confidence: f32,
) -> ExtractedClaim {
let mut concept_path = path_segments.to_vec();
concept_path.push("jwt".to_string());
concept_path.push(leaf.to_string());
ExtractedClaim {
concept_path: format!("code://{}", concept_path.join("/")),
predicate: predicate.to_string(),
value,
file: file.to_string(),
line,
matched_text: matched_text.to_string(),
confidence,
description: description.to_string(),
}
}
}
impl Extractor for JwtConfigExtractor {
fn name(&self) -> &str {
"jwt_config"
}
fn languages(&self) -> &[Language] {
&[
Language::Rust,
Language::Go,
Language::Python,
Language::TypeScript,
Language::JavaScript,
Language::Yaml,
Language::Toml,
Language::Json,
]
}
fn extract(
&self,
path_segments: &[String],
content: &str,
_language: Language,
file: &str,
) -> Vec<ExtractedClaim> {
let mut claims = Vec::new();
for (line_idx, line) in content.lines().enumerate() {
let line_num = line_idx + 1;
// Audience validation disabled
if let Some(matched) = self.aud_disabled.find(line) {
claims.push(self.extract_claim(
path_segments,
file,
line_num,
matched.as_str(),
"audience_validation",
"enabled",
ObjectValue::Boolean(false),
"JWT audience validation is disabled",
1.0,
));
}
// Algorithm none allowed
if let Some(matched) = self.alg_none.find(line) {
// Filter out documentation that describes PREVENTING alg:none
// e.g., "(no alg: none)" or "don't allow alg none"
let lower_line = line.to_lowercase();
let is_prevention_doc = lower_line.contains("no alg")
|| lower_line.contains("no `alg")
|| lower_line.contains("don't allow")
|| lower_line.contains("do not allow")
|| lower_line.contains("whitelist")
|| lower_line.contains("reject")
|| (lower_line.contains("algorithm") && lower_line.contains("none"))
&& (lower_line.contains("checksum") || lower_line.contains("crc"));
if is_prevention_doc {
continue;
}
claims.push(self.extract_claim(
path_segments,
file,
line_num,
matched.as_str(),
"algorithm_restriction",
"config_value",
ObjectValue::Text("none_allowed".to_string()),
"JWT allows 'none' algorithm (signature bypass)",
1.0,
));
}
// Signature verification skipped
if let Some(matched) = self.sig_skip.find(line) {
claims.push(self.extract_claim(
path_segments,
file,
line_num,
matched.as_str(),
"signature_verification",
"enabled",
ObjectValue::Boolean(false),
"JWT signature verification is disabled",
1.0,
));
}
// Expiry validation disabled
if let Some(matched) = self.exp_disabled.find(line) {
claims.push(self.extract_claim(
path_segments,
file,
line_num,
matched.as_str(),
"expiry_validation",
"enabled",
ObjectValue::Boolean(false),
"JWT expiry validation is disabled",
1.0,
));
}
}
// Check for Go insecure parse pattern (multi-line, lower confidence)
if let Some(matched) = self.go_parse_insecure.find(content) {
// Find line number for start of match
let line_num = content[..matched.start()].lines().count() + 1;
claims.push(self.extract_claim(
path_segments,
file,
line_num,
&matched.as_str()[..matched.as_str().len().min(50)],
"signature_verification",
"enabled",
ObjectValue::Boolean(false),
"JWT parsed without algorithm verification (heuristic)",
0.7, // Lower confidence - heuristic match
));
}
claims
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audience_disabled() {
let extractor = JwtConfigExtractor::new();
let content = r#"
let validation = Validation::new(Algorithm::HS256);
validation.validate_aud = false;
"#;
let claims =
extractor.extract(&["rust".to_string()], content, Language::Rust, "src/auth.rs");
assert_eq!(claims.len(), 1);
assert!(claims[0].concept_path.contains("audience_validation"));
assert_eq!(claims[0].value, ObjectValue::Boolean(false));
}
#[test]
fn test_algorithm_none() {
let extractor = JwtConfigExtractor::new();
let content = r#"
// Dangerous: allows unsigned tokens
Algorithm::None
"#;
let claims =
extractor.extract(&["rust".to_string()], content, Language::Rust, "src/auth.rs");
assert_eq!(claims.len(), 1);
assert!(claims[0].concept_path.contains("algorithm_restriction"));
}
#[test]
fn test_signature_skip() {
let extractor = JwtConfigExtractor::new();
let content = r#"
let claims = dangerous_insecure_decode(&token)?;
"#;
let claims =
extractor.extract(&["rust".to_string()], content, Language::Rust, "src/auth.rs");
assert_eq!(claims.len(), 1);
assert!(claims[0].concept_path.contains("signature_verification"));
}
#[test]
fn test_multiple_issues() {
let extractor = JwtConfigExtractor::new();
let content = r#"
validation.validate_aud = false;
validation.validate_exp = false;
"#;
let claims =
extractor.extract(&["rust".to_string()], content, Language::Rust, "src/auth.rs");
assert_eq!(claims.len(), 2);
}
// Regression tests for false positives
#[test]
fn test_skip_verify_false_not_flagged() {
// "insecure_skip_verify = false" means verification IS enabled (secure)
let extractor = JwtConfigExtractor::new();
let content = r#"
insecure_skip_verify = false
"#;
let claims =
extractor.extract(&["config".to_string()], content, Language::Toml, "config.toml");
assert!(claims.is_empty(), "skip_verify = false should NOT be flagged (it's secure)");
}
#[test]
fn test_ui_state_not_flagged() {
// React state "isVerifying = false" is UI state, not security
let extractor = JwtConfigExtractor::new();
let content = r#"
const [isVerifying, setIsVerifying] = useState(false);
setIsVerifying(false);
"#;
let claims = extractor.extract(
&["typescript".to_string()],
content,
Language::TypeScript,
"page.tsx",
);
assert!(claims.is_empty(), "UI state isVerifying should NOT be flagged");
}
#[test]
fn test_verify_release_not_flagged() {
// verify_release(version, false) is a release verification function, not JWT
let extractor = JwtConfigExtractor::new();
let content = r#"
verify_release(version, false);
"#;
let claims =
extractor.extract(&["rust".to_string()], content, Language::Rust, "release.rs");
assert!(claims.is_empty(), "verify_release function should NOT be flagged as JWT");
}
#[test]
fn test_actual_signature_verify_disabled() {
// verify_signature = false should be flagged
let extractor = JwtConfigExtractor::new();
let content = r#"
verify_signature = false
"#;
let claims =
extractor.extract(&["config".to_string()], content, Language::Toml, "jwt.toml");
assert_eq!(claims.len(), 1, "verify_signature = false should be flagged");
assert!(claims[0].concept_path.contains("signature_verification"));
}
#[test]
fn test_yaml_algorithms_list_none() {
let extractor = JwtConfigExtractor::new();
let content = r#"
jwt:
algorithms:
- HS256
- none
"#;
let claims = extractor.extract(
&["config".to_string()],
content,
Language::Yaml,
"config/production.yaml",
);
assert!(claims.iter().any(|c| c.concept_path.contains("algorithm_restriction")));
}
#[test]
fn test_go_with_valid_methods_none() {
let extractor = JwtConfigExtractor::new();
let content = r#"
parser := jwt.NewParser(jwt.WithValidMethods([]string{"none", "HS256"}))
"#;
let claims = extractor.extract(&["go".to_string()], content, Language::Go, "auth.go");
assert!(claims.iter().any(|c| c.concept_path.contains("algorithm_restriction")));
}
}