-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[flake8-bandit
]: Implement S610
rule
#10316
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
from django.contrib.auth.models import User | ||
|
||
# Errors | ||
User.objects.filter(username='admin').extra(dict(could_be='insecure')) | ||
User.objects.filter(username='admin').extra(select=dict(could_be='insecure')) | ||
User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'}) | ||
User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')}) | ||
User.objects.filter(username='admin').extra(where=['%secure' % 'nos']) | ||
User.objects.filter(username='admin').extra(where=['{}secure'.format('no')]) | ||
|
||
query = '"username") AS "username", * FROM "auth_user" WHERE 1=1 OR "username"=? --' | ||
User.objects.filter(username='admin').extra(select={'test': query}) | ||
|
||
where_var = ['1=1) OR 1=1 AND (1=1'] | ||
User.objects.filter(username='admin').extra(where=where_var) | ||
|
||
where_str = '1=1) OR 1=1 AND (1=1' | ||
User.objects.filter(username='admin').extra(where=[where_str]) | ||
|
||
tables_var = ['django_content_type" WHERE "auth_user"."username"="admin'] | ||
User.objects.all().extra(tables=tables_var).distinct() | ||
|
||
tables_str = 'django_content_type" WHERE "auth_user"."username"="admin' | ||
User.objects.all().extra(tables=[tables_str]).distinct() | ||
|
||
# OK | ||
User.objects.filter(username='admin').extra( | ||
select={'test': 'secure'}, | ||
where=['secure'], | ||
tables=['secure'] | ||
) | ||
User.objects.filter(username='admin').extra({'test': 'secure'}) | ||
User.objects.filter(username='admin').extra(select={'test': 'secure'}) | ||
User.objects.filter(username='admin').extra(where=['secure']) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
use ruff_diagnostics::{Diagnostic, Violation}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_ast::{self as ast, Expr, ExprAttribute, ExprDict, ExprList}; | ||
use ruff_python_semantic::Modules; | ||
use ruff_text_size::Ranged; | ||
|
||
use crate::checkers::ast::Checker; | ||
|
||
/// ## What it does | ||
/// Checks for uses of Django's `extra` function. | ||
/// | ||
/// ## Why is this bad? | ||
/// Django's `extra` function can be used to execute arbitrary SQL queries, | ||
/// which can in turn lead to SQL injection vulnerabilities. | ||
/// | ||
/// ## Example | ||
/// ```python | ||
/// from django.contrib.auth.models import User | ||
/// | ||
/// User.objects.all().extra(select={"test": "%secure" % "nos"}) | ||
/// ``` | ||
/// | ||
/// ## References | ||
/// - [Django documentation: SQL injection protection](https://docs.djangoproject.com/en/dev/topics/security/#sql-injection-protection) | ||
/// - [Common Weakness Enumeration: CWE-89](https://cwe.mitre.org/data/definitions/89.html) | ||
#[violation] | ||
pub struct DjangoExtra; | ||
|
||
impl Violation for DjangoExtra { | ||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
format!("Use of `extra` can lead to SQL injection vulnerabilities") | ||
} | ||
} | ||
|
||
/// S610 | ||
pub(crate) fn django_extra(checker: &mut Checker, call: &ast::ExprCall) { | ||
if !checker.semantic().seen_module(Modules::DJANGO) { | ||
return; | ||
} | ||
|
||
let Expr::Attribute(ExprAttribute { attr, .. }) = call.func.as_ref() else { | ||
return; | ||
}; | ||
|
||
if attr.as_str() != "extra" { | ||
return; | ||
} | ||
|
||
if is_call_insecure(call) { | ||
checker | ||
.diagnostics | ||
.push(Diagnostic::new(DjangoExtra, call.arguments.range())); | ||
} | ||
} | ||
|
||
fn is_call_insecure(call: &ast::ExprCall) -> bool { | ||
for (argument_name, position) in [("select", 0), ("where", 1), ("tables", 3)] { | ||
if let Some(argument) = call.arguments.find_argument(argument_name, position) { | ||
match argument_name { | ||
"select" => match argument { | ||
Expr::Dict(ExprDict { keys, values, .. }) => { | ||
if !keys.iter().flatten().all(Expr::is_string_literal_expr) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to flag on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nevermind, I misread. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might want to consider allowing: |
||
return true; | ||
} | ||
if !values.iter().all(Expr::is_string_literal_expr) { | ||
return true; | ||
} | ||
} | ||
_ => return true, | ||
}, | ||
"where" | "tables" => match argument { | ||
Expr::List(ExprList { elts, .. }) => { | ||
if !elts.iter().all(Expr::is_string_literal_expr) { | ||
return true; | ||
} | ||
} | ||
_ => return true, | ||
}, | ||
_ => (), | ||
} | ||
} | ||
} | ||
|
||
false | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
--- | ||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs | ||
--- | ||
S610.py:4:44: S610 Use of `extra` can lead to SQL injection vulnerabilities | ||
| | ||
3 | # Errors | ||
4 | User.objects.filter(username='admin').extra(dict(could_be='insecure')) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ S610 | ||
5 | User.objects.filter(username='admin').extra(select=dict(could_be='insecure')) | ||
6 | User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'}) | ||
| | ||
|
||
S610.py:5:44: S610 Use of `extra` can lead to SQL injection vulnerabilities | ||
| | ||
3 | # Errors | ||
4 | User.objects.filter(username='admin').extra(dict(could_be='insecure')) | ||
5 | User.objects.filter(username='admin').extra(select=dict(could_be='insecure')) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S610 | ||
6 | User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'}) | ||
7 | User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')}) | ||
| | ||
|
||
S610.py:6:44: S610 Use of `extra` can lead to SQL injection vulnerabilities | ||
| | ||
4 | User.objects.filter(username='admin').extra(dict(could_be='insecure')) | ||
5 | User.objects.filter(username='admin').extra(select=dict(could_be='insecure')) | ||
6 | User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'}) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S610 | ||
7 | User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')}) | ||
8 | User.objects.filter(username='admin').extra(where=['%secure' % 'nos']) | ||
| | ||
|
||
S610.py:7:44: S610 Use of `extra` can lead to SQL injection vulnerabilities | ||
| | ||
5 | User.objects.filter(username='admin').extra(select=dict(could_be='insecure')) | ||
6 | User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'}) | ||
7 | User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')}) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S610 | ||
8 | User.objects.filter(username='admin').extra(where=['%secure' % 'nos']) | ||
9 | User.objects.filter(username='admin').extra(where=['{}secure'.format('no')]) | ||
| | ||
|
||
S610.py:8:44: S610 Use of `extra` can lead to SQL injection vulnerabilities | ||
| | ||
6 | User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'}) | ||
7 | User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')}) | ||
8 | User.objects.filter(username='admin').extra(where=['%secure' % 'nos']) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ S610 | ||
9 | User.objects.filter(username='admin').extra(where=['{}secure'.format('no')]) | ||
| | ||
|
||
S610.py:9:44: S610 Use of `extra` can lead to SQL injection vulnerabilities | ||
| | ||
7 | User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')}) | ||
8 | User.objects.filter(username='admin').extra(where=['%secure' % 'nos']) | ||
9 | User.objects.filter(username='admin').extra(where=['{}secure'.format('no')]) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S610 | ||
10 | | ||
11 | query = '"username") AS "username", * FROM "auth_user" WHERE 1=1 OR "username"=? --' | ||
| | ||
|
||
S610.py:12:44: S610 Use of `extra` can lead to SQL injection vulnerabilities | ||
| | ||
11 | query = '"username") AS "username", * FROM "auth_user" WHERE 1=1 OR "username"=? --' | ||
12 | User.objects.filter(username='admin').extra(select={'test': query}) | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^ S610 | ||
13 | | ||
14 | where_var = ['1=1) OR 1=1 AND (1=1'] | ||
| | ||
|
||
S610.py:15:44: S610 Use of `extra` can lead to SQL injection vulnerabilities | ||
| | ||
14 | where_var = ['1=1) OR 1=1 AND (1=1'] | ||
15 | User.objects.filter(username='admin').extra(where=where_var) | ||
| ^^^^^^^^^^^^^^^^^ S610 | ||
16 | | ||
17 | where_str = '1=1) OR 1=1 AND (1=1' | ||
| | ||
|
||
S610.py:18:44: S610 Use of `extra` can lead to SQL injection vulnerabilities | ||
| | ||
17 | where_str = '1=1) OR 1=1 AND (1=1' | ||
18 | User.objects.filter(username='admin').extra(where=[where_str]) | ||
| ^^^^^^^^^^^^^^^^^^^ S610 | ||
19 | | ||
20 | tables_var = ['django_content_type" WHERE "auth_user"."username"="admin'] | ||
| | ||
|
||
S610.py:21:25: S610 Use of `extra` can lead to SQL injection vulnerabilities | ||
| | ||
20 | tables_var = ['django_content_type" WHERE "auth_user"."username"="admin'] | ||
21 | User.objects.all().extra(tables=tables_var).distinct() | ||
| ^^^^^^^^^^^^^^^^^^^ S610 | ||
22 | | ||
23 | tables_str = 'django_content_type" WHERE "auth_user"."username"="admin' | ||
| | ||
|
||
S610.py:24:25: S610 Use of `extra` can lead to SQL injection vulnerabilities | ||
| | ||
23 | tables_str = 'django_content_type" WHERE "auth_user"."username"="admin' | ||
24 | User.objects.all().extra(tables=[tables_str]).distinct() | ||
| ^^^^^^^^^^^^^^^^^^^^^ S610 | ||
25 | | ||
26 | # OK | ||
| |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also done for django_raw_sql, but I'm wondering if this should also be done for this rule.
In
django_raw_sql
,RawSQL
import comes fromdjango
module, so this is reliable, but in our case,extra
can be used even if nodjango
imports occurs, if for instance a model defined in another file in a codebase is used, so this could skip the check for several valid cases.On the other hand, the entire rule has the potential of having several false positives, so this could be seen as a way to know if a file belongs to a project using Django. But if I'm not mistaken,
seen_module
only applies to the current file, not the whole codebase, and in this specific case the latter would probably be preferable.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Case in point, the ecosystem checks detects 18 errors while
bandit
detects 22. That's because there are 4 things that would have been flagged in https://github.com/zulip/zulip/blob/29ca4ba6620f16598e1171be7495356f56b84328/zerver/tests/test_message_move_topic.py, but are skipped because there is no import fromdjango
in this file.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm okay with leaving this in for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could go either way though.
extra
isn't a common method name.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think that the combination of
extra
andselect
/where
/tables
arguments should not be that common outside of Django. Worst case, for the few times it would get triggered, it's always possible for users to ignore the false positives, while skipping the check entirely if Ruff does not find adjango
import would leave the user with no clue that the check was not run because of that. So I think it might be best in the end to not check if Django was seen.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went ahead and removed the check in 4e88142. Also updated the error message so that in case of false positives, the user at least knows that this is related to Django.