diff --git a/crates/ruff/resources/test/fixtures/flake8_bandit/S201.py b/crates/ruff/resources/test/fixtures/flake8_bandit/S201.py new file mode 100644 index 0000000000..a1e7bddafa --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_bandit/S201.py @@ -0,0 +1,22 @@ +from flask import Flask + +app = Flask(__name__) + +@app.route('/') +def main(): + raise + +# OK +app.run(debug=True) + +# Errors +app.run() +app.run(debug=False) + +# Unrelated +run() +run(debug=True) +run(debug) +foo.run(debug=True) +app = 1 +app.run(debug=True) diff --git a/crates/ruff/src/checkers/ast/analyze/expression.rs b/crates/ruff/src/checkers/ast/analyze/expression.rs index b28170a350..0df4531e4a 100644 --- a/crates/ruff/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff/src/checkers/ast/analyze/expression.rs @@ -586,6 +586,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::LoggingConfigInsecureListen) { flake8_bandit::rules::logging_config_insecure_listen(checker, call); } + if checker.enabled(Rule::FlaskDebugTrue) { + flake8_bandit::rules::flask_debug_true(checker, call); + } if checker.any_enabled(&[ Rule::SubprocessWithoutShellEqualsTrue, Rule::SubprocessPopenWithShellEqualsTrue, diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index d9d8b7b1b5..4c553f1ccd 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -573,6 +573,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Bandit, "110") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::TryExceptPass), (Flake8Bandit, "112") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::TryExceptContinue), (Flake8Bandit, "113") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::RequestWithoutTimeout), + (Flake8Bandit, "201") => (RuleGroup::Preview, rules::flake8_bandit::rules::FlaskDebugTrue), (Flake8Bandit, "301") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::SuspiciousPickleUsage), (Flake8Bandit, "302") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::SuspiciousMarshalUsage), (Flake8Bandit, "303") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::SuspiciousInsecureHashUsage), diff --git a/crates/ruff/src/rules/flake8_bandit/mod.rs b/crates/ruff/src/rules/flake8_bandit/mod.rs index 87d0e449eb..c02f8fe8e9 100644 --- a/crates/ruff/src/rules/flake8_bandit/mod.rs +++ b/crates/ruff/src/rules/flake8_bandit/mod.rs @@ -19,6 +19,7 @@ mod tests { #[test_case(Rule::BadFilePermissions, Path::new("S103.py"))] #[test_case(Rule::CallWithShellEqualsTrue, Path::new("S604.py"))] #[test_case(Rule::ExecBuiltin, Path::new("S102.py"))] + #[test_case(Rule::FlaskDebugTrue, Path::new("S201.py"))] #[test_case(Rule::HardcodedBindAllInterfaces, Path::new("S104.py"))] #[test_case(Rule::HardcodedPasswordDefault, Path::new("S107.py"))] #[test_case(Rule::HardcodedPasswordFuncArg, Path::new("S106.py"))] diff --git a/crates/ruff/src/rules/flake8_bandit/rules/flask_debug_true.rs b/crates/ruff/src/rules/flake8_bandit/rules/flask_debug_true.rs new file mode 100644 index 0000000000..4ecd7a6b9f --- /dev/null +++ b/crates/ruff/src/rules/flake8_bandit/rules/flask_debug_true.rs @@ -0,0 +1,92 @@ +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_true; +use ruff_python_ast::{Expr, ExprAttribute, ExprCall, Stmt, StmtAssign}; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for uses of `debug=True` in Flask. +/// +/// ## Why is this bad? +/// Enabling debug mode shows an interactive debugger in the browser if an +/// error occurs, and allows running arbitrary Python code from the browser. +/// This could leak sensitive information, or allow an attacker to run +/// arbitrary code. +/// +/// ## Example +/// ```python +/// import flask +/// +/// app = Flask() +/// +/// app.run(debug=True) +/// ``` +/// +/// Use instead: +/// ```python +/// import flask +/// +/// app = Flask() +/// +/// app.run(debug=os.environ["ENV"] == "dev") +/// ``` +/// +/// ## References +/// - [Flask documentation: Debug Mode](https://flask.palletsprojects.com/en/latest/quickstart/#debug-mode) +#[violation] +pub struct FlaskDebugTrue; + +impl Violation for FlaskDebugTrue { + #[derive_message_formats] + fn message(&self) -> String { + format!("Use of `debug=True` in Flask app detected") + } +} + +/// S201 +pub(crate) fn flask_debug_true(checker: &mut Checker, call: &ExprCall) { + let Expr::Attribute(ExprAttribute { attr, value, .. }) = call.func.as_ref() else { + return; + }; + + if attr.as_str() != "run" { + return; + } + + let Some(debug_argument) = call.arguments.find_keyword("debug") else { + return; + }; + + if !is_const_true(&debug_argument.value) { + return; + } + + let Expr::Name(name) = value.as_ref() else { + return; + }; + + checker + .semantic() + .resolve_name(name) + .map_or((), |binding_id| { + if let Some(Stmt::Assign(StmtAssign { value, .. })) = checker + .semantic() + .binding(binding_id) + .statement(checker.semantic()) + { + if let Expr::Call(ExprCall { func, .. }) = value.as_ref() { + if checker + .semantic() + .resolve_call_path(func) + .is_some_and(|call_path| matches!(call_path.as_slice(), ["flask", "Flask"])) + { + checker + .diagnostics + .push(Diagnostic::new(FlaskDebugTrue, debug_argument.range())); + } + } + } + }); +} diff --git a/crates/ruff/src/rules/flake8_bandit/rules/mod.rs b/crates/ruff/src/rules/flake8_bandit/rules/mod.rs index 83cf002caf..a2f06ca28b 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/mod.rs @@ -1,6 +1,7 @@ pub(crate) use assert_used::*; pub(crate) use bad_file_permissions::*; pub(crate) use exec_used::*; +pub(crate) use flask_debug_true::*; pub(crate) use hardcoded_bind_all_interfaces::*; pub(crate) use hardcoded_password_default::*; pub(crate) use hardcoded_password_func_arg::*; @@ -24,6 +25,7 @@ pub(crate) use unsafe_yaml_load::*; mod assert_used; mod bad_file_permissions; mod exec_used; +mod flask_debug_true; mod hardcoded_bind_all_interfaces; mod hardcoded_password_default; mod hardcoded_password_func_arg; diff --git a/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S201_S201.py.snap b/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S201_S201.py.snap new file mode 100644 index 0000000000..7f1c3af924 --- /dev/null +++ b/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S201_S201.py.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff/src/rules/flake8_bandit/mod.rs +--- +S201.py:10:9: S201 Use of `debug=True` in Flask app detected + | + 9 | # OK +10 | app.run(debug=True) + | ^^^^^^^^^^ S201 +11 | +12 | # Errors + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 74f7245fcb..762e82f75a 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2572,6 +2572,9 @@ "S110", "S112", "S113", + "S2", + "S20", + "S201", "S3", "S30", "S301",