Skip to content

Commit

Permalink
feat(python): add sql injection rules
Browse files Browse the repository at this point in the history
  • Loading branch information
didroe committed May 21, 2024
1 parent f4b8158 commit c72d02d
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 0 deletions.
49 changes: 49 additions & 0 deletions rules/python/django/sql_injection.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
imports:
- python_shared_common_sql_user_input
patterns:
- pattern: $<_>.objects.raw($<USER_INPUT>$<...>)
filters:
- variable: USER_INPUT
detection: python_shared_common_sql_user_input
scope: result
languages:
- python
severity: critical
metadata:
description: Unsanitized external input in SQL query
remediation_message: |-
## Description
Using unsanitized data, such as user input or request data, or externally influenced data passed to a function, in SQL query exposes your application to SQL injection attacks. This vulnerability arises when externally controlled data is directly included in SQL statements without proper sanitation, allowing attackers to manipulate queries and access or modify data.
## Remediations
- **Do not** include raw external input in SQL queries. This practice can lead to SQL injection vulnerabilities.
```python
sorting_order = request.GET["untrusted"]
query = f"SELECT id, name FROM products ORDER BY name LIMIT 20 {sorting_order};"; # unsafe
```
- **Do** validate all external input to ensure it meets the expected format before including it in SQL queries.
```python
sorting_order = "DESC" if request.GET["sortingOrder"] == "DESC" else "ASC"
```
- **Do** use parameters for database queries to separate SQL logic from external input, significantly reducing the risk of SQL injection.
```python
Product.objects.raw("SELECT * FROM products WHERE id LIKE ?", [f"%{product_id}%"])
```
- **Do** escape all external input using appropriate database-specific escaping functions before including it in SQL queries.
```python
from mysql.connector.conversion import MySQLConverter
converter = MySQLConverter(connection)
ok = converter.escape(request.GET["value"])
```
## References
- [OWASP SQL injection explained](https://owasp.org/www-community/attacks/SQL_Injection)
- [OWASP SQL injection prevention cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)
cwe_id:
- 89
id: python_django_sql_injection
documentation_url: https://docs.bearer.com/reference/rules/python_django_sql_injection
75 changes: 75 additions & 0 deletions rules/python/lang/sql_injection.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
imports:
- python_shared_common_sql_user_input
- python_shared_lang_import1
patterns:
- pattern: $<CURSOR>.$<METHOD>($<USER_INPUT>$<...>)
filters:
- variable: CURSOR
detection: python_lang_sql_injection_cursor
scope: cursor
- variable: METHOD
values:
- callproc
- execute
- executemany
- variable: USER_INPUT
detection: python_shared_common_sql_user_input
scope: result
- pattern: $<TEXT>($<USER_INPUT>)
filters:
- variable: TEXT
detection: python_shared_lang_import1
scope: cursor
filters:
- variable: MODULE1
values: [sqlalchemy]
- variable: NAME
values: [text]
- variable: USER_INPUT
detection: python_shared_common_sql_user_input
scope: result
auxiliary:
- id: python_lang_sql_injection_cursor
patterns:
- $<_>.cursor()
languages:
- python
severity: critical
metadata:
description: Unsanitized external input in SQL query
remediation_message: |-
## Description
Using unsanitized data, such as user input or request data, or externally influenced data passed to a function, in SQL query exposes your application to SQL injection attacks. This vulnerability arises when externally controlled data is directly included in SQL statements without proper sanitation, allowing attackers to manipulate queries and access or modify data.
## Remediations
- **Do not** include raw external input in SQL queries. This practice can lead to SQL injection vulnerabilities.
```python
sorting_order = request.GET["untrusted"]
query = f"SELECT id, name FROM products ORDER BY name LIMIT 20 {sorting_order};"; # unsafe
```
- **Do** validate all external input to ensure it meets the expected format before including it in SQL queries.
```python
sorting_order = "DESC" if request.GET["sortingOrder"] == "DESC" else "ASC"
```
- **Do** use parameters for database queries to separate SQL logic from external input, significantly reducing the risk of SQL injection.
```python
cursor.execute("SELECT * FROM products WHERE id LIKE ?", [f"%{product_id}%"])
```
- **Do** escape all external input using appropriate database-specific escaping functions before including it in SQL queries.
```python
from mysql.connector.conversion import MySQLConverter
converter = MySQLConverter(connection)
ok = converter.escape(request.GET["value"])
```
## References
- [OWASP SQL injection explained](https://owasp.org/www-community/attacks/SQL_Injection)
- [OWASP SQL injection prevention cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)
cwe_id:
- 89
id: python_lang_sql_injection
documentation_url: https://docs.bearer.com/reference/rules/python_lang_sql_injection
80 changes: 80 additions & 0 deletions rules/python/shared/common/sql_user_input.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
type: shared
languages:
- python
imports:
- python_shared_common_external_input
- python_shared_lang_instance
- python_shared_lang_import1
- python_shared_lang_import2
- python_shared_lang_import3
sanitizer: python_shared_common_sql_user_input_sanitizer
patterns:
- pattern: $<INPUT>
filters:
- variable: INPUT
detection: python_shared_common_external_input
scope: cursor
auxiliary:
- id: python_shared_common_sql_user_input_sanitizer
patterns:
- pattern: $<CONVERTER>.escape($<_>)
filters:
- variable: CONVERTER
detection: python_shared_lang_instance
scope: cursor
filters:
- variable: CLASS
detection: python_shared_lang_import3
scope: cursor
filters:
- variable: MODULE1
values: [mysql]
- variable: MODULE2
values: [connector]
- variable: MODULE3
values: [conversion]
- variable: NAME
values: [MySQLConverter]
- pattern: $<CONNECTION>.$<METHOD>($<...>)
filters:
- variable: CONNECTION
detection: python_shared_common_sql_user_input_pymysql_connection
scope: cursor
- variable: METHOD
values:
- escape
- escape_string
- pattern: $<ADAPTER>.getquoted()
filters:
- variable: ADAPTER
detection: python_shared_common_sql_user_input_psycopg_adapter
scope: cursor
- id: python_shared_common_sql_user_input_pymysql_connection
patterns:
- pattern: $<CONNECT>($<...>)
filters:
- variable: CONNECT
detection: python_shared_lang_import1
scope: cursor
filters:
- variable: MODULE1
values: [pymysql]
- variable: NAME
values: [connect]
- id: python_shared_common_sql_user_input_psycopg_adapter
patterns:
- pattern: $<ADAPT>($<...>)
filters:
- variable: ADAPT
detection: python_shared_lang_import2
scope: cursor
filters:
- variable: MODULE1
values: [psycopg2]
- variable: MODULE2
values: [extensions]
- variable: NAME
values: [adapt]
metadata:
description: "Python SQL user input."
id: python_shared_common_sql_user_input
20 changes: 20 additions & 0 deletions tests/python/django/sql_injection/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const {
createNewInvoker,
getEnvironment,
} = require("../../../helper.js")
const { ruleId, ruleFile, testBase } = getEnvironment(__dirname)

describe(ruleId, () => {
const invoke = createNewInvoker(ruleId, ruleFile, testBase)

test("sql_injection", () => {
const testCase = "main.py"

const results = invoke(testCase)

expect(results).toEqual({
Missing: [],
Extra: []
})
})
})
10 changes: 10 additions & 0 deletions tests/python/django/sql_injection/testdata/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
user_input = input()

User.objects.raw(f"SELECT * FROM x WHERE y = ?", [user_input])
# bearer:expected python_django_sql_injection
User.objects.raw(f"SELECT * FROM x WHERE y = {user_input}", [])

import pymysql
safe = pymysql.connect().escape_string(user_input)
User.objects.raw(f"SELECT * FROM x WHERE y = {safe}", [])

20 changes: 20 additions & 0 deletions tests/python/lang/sql_injection/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const {
createNewInvoker,
getEnvironment,
} = require("../../../helper.js")
const { ruleId, ruleFile, testBase } = getEnvironment(__dirname)

describe(ruleId, () => {
const invoke = createNewInvoker(ruleId, ruleFile, testBase)

test("sql_injection", () => {
const testCase = "main.py"

const results = invoke(testCase)

expect(results).toEqual({
Missing: [],
Extra: []
})
})
})
61 changes: 61 additions & 0 deletions tests/python/lang/sql_injection/testdata/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
user_input = input()

def generic():
cursor = conn.cursor()

cursor.callproc("SELECT * FROM x WHERE y = :foo", { "foo": user_input })
# bearer:expected python_lang_sql_injection
cursor.callproc(user_input, { "foo": 42 })
cursor.execute("SELECT * FROM x WHERE y = ?", [user_input])
# bearer:expected python_lang_sql_injection
cursor.execute("SELECT * FROM " + user_input, { "foo": 42 })

with connection.cursor() as c:
c.executemany("UPDATE bar SET foo = 1 WHERE baz = :b", { "b": user_input })
# bearer:expected python_lang_sql_injection
c.executemany(f"UPDATE bar SET foo = 1 WHERE baz = {user_input}", {})


def sqlalchemy():
from sqlalchemy import create_engine, text

engine = create_engine('sqlite:///example.db')

with engine.connect() as sqlalcon:
result = sqlalcon.execute(text("SELECT * FROM :x"), { "x": user_input })
# bearer:expected python_lang_sql_injection
result = sqlalcon.execute(text(f"SELECT * FROM {user_input}"))


def mysql_connector_sanitizer():
import mysql.connector
from mysql.connector.conversion import MySQLConverter

conn = mysql.connector.connect()
converter = MySQLConverter(conn)
cursor = conn.cursor()

# bearer:expected python_lang_sql_injection
cursor.execute(user_input)
cursor.execute(converter.escape(user_input))

def pymysql_sanitizer():
import pymysql

conn = pymysql.connect()
cursor = conn.cursor()

# bearer:expected python_lang_sql_injection
cursor.execute(user_input)
cursor.execute(conn.escape_string(user_input))

def psycopg_sanitizer():
import psycopg2
import psycopg2.extensions

conn = psycopg2.connect(database="your_database")
cursor = conn.cursor()

# bearer:expected python_lang_sql_injection
cursor.execute(user_input)
cursor.execute(psycopg2.extensions.adapt(user_input).getquoted())

0 comments on commit c72d02d

Please sign in to comment.