Skip to content

Commit 653e51a

Browse files
authored
[flake8-bandit] Implement django-raw-sql (S611) (#8651)
See: #1646.
1 parent 04f0625 commit 653e51a

File tree

8 files changed

+139
-0
lines changed

8 files changed

+139
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from django.db.models.expressions import RawSQL
2+
from django.contrib.auth.models import User
3+
4+
User.objects.annotate(val=RawSQL('secure', []))
5+
User.objects.annotate(val=RawSQL('%secure' % 'nos', []))
6+
User.objects.annotate(val=RawSQL('{}secure'.format('no'), []))
7+
raw = '"username") AS "val" FROM "auth_user" WHERE "username"="admin" --'
8+
User.objects.annotate(val=RawSQL(raw, []))
9+
raw = '"username") AS "val" FROM "auth_user"' \
10+
' WHERE "username"="admin" OR 1=%s --'
11+
User.objects.annotate(val=RawSQL(raw, [0]))
12+
User.objects.annotate(val=RawSQL(sql='{}secure'.format('no'), params=[]))
13+
User.objects.annotate(val=RawSQL(params=[], sql='{}secure'.format('no')))

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

+3
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
612612
]) {
613613
flake8_bandit::rules::shell_injection(checker, call);
614614
}
615+
if checker.enabled(Rule::DjangoRawSql) {
616+
flake8_bandit::rules::django_raw_sql(checker, call);
617+
}
615618
if checker.enabled(Rule::UnnecessaryGeneratorList) {
616619
flake8_comprehensions::rules::unnecessary_generator_list(
617620
checker, expr, func, args, keywords,

crates/ruff_linter/src/codes.rs

+1
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
632632
(Flake8Bandit, "607") => (RuleGroup::Stable, rules::flake8_bandit::rules::StartProcessWithPartialPath),
633633
(Flake8Bandit, "608") => (RuleGroup::Stable, rules::flake8_bandit::rules::HardcodedSQLExpression),
634634
(Flake8Bandit, "609") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnixCommandWildcardInjection),
635+
(Flake8Bandit, "611") => (RuleGroup::Preview, rules::flake8_bandit::rules::DjangoRawSql),
635636
(Flake8Bandit, "612") => (RuleGroup::Stable, rules::flake8_bandit::rules::LoggingConfigInsecureListen),
636637
(Flake8Bandit, "701") => (RuleGroup::Stable, rules::flake8_bandit::rules::Jinja2AutoescapeFalse),
637638
(Flake8Bandit, "702") => (RuleGroup::Preview, rules::flake8_bandit::rules::MakoTemplates),

crates/ruff_linter/src/rules/flake8_bandit/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ mod tests {
5050
#[test_case(Rule::UnixCommandWildcardInjection, Path::new("S609.py"))]
5151
#[test_case(Rule::UnsafeYAMLLoad, Path::new("S506.py"))]
5252
#[test_case(Rule::WeakCryptographicKey, Path::new("S505.py"))]
53+
#[test_case(Rule::DjangoRawSql, Path::new("S611.py"))]
5354
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
5455
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
5556
let diagnostics = test_path(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use ruff_diagnostics::{Diagnostic, Violation};
2+
use ruff_macros::{derive_message_formats, violation};
3+
use ruff_python_ast::{self as ast, Expr};
4+
use ruff_text_size::Ranged;
5+
6+
use crate::checkers::ast::Checker;
7+
8+
/// ## What it does
9+
/// Checks for uses of Django's `RawSQL` function.
10+
///
11+
/// ## Why is this bad?
12+
/// Django's `RawSQL` function can be used to execute arbitrary SQL queries,
13+
/// which can in turn lead to SQL injection vulnerabilities.
14+
///
15+
/// ## Example
16+
/// ```python
17+
/// from django.db.models.expressions import RawSQL
18+
/// from django.contrib.auth.models import User
19+
///
20+
/// User.objects.annotate(val=("%secure" % "nos", []))
21+
/// ```
22+
///
23+
/// ## References
24+
/// - [Django documentation: SQL injection protection](https://docs.djangoproject.com/en/dev/topics/security/#sql-injection-protection)
25+
/// - [Common Weakness Enumeration: CWE-89](https://cwe.mitre.org/data/definitions/89.html)
26+
#[violation]
27+
pub struct DjangoRawSql;
28+
29+
impl Violation for DjangoRawSql {
30+
#[derive_message_formats]
31+
fn message(&self) -> String {
32+
format!("Use of `RawSQL` can lead to SQL injection vulnerabilities")
33+
}
34+
}
35+
36+
/// S611
37+
pub(crate) fn django_raw_sql(checker: &mut Checker, call: &ast::ExprCall) {
38+
if checker
39+
.semantic()
40+
.resolve_call_path(&call.func)
41+
.is_some_and(|call_path| {
42+
matches!(
43+
call_path.as_slice(),
44+
["django", "db", "models", "expressions", "RawSQL"]
45+
)
46+
})
47+
{
48+
if !call
49+
.arguments
50+
.find_argument("sql", 0)
51+
.is_some_and(Expr::is_string_literal_expr)
52+
{
53+
checker
54+
.diagnostics
55+
.push(Diagnostic::new(DjangoRawSql, call.func.range()));
56+
}
57+
}
58+
}

crates/ruff_linter/src/rules/flake8_bandit/rules/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub(crate) use assert_used::*;
22
pub(crate) use bad_file_permissions::*;
3+
pub(crate) use django_raw_sql::*;
34
pub(crate) use exec_used::*;
45
pub(crate) use flask_debug_true::*;
56
pub(crate) use hardcoded_bind_all_interfaces::*;
@@ -27,6 +28,7 @@ pub(crate) use weak_cryptographic_key::*;
2728

2829
mod assert_used;
2930
mod bad_file_permissions;
31+
mod django_raw_sql;
3032
mod exec_used;
3133
mod flask_debug_true;
3234
mod hardcoded_bind_all_interfaces;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
3+
---
4+
S611.py:5:27: S611 Use of `RawSQL` can lead to SQL injection vulnerabilities
5+
|
6+
4 | User.objects.annotate(val=RawSQL('secure', []))
7+
5 | User.objects.annotate(val=RawSQL('%secure' % 'nos', []))
8+
| ^^^^^^ S611
9+
6 | User.objects.annotate(val=RawSQL('{}secure'.format('no'), []))
10+
7 | raw = '"username") AS "val" FROM "auth_user" WHERE "username"="admin" --'
11+
|
12+
13+
S611.py:6:27: S611 Use of `RawSQL` can lead to SQL injection vulnerabilities
14+
|
15+
4 | User.objects.annotate(val=RawSQL('secure', []))
16+
5 | User.objects.annotate(val=RawSQL('%secure' % 'nos', []))
17+
6 | User.objects.annotate(val=RawSQL('{}secure'.format('no'), []))
18+
| ^^^^^^ S611
19+
7 | raw = '"username") AS "val" FROM "auth_user" WHERE "username"="admin" --'
20+
8 | User.objects.annotate(val=RawSQL(raw, []))
21+
|
22+
23+
S611.py:8:27: S611 Use of `RawSQL` can lead to SQL injection vulnerabilities
24+
|
25+
6 | User.objects.annotate(val=RawSQL('{}secure'.format('no'), []))
26+
7 | raw = '"username") AS "val" FROM "auth_user" WHERE "username"="admin" --'
27+
8 | User.objects.annotate(val=RawSQL(raw, []))
28+
| ^^^^^^ S611
29+
9 | raw = '"username") AS "val" FROM "auth_user"' \
30+
10 | ' WHERE "username"="admin" OR 1=%s --'
31+
|
32+
33+
S611.py:11:27: S611 Use of `RawSQL` can lead to SQL injection vulnerabilities
34+
|
35+
9 | raw = '"username") AS "val" FROM "auth_user"' \
36+
10 | ' WHERE "username"="admin" OR 1=%s --'
37+
11 | User.objects.annotate(val=RawSQL(raw, [0]))
38+
| ^^^^^^ S611
39+
12 | User.objects.annotate(val=RawSQL(sql='{}secure'.format('no'), params=[]))
40+
13 | User.objects.annotate(val=RawSQL(params=[], sql='{}secure'.format('no')))
41+
|
42+
43+
S611.py:12:27: S611 Use of `RawSQL` can lead to SQL injection vulnerabilities
44+
|
45+
10 | ' WHERE "username"="admin" OR 1=%s --'
46+
11 | User.objects.annotate(val=RawSQL(raw, [0]))
47+
12 | User.objects.annotate(val=RawSQL(sql='{}secure'.format('no'), params=[]))
48+
| ^^^^^^ S611
49+
13 | User.objects.annotate(val=RawSQL(params=[], sql='{}secure'.format('no')))
50+
|
51+
52+
S611.py:13:27: S611 Use of `RawSQL` can lead to SQL injection vulnerabilities
53+
|
54+
11 | User.objects.annotate(val=RawSQL(raw, [0]))
55+
12 | User.objects.annotate(val=RawSQL(sql='{}secure'.format('no'), params=[]))
56+
13 | User.objects.annotate(val=RawSQL(params=[], sql='{}secure'.format('no')))
57+
| ^^^^^^ S611
58+
|
59+
60+

ruff.schema.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)