diff --git a/crates/aegis-core/src/security.rs b/crates/aegis-core/src/security.rs index 8db6791..10287d3 100644 --- a/crates/aegis-core/src/security.rs +++ b/crates/aegis-core/src/security.rs @@ -320,10 +320,21 @@ fn check_tls_off(name: &str, node: Node, src: &[u8], out: &mut Vec false)`. Both shapes + // have whitespace and either `,` or `=>` between key and value + // that normalize_kv doesn't unify, so check with raw text. + let lower = text.to_ascii_lowercase(); + let php_curl_disabled = ( + lower.contains("curlopt_ssl_verifypeer") + && (lower.contains(",false") || lower.contains(", false") + || lower.contains(",0)") || lower.contains(", 0)") + || lower.contains("=>false") || lower.contains("=> false") + || lower.contains("=>0") || lower.contains("=> 0")) + ) || ( + lower.contains("curlopt_ssl_verifyhost") + && (lower.contains(",0)") || lower.contains(", 0)") + || lower.contains("=>0") || lower.contains("=> 0")) + ); + if php_curl_disabled { + push(out, node, "SEC003", "block", + "TLS verification disabled (`CURLOPT_SSL_VERIFYPEER`/`VERIFYHOST` set to 0/false) — exposes traffic to MITM".into()); + } } /// Collapse whitespace around `=` and `:` and lowercase the whole @@ -455,8 +486,30 @@ fn text_has_interp(text: &str) -> bool { // ─── Rule SEC005: SQL string concat ────────────────────────────── fn check_sql_concat(name: &str, node: Node, src: &[u8], out: &mut Vec) { - let last = name.rsplit('.').next().unwrap_or(name); - if !matches!(last, "execute" | "executemany" | "query" | "executeQuery" | "executeUpdate") { + let last = leaf_method_name(name); + // Database-execution method names across Python / JDBC / Node / + // Go / PHP / Ruby. False positives only fire when the call's + // first arg also contains a SQL keyword in a string literal — + // contains_sql_in_string_literal does that gating below, so + // adding receiver names here is low-risk. + let is_db_call = matches!( + last, + // Python (cursor.execute / .executemany) + "execute" | "executemany" + // JDBC: PreparedStatement / Statement + | "executeQuery" | "executeUpdate" | "executeLargeUpdate" + | "executeBatch" + // Node.js / Go / generic: connection.query / db.Query / db.Exec + | "query" | "Query" | "QueryRow" | "QueryContext" + | "Exec" | "ExecContext" + // PHP PDO / mysqli + | "exec" | "prepare" + // ActiveRecord style + | "find_by_sql" + // PHP global functions + | "mysqli_query" | "mysql_query" | "pg_query" | "sqlite_query" + ); + if !is_db_call { return; } let Ok(text) = node.utf8_text(src) else { return }; @@ -470,12 +523,9 @@ fn check_sql_concat(name: &str, node: Node, src: &[u8], out: &mut Vec bool { let kind = node.kind(); - if matches!(kind, "string" | "string_literal" | "interpreted_string_literal" | "string_fragment") { + if matches!( + kind, + "string" | "string_literal" | "interpreted_string_literal" | "string_fragment" + // PHP: regular `"..."` strings parse as `encapsed_string` + | "encapsed_string" | "string_value" + // Heredoc / template literals + | "heredoc" | "template_string" + ) { if let Ok(text) = node.utf8_text(src) { let upper = text.to_ascii_uppercase(); return ["SELECT ", "INSERT ", "UPDATE ", "DELETE ", "DROP "] @@ -613,11 +670,26 @@ fn check_jwt_unsafe( // ─── Rule SEC008: insecure deserialization ─────────────────────── fn check_insecure_deserialization(name: &str, node: Node, src: &[u8], out: &mut Vec) { let dangerous = [ + // Python "pickle.loads", "pickle.load", "marshal.loads", "marshal.load", - "yaml.load", - "node-serialize.unserialize", + "yaml.load", // unsafe by default; SafeLoader is the safe alternative + "shelve.open", "dill.loads", "dill.load", + // Node.js: serialize-javascript / node-serialize family + "node-serialize.unserialize", "serialize.unserialize", + // Java: ObjectInputStream / XStream / SnakeYaml "ObjectInputStream", "readObject", + "XMLDecoder.readObject", + // PHP: native unserialize is the textbook unsafe path + "unserialize", + // Ruby: Marshal.load / YAML.load (default is unsafe Psych load) + "Marshal.load", "YAML.load", "Oj.load", + // C# / .NET: BinaryFormatter is deprecated for security reasons + "BinaryFormatter.Deserialize", + "SoapFormatter.Deserialize", + "NetDataContractSerializer.ReadObject", + // Go: gob.NewDecoder + Decode on attacker-controlled bytes + "gob.NewDecoder", ]; if !dangerous.iter().any(|n| name.contains(n)) { return; @@ -1557,6 +1629,99 @@ mod tests { } // ─── SEC004 multi-language (PR #18) ────────────────────────── + // ─── SEC003 multi-language extensions (PR #19) ─────────────── + #[test] + fn sec003_php_curl_verifypeer_zero_blocks() { + let v = check( + ".php", + " false));\n", + ); + assert!(v.iter().any(|v| v.rule_id == "SEC003"), "got {v:?}"); + } + + #[test] + fn sec003_php_curl_verifypeer_one_does_not_block() { + // 1 / true is the safe (default) value. + let v = check( + ".php", + "