//! Laravel security extractor. //! //! Detects security misconfigurations in Laravel applications: //! - APP_DEBUG enabled in production //! - Empty or weak APP_KEY //! - Mass assignment vulnerabilities //! - SQL injection via DB::raw //! - CSRF protection bypassed //! - Insecure session/cookie settings use regex::Regex; use stemedb_core::types::ObjectValue; use super::traits::build_claim; use super::Extractor; use crate::types::{ExtractedClaim, Language}; /// Extractor for Laravel security misconfigurations. #[allow(dead_code)] pub struct LaravelSecurityExtractor { // .env patterns app_debug_true: Regex, app_key_empty: Regex, session_secure_false: Regex, session_http_only_false: Regex, // PHP config patterns debug_hardcoded: Regex, key_hardcoded: Regex, cors_wildcard_credentials: Regex, // PHP code patterns csrf_except_all: Regex, csrf_except_api: Regex, mass_assignment_all: Regex, mass_assignment_fill: Regex, db_raw_interpolation: Regex, db_select_interpolation: Regex, eval_request: Regex, exec_request: Regex, shell_exec_request: Regex, } impl Default for LaravelSecurityExtractor { fn default() -> Self { Self::new() } } impl LaravelSecurityExtractor { /// Create a new Laravel security extractor. /// /// # Panics /// Panics if any regex pattern is invalid (programmer error). #[allow(clippy::expect_used)] pub fn new() -> Self { Self { // .env patterns app_debug_true: Regex::new(r"(?i)^APP_DEBUG\s*=\s*true").expect("valid regex"), app_key_empty: Regex::new(r"(?i)^APP_KEY\s*=\s*$").expect("valid regex"), session_secure_false: Regex::new(r"(?i)^SESSION_SECURE_COOKIE\s*=\s*false") .expect("valid regex"), session_http_only_false: Regex::new(r"(?i)^SESSION_HTTP_ONLY\s*=\s*false") .expect("valid regex"), // PHP config patterns debug_hardcoded: Regex::new(r#"['"]debug['"]\s*=>\s*true"#).expect("valid regex"), key_hardcoded: Regex::new(r#"['"]key['"]\s*=>\s*['"][^'"]{1,50}['"]"#) .expect("valid regex"), cors_wildcard_credentials: Regex::new( r#"['"]allowed_origins['"]\s*=>\s*\[\s*['"]?\*['"]?\s*\][^]]*['"]supports_credentials['"]\s*=>\s*true"#, ) .expect("valid regex"), // PHP code patterns csrf_except_all: Regex::new(r#"protected\s+\$except\s*=\s*\[\s*['"]?\*['"]?\s*\]"#) .expect("valid regex"), csrf_except_api: Regex::new(r#"\$except\s*=\s*\[[^\]]*['"]api/\*['"]"#) .expect("valid regex"), mass_assignment_all: Regex::new(r"::\s*create\s*\(\s*\$request->all\s*\(\s*\)\s*\)") .expect("valid regex"), mass_assignment_fill: Regex::new(r"->fill\s*\(\s*\$request->all\s*\(\s*\)\s*\)") .expect("valid regex"), db_raw_interpolation: Regex::new(r#"DB::raw\s*\([^)]*\.\s*\$"#) .expect("valid regex"), db_select_interpolation: Regex::new(r#"DB::select\s*\(\s*['"][^'"]*\{\$"#) .expect("valid regex"), eval_request: Regex::new(r"eval\s*\(\s*\$request").expect("valid regex"), exec_request: Regex::new(r"exec\s*\(\s*\$request").expect("valid regex"), shell_exec_request: Regex::new(r"shell_exec\s*\(\s*\$request").expect("valid regex"), } } fn check_env_patterns( &self, path_segments: &[String], content: &str, file: &str, ) -> Vec { let mut claims = Vec::new(); for (line_idx, line) in content.lines().enumerate() { let line_num = line_idx + 1; // APP_DEBUG=true if let Some(m) = self.app_debug_true.find(line) { claims.push(build_claim( path_segments, &["laravel", "debug_mode"], "enabled", ObjectValue::Boolean(true), file, line_num, m.as_str(), 1.0, "Laravel APP_DEBUG enabled - must be false in production", )); } // APP_KEY empty if let Some(m) = self.app_key_empty.find(line) { claims.push(build_claim( path_segments, &["laravel", "app_key"], "missing", ObjectValue::Boolean(true), file, line_num, m.as_str(), 1.0, "Laravel APP_KEY is empty - encryption will fail", )); } // SESSION_SECURE_COOKIE=false if let Some(m) = self.session_secure_false.find(line) { claims.push(build_claim( path_segments, &["laravel", "session_cookie", "secure"], "enabled", ObjectValue::Boolean(false), file, line_num, m.as_str(), 1.0, "Laravel session cookie not marked secure", )); } // SESSION_HTTP_ONLY=false if let Some(m) = self.session_http_only_false.find(line) { claims.push(build_claim( path_segments, &["laravel", "session_cookie", "httponly"], "enabled", ObjectValue::Boolean(false), file, line_num, m.as_str(), 1.0, "Laravel session cookie accessible to JavaScript", )); } } claims } fn check_php_patterns( &self, path_segments: &[String], content: &str, file: &str, ) -> Vec { let mut claims = Vec::new(); for (line_idx, line) in content.lines().enumerate() { let line_num = line_idx + 1; // Debug hardcoded if let Some(m) = self.debug_hardcoded.find(line) { // Skip if using env() if !line.contains("env(") { claims.push(build_claim( path_segments, &["laravel", "debug_mode"], "hardcoded", ObjectValue::Boolean(true), file, line_num, m.as_str(), 0.9, "Laravel debug mode hardcoded to true", )); } } // CSRF except all if let Some(m) = self.csrf_except_all.find(line) { claims.push(build_claim( path_segments, &["laravel", "csrf"], "exempt", ObjectValue::Boolean(true), file, line_num, m.as_str(), 1.0, "Laravel CSRF protection disabled for all routes", )); } // CSRF except API if let Some(m) = self.csrf_except_api.find(line) { claims.push(build_claim( path_segments, &["laravel", "csrf", "api_exempt"], "vulnerable", ObjectValue::Boolean(true), file, line_num, m.as_str(), 0.7, "Laravel CSRF protection disabled for API routes", )); } // Mass assignment via create() if let Some(m) = self.mass_assignment_all.find(line) { claims.push(build_claim( path_segments, &["laravel", "mass_assignment"], "vulnerable", ObjectValue::Boolean(true), file, line_num, m.as_str(), 0.95, "Laravel mass assignment via ::create($request->all())", )); } // Mass assignment via fill() if let Some(m) = self.mass_assignment_fill.find(line) { claims.push(build_claim( path_segments, &["laravel", "mass_assignment"], "vulnerable", ObjectValue::Boolean(true), file, line_num, m.as_str(), 0.95, "Laravel mass assignment via ->fill($request->all())", )); } // DB::raw interpolation if let Some(m) = self.db_raw_interpolation.find(line) { claims.push(build_claim( path_segments, &["laravel", "sql_injection"], "vulnerable", ObjectValue::Boolean(true), file, line_num, m.as_str(), 0.95, "Laravel SQL injection via DB::raw() with interpolation", )); } // DB::select interpolation if let Some(m) = self.db_select_interpolation.find(line) { claims.push(build_claim( path_segments, &["laravel", "sql_injection"], "vulnerable", ObjectValue::Boolean(true), file, line_num, m.as_str(), 0.95, "Laravel SQL injection via DB::select() with interpolation", )); } // Command injection if let Some(m) = self.eval_request.find(line) { claims.push(build_claim( path_segments, &["laravel", "code_injection"], "vulnerable", ObjectValue::Boolean(true), file, line_num, m.as_str(), 1.0, "Laravel code injection via eval() with request data", )); } if let Some(m) = self.exec_request.find(line) { claims.push(build_claim( path_segments, &["laravel", "command_injection"], "vulnerable", ObjectValue::Boolean(true), file, line_num, m.as_str(), 1.0, "Laravel command injection via exec() with request data", )); } if let Some(m) = self.shell_exec_request.find(line) { claims.push(build_claim( path_segments, &["laravel", "command_injection"], "vulnerable", ObjectValue::Boolean(true), file, line_num, m.as_str(), 1.0, "Laravel command injection via shell_exec() with request data", )); } } claims } } impl Extractor for LaravelSecurityExtractor { fn name(&self) -> &str { "laravel_security" } fn languages(&self) -> &[Language] { &[Language::Php, Language::Dotenv] } fn extract( &self, path_segments: &[String], content: &str, language: Language, file: &str, ) -> Vec { let mut claims = Vec::new(); // Check if this looks like a Laravel file let is_laravel = content.contains("Laravel") || content.contains("laravel") || content.contains("Illuminate") || content.contains("APP_KEY") || content.contains("APP_DEBUG") || file.contains("artisan") || file.contains("app/Http"); if !is_laravel { return claims; } match language { Language::Dotenv => { claims.extend(self.check_env_patterns(path_segments, content, file)); } Language::Php => { claims.extend(self.check_php_patterns(path_segments, content, file)); } _ => {} } claims } } #[cfg(test)] mod tests { use super::*; #[test] fn test_app_debug_true() { let extractor = LaravelSecurityExtractor::new(); let content = r#" APP_NAME=Laravel APP_ENV=production APP_KEY=base64:abcdef... APP_DEBUG=true "#; let claims = extractor.extract(&["env".to_string()], content, Language::Dotenv, ".env"); assert!(claims.iter().any(|c| c.concept_path.contains("debug_mode"))); } #[test] fn test_app_key_empty() { let extractor = LaravelSecurityExtractor::new(); let content = r#" APP_NAME=Laravel APP_KEY= APP_DEBUG=false "#; let claims = extractor.extract(&["env".to_string()], content, Language::Dotenv, ".env"); assert!(claims.iter().any(|c| c.concept_path.contains("app_key"))); } #[test] fn test_mass_assignment() { let extractor = LaravelSecurityExtractor::new(); let content = r#" all()); } } "#; let claims = extractor.extract(&["php".to_string()], content, Language::Php, "UserController.php"); assert!(claims.iter().any(|c| c.concept_path.contains("mass_assignment"))); } #[test] fn test_csrf_exempt_all() { let extractor = LaravelSecurityExtractor::new(); let content = r#" name . "'"); } } "#; let claims = extractor.extract(&["php".to_string()], content, Language::Php, "SearchController.php"); assert!(claims.iter().any(|c| c.concept_path.contains("sql_injection"))); } #[test] fn test_non_laravel_file_skipped() { let extractor = LaravelSecurityExtractor::new(); let content = r#"