Skip to content

Commit a134495

Browse files
bumahkib7claude
andcommitted
feat: Add advanced security rules with taint tracking and type inference
New security rules using dataflow infrastructure: - ResourceLeakRule: CFG path analysis for unclosed files/connections - SqlInjectionTaintRule: Taint flow from user input to SQL sinks - PathTraversalTaintRule: Taint flow to file operations - CommandInjectionTaintRule: Taint flow to shell execution - XssDetectionRule: Taint flow to DOM sinks with XSS type classification - SsrfTaintRule: Taint flow to HTTP clients with private IP detection - NullPointerRule: Reaching definitions for nullable origins Infrastructure enhancements: - String concatenation taint tracking (+, template literals, .concat()) - Type inference layer (InferredType, Nullability) - Cross-file taint via CallGraph integration - TaintSummary for inter-procedural analysis New ValueOrigin variants: - StringConcat(Vec<String>) for binary + operations - TemplateLiteral(Vec<String>) for template strings - MethodCall for .concat(), .join(), .format() Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 746201f commit a134495

File tree

11 files changed

+8918
-35
lines changed

11 files changed

+8918
-35
lines changed

crates/analyzer/src/flow/interprocedural.rs

Lines changed: 752 additions & 2 deletions
Large diffs are not rendered by default.

crates/analyzer/src/flow/mod.rs

Lines changed: 314 additions & 1 deletion
Large diffs are not rendered by default.

crates/analyzer/src/flow/sources.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,101 @@ impl TaintConfig {
223223
})
224224
}
225225

