diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S702.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S702.py new file mode 100644 index 00000000000000..29dd38c9f7ebc0 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S702.py @@ -0,0 +1,11 @@ +from mako.template import Template +import mako + +from mako import template + +Template("hello") + +# XXX(fletcher): for some reason, bandit is missing the one below. keeping it +# in for now so that if it gets fixed inadvertitently we know. +mako.template.Template("hern") +template.Template("hern") diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index e769f40e3cd7ec..96ef448f06cedf 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -571,6 +571,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::Jinja2AutoescapeFalse) { flake8_bandit::rules::jinja2_autoescape_false(checker, call); } + if checker.enabled(Rule::UseOfMakoTemplates) { + flake8_bandit::rules::use_of_mako_templates(checker, call); + } if checker.enabled(Rule::HardcodedPasswordFuncArg) { flake8_bandit::rules::hardcoded_password_func_arg(checker, keywords); } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 320b62020d4b31..3994e4b2e7d4f5 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -629,6 +629,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Bandit, "609") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnixCommandWildcardInjection), (Flake8Bandit, "612") => (RuleGroup::Stable, rules::flake8_bandit::rules::LoggingConfigInsecureListen), (Flake8Bandit, "701") => (RuleGroup::Stable, rules::flake8_bandit::rules::Jinja2AutoescapeFalse), + (Flake8Bandit, "702") => (RuleGroup::Stable, rules::flake8_bandit::rules::UseOfMakoTemplates), // flake8-boolean-trap (Flake8BooleanTrap, "001") => (RuleGroup::Stable, rules::flake8_boolean_trap::rules::BooleanTypeHintPositionalArgument), diff --git a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs index f915fd88190ad5..133fe3332bcbeb 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs @@ -28,6 +28,7 @@ mod tests { #[test_case(Rule::HardcodedTempFile, Path::new("S108.py"))] #[test_case(Rule::HashlibInsecureHashFunction, Path::new("S324.py"))] #[test_case(Rule::Jinja2AutoescapeFalse, Path::new("S701.py"))] + #[test_case(Rule::UseOfMakoTemplates, Path::new("S702.py"))] #[test_case(Rule::LoggingConfigInsecureListen, Path::new("S612.py"))] #[test_case(Rule::ParamikoCall, Path::new("S601.py"))] #[test_case(Rule::RequestWithNoCertValidation, Path::new("S501.py"))] diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/mod.rs index 24ad5a3721c856..876ba1fb63a4cd 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/mod.rs @@ -22,6 +22,7 @@ pub(crate) use suspicious_function_call::*; pub(crate) use try_except_continue::*; pub(crate) use try_except_pass::*; pub(crate) use unsafe_yaml_load::*; +pub(crate) use use_of_mako_templates::*; pub(crate) use weak_cryptographic_key::*; mod assert_used; @@ -48,4 +49,5 @@ mod suspicious_function_call; mod try_except_continue; mod try_except_pass; mod unsafe_yaml_load; +mod use_of_mako_templates; mod weak_cryptographic_key; diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/use_of_mako_templates.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/use_of_mako_templates.rs new file mode 100644 index 00000000000000..27d99d67bb2c42 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/use_of_mako_templates.rs @@ -0,0 +1,59 @@ +use crate::checkers::ast::Checker; +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::{self as ast}; +use ruff_text_size::Ranged; + +/// ## What it does +/// Checks for uses of the `mako` templates. +/// +/// ## Why is this bad? +/// Mako templates allow HTML/JS rendering by default and are +/// inherently open to XSS attacks. Ensure variables in all templates are +/// properly sanitized via the 'n', 'h' or 'x' flags (depending on context). +/// For example, to HTML escape the variable 'data' do ${ data |h }. +/// +/// ## Example +/// ```python +/// from mako.template import Template +/// +/// Template("hello") +/// ``` +/// +/// Use instead: +/// ```python +/// from mako.template import Template +/// +/// Template("hello |h") +/// ``` +/// +/// ## References +/// - [Mako documentation](https://www.makotemplates.org/) +/// - [OpenStack security: Cross site scripting XSS](https://security.openstack.org/guidelines/dg_cross-site-scripting-xss.html) +/// - [Common Weakness Enumeration: CWE-80](https://cwe.mitre.org/data/definitions/80.html) +#[violation] +pub struct UseOfMakoTemplates; + +impl Violation for UseOfMakoTemplates { + #[derive_message_formats] + fn message(&self) -> String { + format!( + "Mako templates allow HTML/JS rendering by default and are inherently open to XSS attacks.\ + Ensure variables in all templates are properly sanitized via the 'n', 'h' or 'x' flags (depending on context).\ + For example, to HTML escape the variable 'data' do ${{ data |h }}." + ) + } +} + +/// S702 +pub(crate) fn use_of_mako_templates(checker: &mut Checker, call: &ast::ExprCall) { + if checker + .semantic() + .resolve_call_path(&call.func) + .is_some_and(|call_path| matches!(call_path.as_slice(), ["mako", "template", "Template"])) + { + checker + .diagnostics + .push(Diagnostic::new(UseOfMakoTemplates, call.func.range())); + } +} diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S702_S702.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S702_S702.py.snap new file mode 100644 index 00000000000000..8b504138b4cecc --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S702_S702.py.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs +--- +S702.py:6:1: S702 Mako templates allow HTML/JS rendering by default and are inherently open to XSS attacks.Ensure variables in all templates are properly sanitized via the 'n', 'h' or 'x' flags (depending on context).For example, to HTML escape the variable 'data' do ${ data |h }. + | +4 | from mako import template +5 | +6 | Template("hello") + | ^^^^^^^^ S702 +7 | +8 | # XXX(fletcher): for some reason, bandit is missing the one below. keeping it + | + +S702.py:10:1: S702 Mako templates allow HTML/JS rendering by default and are inherently open to XSS attacks.Ensure variables in all templates are properly sanitized via the 'n', 'h' or 'x' flags (depending on context).For example, to HTML escape the variable 'data' do ${ data |h }. + | + 8 | # XXX(fletcher): for some reason, bandit is missing the one below. keeping it + 9 | # in for now so that if it gets fixed inadvertitently we know. +10 | mako.template.Template("hern") + | ^^^^^^^^^^^^^^^^^^^^^^ S702 +11 | template.Template("hern") + | + +S702.py:11:1: S702 Mako templates allow HTML/JS rendering by default and are inherently open to XSS attacks.Ensure variables in all templates are properly sanitized via the 'n', 'h' or 'x' flags (depending on context).For example, to HTML escape the variable 'data' do ${ data |h }. + | + 9 | # in for now so that if it gets fixed inadvertitently we know. +10 | mako.template.Template("hern") +11 | template.Template("hern") + | ^^^^^^^^^^^^^^^^^ S702 + | + + diff --git a/ruff.schema.json b/ruff.schema.json index c77e480c468605..30f4b342889def 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3386,6 +3386,7 @@ "S7", "S70", "S701", + "S702", "SIM", "SIM1", "SIM10",