Skip to content

Commit 1a90408

Browse files
[flake8-bandit] Add Rule for S701 (jinja2 autoescape false) (#1815)
ref: #1646 Co-authored-by: Charlie Marsh <[email protected]>
1 parent 07134c5 commit 1a90408

File tree

10 files changed

+183
-1
lines changed

10 files changed

+183
-1
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,7 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/4.1.1/) on
783783
| S506 | UnsafeYAMLLoad | Probable use of unsafe `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. | |
784784
| S508 | SnmpInsecureVersion | The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. | |
785785
| S509 | SnmpWeakCryptography | You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. | |
786+
| S701 | Jinja2AutoescapeFalse | By default, jinja2 sets `autoescape` to `False`. Consider using `autoescape=True` or the `select_autoescape` function to mitigate XSS vulnerabilities. | |
786787

787788
### flake8-blind-except (BLE)
788789

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import jinja2
2+
from jinja2 import Environment, select_autoescape
3+
templateLoader = jinja2.FileSystemLoader( searchpath="/" )
4+
something = ''
5+
6+
Environment(loader=templateLoader, load=templateLoader, autoescape=True)
7+
templateEnv = jinja2.Environment(autoescape=True,
8+
loader=templateLoader )
9+
Environment(loader=templateLoader, load=templateLoader, autoescape=something) # S701
10+
templateEnv = jinja2.Environment(autoescape=False, loader=templateLoader ) # S701
11+
Environment(loader=templateLoader,
12+
load=templateLoader,
13+
autoescape=False) # S701
14+
15+
Environment(loader=templateLoader, # S701
16+
load=templateLoader)
17+
18+
Environment(loader=templateLoader, autoescape=select_autoescape())
19+
20+
Environment(loader=templateLoader,
21+
autoescape=select_autoescape(['html', 'htm', 'xml']))
22+
23+
Environment(loader=templateLoader,
24+
autoescape=jinja2.select_autoescape(['html', 'htm', 'xml']))
25+
26+
27+
def fake_func():
28+
return 'foobar'
29+
Environment(loader=templateLoader, autoescape=fake_func()) # S701

ruff.schema.json

+3
Original file line numberDiff line numberDiff line change
@@ -1515,6 +1515,9 @@
15151515
"S506",
15161516
"S508",
15171517
"S509",
1518+
"S7",
1519+
"S70",
1520+
"S701",
15181521
"SIM",
15191522
"SIM1",
15201523
"SIM10",

src/checkers/ast.rs

+11
Original file line numberDiff line numberDiff line change
@@ -2023,6 +2023,17 @@ where
20232023
self.diagnostics.push(diagnostic);
20242024
}
20252025
}
2026+
if self.settings.enabled.contains(&RuleCode::S701) {
2027+
if let Some(diagnostic) = flake8_bandit::rules::jinja2_autoescape_false(
2028+
func,
2029+
args,
2030+
keywords,
2031+
&self.from_imports,
2032+
&self.import_aliases,
2033+
) {
2034+
self.diagnostics.push(diagnostic);
2035+
}
2036+
}
20262037
if self.settings.enabled.contains(&RuleCode::S106) {
20272038
self.diagnostics
20282039
.extend(flake8_bandit::rules::hardcoded_password_func_arg(keywords));

src/flake8_bandit/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ mod tests {
2828
#[test_case(RuleCode::S506, Path::new("S506.py"); "S506")]
2929
#[test_case(RuleCode::S508, Path::new("S508.py"); "S508")]
3030
#[test_case(RuleCode::S509, Path::new("S509.py"); "S509")]
31+
#[test_case(RuleCode::S701, Path::new("S701.py"); "S701")]
3132
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {
3233
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
3334
let diagnostics = test_path(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use rustc_hash::{FxHashMap, FxHashSet};
2+
use rustpython_ast::{Expr, ExprKind, Keyword};
3+
use rustpython_parser::ast::Constant;
4+
5+
use crate::ast::helpers::{collect_call_paths, dealias_call_path, match_call_path, SimpleCallArgs};
6+
use crate::ast::types::Range;
7+
use crate::registry::Diagnostic;
8+
use crate::violations;
9+
10+
/// S701
11+
pub fn jinja2_autoescape_false(
12+
func: &Expr,
13+
args: &[Expr],
14+
keywords: &[Keyword],
15+
from_imports: &FxHashMap<&str, FxHashSet<&str>>,
16+
import_aliases: &FxHashMap<&str, &str>,
17+
) -> Option<Diagnostic> {
18+
if match_call_path(
19+
&dealias_call_path(collect_call_paths(func), import_aliases),
20+
"jinja2",
21+
"Environment",
22+
from_imports,
23+
) {
24+
let call_args = SimpleCallArgs::new(args, keywords);
25+
26+
if let Some(autoescape_arg) = call_args.get_argument("autoescape", None) {
27+
match &autoescape_arg.node {
28+
ExprKind::Constant {
29+
value: Constant::Bool(true),
30+
..
31+
} => (),
32+
ExprKind::Call { func, .. } => {
33+
if let ExprKind::Name { id, .. } = &func.node {
34+
if id.as_str() != "select_autoescape" {
35+
return Some(Diagnostic::new(
36+
violations::Jinja2AutoescapeFalse(true),
37+
Range::from_located(autoescape_arg),
38+
));
39+
}
40+
}
41+
}
42+
_ => {
43+
return Some(Diagnostic::new(
44+
violations::Jinja2AutoescapeFalse(true),
45+
Range::from_located(autoescape_arg),
46+
))
47+
}
48+
}
49+
} else {
50+
return Some(Diagnostic::new(
51+
violations::Jinja2AutoescapeFalse(false),
52+
Range::from_located(func),
53+
));
54+
}
55+
}
56+
None
57+
}

src/flake8_bandit/rules/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub use hardcoded_password_string::{
99
};
1010
pub use hardcoded_tmp_directory::hardcoded_tmp_directory;
1111
pub use hashlib_insecure_hash_functions::hashlib_insecure_hash_functions;
12+
pub use jinja2_autoescape_false::jinja2_autoescape_false;
1213
pub use request_with_no_cert_validation::request_with_no_cert_validation;
1314
pub use request_without_timeout::request_without_timeout;
1415
pub use snmp_insecure_version::snmp_insecure_version;
@@ -24,6 +25,7 @@ mod hardcoded_password_func_arg;
2425
mod hardcoded_password_string;
2526
mod hardcoded_tmp_directory;
2627
mod hashlib_insecure_hash_functions;
28+
mod jinja2_autoescape_false;
2729
mod request_with_no_cert_validation;
2830
mod request_without_timeout;
2931
mod snmp_insecure_version;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
source: src/flake8_bandit/mod.rs
3+
expression: diagnostics
4+
---
5+
- kind:
6+
Jinja2AutoescapeFalse: true
7+
location:
8+
row: 9
9+
column: 67
10+
end_location:
11+
row: 9
12+
column: 76
13+
fix: ~
14+
parent: ~
15+
- kind:
16+
Jinja2AutoescapeFalse: true
17+
location:
18+
row: 10
19+
column: 44
20+
end_location:
21+
row: 10
22+
column: 49
23+
fix: ~
24+
parent: ~
25+
- kind:
26+
Jinja2AutoescapeFalse: true
27+
location:
28+
row: 13
29+
column: 23
30+
end_location:
31+
row: 13
32+
column: 28
33+
fix: ~
34+
parent: ~
35+
- kind:
36+
Jinja2AutoescapeFalse: false
37+
location:
38+
row: 15
39+
column: 0
40+
end_location:
41+
row: 15
42+
column: 11
43+
fix: ~
44+
parent: ~
45+
- kind:
46+
Jinja2AutoescapeFalse: true
47+
location:
48+
row: 29
49+
column: 46
50+
end_location:
51+
row: 29
52+
column: 57
53+
fix: ~
54+
parent: ~
55+

src/registry.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -430,8 +430,9 @@ define_rule_mapping!(
430430
S324 => violations::HashlibInsecureHashFunction,
431431
S501 => violations::RequestWithNoCertValidation,
432432
S506 => violations::UnsafeYAMLLoad,
433-
S508 => violations::SnmpInsecureVersion,
433+
S508 => violations::SnmpInsecureVersion,
434434
S509 => violations::SnmpWeakCryptography,
435+
S701 => violations::Jinja2AutoescapeFalse,
435436
// flake8-boolean-trap
436437
FBT001 => violations::BooleanPositionalArgInFunctionDefinition,
437438
FBT002 => violations::BooleanDefaultValueInFunctionDefinition,

src/violations.rs

+22
Original file line numberDiff line numberDiff line change
@@ -4704,6 +4704,28 @@ impl AlwaysAutofixableViolation for CommentedOutCode {
47044704

47054705
// flake8-bandit
47064706

4707+
define_violation!(
4708+
pub struct Jinja2AutoescapeFalse(pub bool);
4709+
);
4710+
impl Violation for Jinja2AutoescapeFalse {
4711+
fn message(&self) -> String {
4712+
let Jinja2AutoescapeFalse(value) = self;
4713+
match value {
4714+
true => "Using jinja2 templates with `autoescape=False` is dangerous and can lead to \
4715+
XSS. Ensure `autoescape=True` or use the `select_autoescape` function."
4716+
.to_string(),
4717+
false => "By default, jinja2 sets `autoescape` to `False`. Consider using \
4718+
`autoescape=True` or the `select_autoescape` function to mitigate XSS \
4719+
vulnerabilities."
4720+
.to_string(),
4721+
}
4722+
}
4723+
4724+
fn placeholder() -> Self {
4725+
Jinja2AutoescapeFalse(false)
4726+
}
4727+
}
4728+
47074729
define_violation!(
47084730
pub struct AssertUsed;
47094731
);

0 commit comments

Comments
 (0)