226+
/// Check if a function call is an SQL sink
227+
pub fn is_sql_sink(&self, func_name: &str) -> bool {
228+
self.sinks.iter().any(|s| match &s.pattern {
229+
SinkPattern::FunctionCall(pattern) => {
230+
s.rule_id.contains("sql-injection")
231+
&& (func_name == pattern
232+
|| func_name.ends_with(&format!(".{}", pattern))
233+
|| func_name.contains(pattern))
234+
}
235+
_ => false,
236+
})
237+
}
238+
239+
/// Get all SQL sink patterns for matching
240+
pub fn sql_sink_patterns(&self) -> Vec<&str> {
241+
self.sinks
242+
.iter()
243+
.filter(|s| s.rule_id.contains("sql-injection"))
244+
.filter_map(|s| match &s.pattern {
245+
SinkPattern::FunctionCall(pattern) => Some(pattern.as_str()),
246+
_ => None,
247+
})
248+
.collect()
249+
}
250+
251+
/// Check if a query string uses parameterized placeholders (sanitized for SQL)
252+
///
253+
/// Recognizes common placeholder patterns:
254+
/// - ? (JDBC, MySQL, SQLite)
255+
/// - $1, $2, ... (PostgreSQL)
256+
/// - :name, :param (Named parameters - Python, SQLAlchemy, Oracle)
257+
/// - @param (SQL Server, ADO.NET)
258+
/// - %s (Python DB-API with params tuple)
259+
pub fn is_parameterized_query(query: &str) -> bool {
260+
// Check for common parameterized query patterns
261+
// These indicate safe usage with bound parameters
262+
263+
// ? placeholders (JDBC, MySQL, SQLite)
264+
if query.contains('?') {
265+
return true;
266+
}
267+
268+
// $1, $2, ... PostgreSQL positional placeholders
269+
if query.contains("$1") || query.contains("$2") {
270+
return true;
271+
}
272+
273+
// :name or :param named placeholders (SQLAlchemy, Oracle)
274+
// Match :word patterns that look like bind variables
275+
let has_named_param = query.chars().enumerate().any(|(i, c)| {
276+
if c == ':' && i + 1 < query.len() {
277+
let next_char = query.chars().nth(i + 1).unwrap_or(' ');
278+
// Check it's :name not ::type_cast
279+
next_char.is_alphabetic() && (i == 0 || query.chars().nth(i - 1) != Some(':'))
280+
} else {
281+
false
282+
}
283+
});
284+
if has_named_param {
285+
return true;
286+
}
287+
288+
// @param (SQL Server, ADO.NET)
289+
if query.contains('@') {
290+
let has_at_param = query.chars().enumerate().any(|(i, c)| {
291+
if c == '@' && i + 1 < query.len() {
292+
let next_char = query.chars().nth(i + 1).unwrap_or(' ');
293+
next_char.is_alphabetic()
294+
} else {
295+
false
296+
}
297+
});
298+
if has_at_param {
299+
return true;
300+
}
301+
}
302+
303+
false
304+
}
305+
306+
/// Check if a function call looks like a safe prepared statement usage
307+
pub fn is_prepared_statement_call(func_name: &str) -> bool {
308+
let safe_patterns = [
309+
"prepare",
310+
"prepareStatement",
311+
"preparedStatement",
312+
"PreparedStatement",
313+
"createStatement",
314+
"NamedParameterJdbcTemplate",
315+
"SqlParameterSource",
316+
"text", // Prisma/Knex text() for parameterized queries
317+
];
318+
safe_patterns.iter().any(|p| func_name.contains(p))
319+
}
320+
226321
fn javascript() -> Self {
227322
Self {
228323
sources: vec![
@@ -382,6 +477,43 @@ impl TaintConfig {
382477
rule_id: "js/command-injection".into(),
383478
pattern: SinkPattern::FunctionCall("spawn".into()),
384479
},
480+
// SQL injection sinks for JavaScript
481+
TaintSink {
482+
rule_id: "js/sql-injection".into(),
483+
pattern: SinkPattern::FunctionCall("query".into()),
484+
},
485+
TaintSink {
486+
rule_id: "js/sql-injection".into(),
487+
pattern: SinkPattern::FunctionCall("execute".into()),
488+
},
489+
TaintSink {
490+
rule_id: "js/sql-injection".into(),
491+
pattern: SinkPattern::FunctionCall("db.query".into()),
492+
},
493+
TaintSink {
494+
rule_id: "js/sql-injection".into(),
495+
pattern: SinkPattern::FunctionCall("mysql.query".into()),
496+
},
497+
TaintSink {
498+
rule_id: "js/sql-injection".into(),
499+
pattern: SinkPattern::FunctionCall("pg.query".into()),
500+
},
501+
TaintSink {
502+
rule_id: "js/sql-injection".into(),
503+
pattern: SinkPattern::FunctionCall("connection.query".into()),
504+
},
505+
TaintSink {
506+
rule_id: "js/sql-injection".into(),
507+
pattern: SinkPattern::FunctionCall("pool.query".into()),
508+
},
509+
TaintSink {
510+
rule_id: "js/sql-injection".into(),
511+
pattern: SinkPattern::FunctionCall("$queryRaw".into()),
512+
},
513+
TaintSink {
514+
rule_id: "js/sql-injection".into(),
515+
pattern: SinkPattern::FunctionCall("$executeRaw".into()),
516+
},
385517
],
386518
source_function_cache: Vec::new(),
387519
source_member_cache: Vec::new(),
@@ -478,6 +610,26 @@ impl TaintConfig {
478610
rule_id: "go/sql-injection".into(),
479611
pattern: SinkPattern::FunctionCall("db.Exec".into()),
480612
},
613+
TaintSink {
614+
rule_id: "go/sql-injection".into(),
615+
pattern: SinkPattern::FunctionCall("db.QueryRow".into()),
616+
},
617+
TaintSink {
618+
rule_id: "go/sql-injection".into(),
619+
pattern: SinkPattern::FunctionCall("db.QueryContext".into()),
620+
},
621+
TaintSink {
622+
rule_id: "go/sql-injection".into(),
623+
pattern: SinkPattern::FunctionCall("db.ExecContext".into()),
624+
},
625+
TaintSink {
626+
rule_id: "go/sql-injection".into(),
627+
pattern: SinkPattern::FunctionCall("tx.Query".into()),
628+
},
629+
TaintSink {
630+
rule_id: "go/sql-injection".into(),
631+
pattern: SinkPattern::FunctionCall("tx.Exec".into()),
632+
},
481633
TaintSink {
482634
rule_id: "go/command-injection".into(),
483635
pattern: SinkPattern::FunctionCall("exec.Command".into()),
@@ -560,10 +712,35 @@ impl TaintConfig {
560712
rule_id: "python/command-injection".into(),
561713
pattern: SinkPattern::FunctionCall("subprocess.run".into()),
562714
},
715+
// SQL injection sinks for Python
563716
TaintSink {
564717
rule_id: "python/sql-injection".into(),
565718
pattern: SinkPattern::FunctionCall("cursor.execute".into()),
566719
},
720+
TaintSink {
721+
rule_id: "python/sql-injection".into(),
722+
pattern: SinkPattern::FunctionCall("cursor.executemany".into()),
723+
},
724+
TaintSink {
725+
rule_id: "python/sql-injection".into(),
726+
pattern: SinkPattern::FunctionCall("db.execute".into()),
727+
},
728+
TaintSink {
729+
rule_id: "python/sql-injection".into(),
730+
pattern: SinkPattern::FunctionCall("connection.execute".into()),
731+
},
732+
TaintSink {
733+
rule_id: "python/sql-injection".into(),
734+
pattern: SinkPattern::FunctionCall("engine.execute".into()),
735+
},
736+
TaintSink {
737+
rule_id: "python/sql-injection".into(),
738+
pattern: SinkPattern::FunctionCall("session.execute".into()),
739+
},
740+
TaintSink {
741+
rule_id: "python/sql-injection".into(),
742+
pattern: SinkPattern::FunctionCall("raw".into()),
743+
},
567744
],
568745
source_function_cache: Vec::new(),
569746
source_member_cache: Vec::new(),

0 commit comments

Comments
 (0)