diff --git a/rules/python/sql_injection.ron b/rules/python/sql_injection.ron index 63e4574..7ccb79f 100644 --- a/rules/python/sql_injection.ron +++ b/rules/python/sql_injection.ron @@ -10,7 +10,8 @@ mode: "search", patterns: Some([ "execute_raw_query(", - "*cursor.execute(*%*", + // Match the % operator after a complete query expression, not DB-API placeholders. + "regex:(?s)cursor\\.execute\\(\\s*\\(*\\s*(?:(?:[rRuUbBfF]{0,2})?(?:\"\"\".*?\"\"\"|'''.*?'''|\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*')|[A-Za-z_][A-Za-z0-9_.]*)\\s*\\)*\\s*%\\s*[^\\s,)]", "*cursor.execute(*.format(*", "*cursor.execute(f\"*", "*cursor.execute(f'*", diff --git a/tests/features/false_positive_floor.feature b/tests/features/false_positive_floor.feature index d37e80d..64ed3e1 100644 --- a/tests/features/false_positive_floor.feature +++ b/tests/features/false_positive_floor.feature @@ -7,3 +7,8 @@ Feature: False-positive floor Given a staged benign Python module "calculator.py" When I scan the staging directory as "python" with the production rules Then no findings should be reported + + Scenario: Parameterized cursor execution produces zero findings + Given a staged copy of the fixture "tests/test_files/python/sql_parameterized_execute.py" as "parameterized_query.py" + When I scan the staging directory as "python" with the production rules + Then no findings should be reported diff --git a/tests/features/python_injection.feature b/tests/features/python_injection.feature index 189cba5..d205e6c 100644 --- a/tests/features/python_injection.feature +++ b/tests/features/python_injection.feature @@ -8,3 +8,17 @@ Feature: Python injection detection When I scan the staging directory as "python" with the production rules Then the findings should include a "Command Injection" finding in "mixed_case.py" And the findings should include a "SQL Injection" finding in "mixed_case.py" + + Scenario Outline: Dynamically formatted cursor execution is detected + Given a staged copy of the fixture "" as "unsafe_query.py" + When I scan the staging directory as "python" with the production rules + Then the findings should include a "SQL Injection" finding in "unsafe_query.py" + + Examples: + | fixture | + | tests/test_files/python/sql_percent_spaced.py | + | tests/test_files/python/sql_percent_unspaced.py | + | tests/test_files/python/sql_percent_variable.py | + | tests/test_files/python/sql_fstring_interpolation.py | + | tests/test_files/python/sql_concatenation.py | + | tests/test_files/python/sql_format_interpolation.py | diff --git a/tests/test_files/python/sql_concatenation.py b/tests/test_files/python/sql_concatenation.py new file mode 100644 index 0000000..9d22c02 --- /dev/null +++ b/tests/test_files/python/sql_concatenation.py @@ -0,0 +1,2 @@ +def load_user(cursor, user_id): + cursor.execute("SELECT * FROM users WHERE id = " + user_id) diff --git a/tests/test_files/python/sql_format_interpolation.py b/tests/test_files/python/sql_format_interpolation.py new file mode 100644 index 0000000..b08c99b --- /dev/null +++ b/tests/test_files/python/sql_format_interpolation.py @@ -0,0 +1,2 @@ +def load_user(cursor, user_id): + cursor.execute("SELECT * FROM users WHERE id = {}".format(user_id)) diff --git a/tests/test_files/python/sql_fstring_interpolation.py b/tests/test_files/python/sql_fstring_interpolation.py new file mode 100644 index 0000000..8cc5c64 --- /dev/null +++ b/tests/test_files/python/sql_fstring_interpolation.py @@ -0,0 +1,2 @@ +def load_user(cursor, user_id): + cursor.execute(f"SELECT * FROM users WHERE id = {user_id}") diff --git a/tests/test_files/python/sql_parameterized_execute.py b/tests/test_files/python/sql_parameterized_execute.py new file mode 100644 index 0000000..bdc8dc6 --- /dev/null +++ b/tests/test_files/python/sql_parameterized_execute.py @@ -0,0 +1,13 @@ +def load_firms(cursor, firm_id, firm_name): + cursor.execute( + "SELECT * FROM firms WHERE firm_id = %s", + [firm_id], + ) + cursor.execute( + "SELECT * FROM firms WHERE name = %(name)s", + {"name": firm_name}, + ) + cursor.execute( + "SELECT * FROM firms WHERE name LIKE %s", + [f"%{firm_name}%"], + ) diff --git a/tests/test_files/python/sql_percent_spaced.py b/tests/test_files/python/sql_percent_spaced.py new file mode 100644 index 0000000..69e0696 --- /dev/null +++ b/tests/test_files/python/sql_percent_spaced.py @@ -0,0 +1,2 @@ +def load_user(cursor, user_id): + cursor.execute("SELECT * FROM users WHERE id = %s" % user_id) diff --git a/tests/test_files/python/sql_percent_unspaced.py b/tests/test_files/python/sql_percent_unspaced.py new file mode 100644 index 0000000..fb29acc --- /dev/null +++ b/tests/test_files/python/sql_percent_unspaced.py @@ -0,0 +1,2 @@ +def load_user(cursor, user_id): + cursor.execute("SELECT * FROM users WHERE id = %s"%user_id) diff --git a/tests/test_files/python/sql_percent_variable.py b/tests/test_files/python/sql_percent_variable.py new file mode 100644 index 0000000..53c881f --- /dev/null +++ b/tests/test_files/python/sql_percent_variable.py @@ -0,0 +1,2 @@ +def load_user(cursor, query, user_id): + cursor.execute(query % user_id)