@@ -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