diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S308.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S308.py
new file mode 100644
index 0000000000000..45a335b00c3d7
--- /dev/null
+++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S308.py
@@ -0,0 +1,22 @@
+from django.utils.safestring import mark_safe
+
+
+def some_func():
+ return mark_safe('')
+
+
+@mark_safe
+def some_func():
+ return ''
+
+
+from django.utils.html import mark_safe
+
+
+def some_func():
+ return mark_safe('')
+
+
+@mark_safe
+def some_func():
+ return ''
diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs
index 3ceac945740fc..7786931f8fab0 100644
--- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs
+++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs
@@ -247,6 +247,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::HardcodedPasswordDefault) {
flake8_bandit::rules::hardcoded_password_default(checker, parameters);
}
+ if checker.enabled(Rule::SuspiciousMarkSafeUsage) {
+ for decorator in decorator_list {
+ flake8_bandit::rules::suspicious_function_decorator(checker, decorator);
+ }
+ }
if checker.enabled(Rule::PropertyWithParameters) {
pylint::rules::property_with_parameters(checker, stmt, decorator_list, parameters);
}
diff --git a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs
index f8922655313be..ec2a462aafbbd 100644
--- a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs
+++ b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs
@@ -46,6 +46,7 @@ mod tests {
#[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))]
#[test_case(Rule::SuspiciousPickleUsage, Path::new("S301.py"))]
#[test_case(Rule::SuspiciousEvalUsage, Path::new("S307.py"))]
+ #[test_case(Rule::SuspiciousMarkSafeUsage, Path::new("S308.py"))]
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
#[test_case(Rule::SuspiciousTelnetlibImport, Path::new("S401.py"))]
diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs
index 2589b9514f2dd..312a55bedc053 100644
--- a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs
+++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs
@@ -3,7 +3,7 @@
//! See:
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
use ruff_macros::{derive_message_formats, violation};
-use ruff_python_ast::{self as ast, Expr, ExprCall};
+use ruff_python_ast::{self as ast, Decorator, Expr, ExprCall};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -848,7 +848,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
// Eval
["" | "builtins", "eval"] => Some(SuspiciousEvalUsage.into()),
// MarkSafe
- ["django", "utils", "safestring", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()),
+ ["django", "utils", "safestring" | "html", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()),
// URLOpen (`urlopen`, `urlretrieve`, `Request`)
["urllib", "request", "urlopen" | "urlretrieve" | "Request"] |
["six", "moves", "urllib", "request", "urlopen" | "urlretrieve" | "Request"] => {
@@ -901,3 +901,27 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
checker.diagnostics.push(diagnostic);
}
}
+
+/// S308
+pub(crate) fn suspicious_function_decorator(checker: &mut Checker, decorator: &Decorator) {
+ let Some(diagnostic_kind) = checker
+ .semantic()
+ .resolve_call_path(&decorator.expression)
+ .and_then(|call_path| {
+ match call_path.as_slice() {
+ // MarkSafe
+ ["django", "utils", "safestring" | "html", "mark_safe"] => {
+ Some(SuspiciousMarkSafeUsage.into())
+ }
+ _ => None,
+ }
+ })
+ else {
+ return;
+ };
+
+ let diagnostic = Diagnostic::new::(diagnostic_kind, decorator.range());
+ if checker.enabled(diagnostic.kind.rule()) {
+ checker.diagnostics.push(diagnostic);
+ }
+}
diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S308_S308.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S308_S308.py.snap
new file mode 100644
index 0000000000000..d2484ff7a57e7
--- /dev/null
+++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S308_S308.py.snap
@@ -0,0 +1,34 @@
+---
+source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
+---
+S308.py:5:12: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
+ |
+4 | def some_func():
+5 | return mark_safe('')
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S308
+ |
+
+S308.py:8:1: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
+ |
+ 8 | @mark_safe
+ | ^^^^^^^^^^ S308
+ 9 | def some_func():
+10 | return ''
+ |
+
+S308.py:17:12: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
+ |
+16 | def some_func():
+17 | return mark_safe('')
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S308
+ |
+
+S308.py:20:1: S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
+ |
+20 | @mark_safe
+ | ^^^^^^^^^^ S308
+21 | def some_func():
+22 | return ''
+ |
+
+