codelaw-rules-security-python 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codelaw_rules_security_python/LICENSE +23 -0
- codelaw_rules_security_python/NOTICE +19 -0
- codelaw_rules_security_python/README.md +239 -0
- codelaw_rules_security_python/codelaw-pack.toml +6 -0
- codelaw_rules_security_python/corpus.toml +65 -0
- codelaw_rules_security_python/data/sources/django.json +17 -0
- codelaw_rules_security_python/data/sources/fastapi.json +10 -0
- codelaw_rules_security_python/data/sources/flask.json +14 -0
- codelaw_rules_security_python/presets/sec-py/recommended.toml +60 -0
- codelaw_rules_security_python/presets/sec-py/strict.toml +8 -0
- codelaw_rules_security_python/rules/sec-py/cookie-flags.cases.md +61 -0
- codelaw_rules_security_python/rules/sec-py/cookie-flags.md +95 -0
- codelaw_rules_security_python/rules/sec-py/django-settings.cases.md +61 -0
- codelaw_rules_security_python/rules/sec-py/django-settings.md +68 -0
- codelaw_rules_security_python/rules/sec-py/fastapi-cors.cases.md +31 -0
- codelaw_rules_security_python/rules/sec-py/fastapi-cors.md +46 -0
- codelaw_rules_security_python/rules/sec-py/flask-debug.cases.md +31 -0
- codelaw_rules_security_python/rules/sec-py/flask-debug.md +59 -0
- codelaw_rules_security_python/rules/sec-py/flask-settings.cases.md +51 -0
- codelaw_rules_security_python/rules/sec-py/flask-settings.md +84 -0
- codelaw_rules_security_python/rules/sec-py/hf-pin-revision.cases.md +51 -0
- codelaw_rules_security_python/rules/sec-py/hf-pin-revision.md +58 -0
- codelaw_rules_security_python/rules/sec-py/jinja-autoescape.cases.md +62 -0
- codelaw_rules_security_python/rules/sec-py/jinja-autoescape.md +80 -0
- codelaw_rules_security_python/rules/sec-py/logging-listen.cases.md +31 -0
- codelaw_rules_security_python/rules/sec-py/logging-listen.md +47 -0
- codelaw_rules_security_python/rules/sec-py/ml-unsafe-load.cases.md +61 -0
- codelaw_rules_security_python/rules/sec-py/ml-unsafe-load.md +68 -0
- codelaw_rules_security_python/rules/sec-py/no-assert-for-auth.cases.md +31 -0
- codelaw_rules_security_python/rules/sec-py/no-assert-for-auth.md +51 -0
- codelaw_rules_security_python/rules/sec-py/no-bind-all-interfaces.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/no-bind-all-interfaces.md +75 -0
- codelaw_rules_security_python/rules/sec-py/no-cleartext-http.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/no-cleartext-http.md +56 -0
- codelaw_rules_security_python/rules/sec-py/no-csrf-exempt.cases.md +49 -0
- codelaw_rules_security_python/rules/sec-py/no-csrf-exempt.md +47 -0
- codelaw_rules_security_python/rules/sec-py/no-dynamic-import.cases.md +35 -0
- codelaw_rules_security_python/rules/sec-py/no-dynamic-import.md +51 -0
- codelaw_rules_security_python/rules/sec-py/no-exec-eval.cases.md +51 -0
- codelaw_rules_security_python/rules/sec-py/no-exec-eval.md +56 -0
- codelaw_rules_security_python/rules/sec-py/no-format-string-injection.cases.md +35 -0
- codelaw_rules_security_python/rules/sec-py/no-format-string-injection.md +44 -0
- codelaw_rules_security_python/rules/sec-py/no-hardcoded-crypto-key.cases.md +51 -0
- codelaw_rules_security_python/rules/sec-py/no-hardcoded-crypto-key.md +91 -0
- codelaw_rules_security_python/rules/sec-py/no-hardcoded-secret.cases.md +61 -0
- codelaw_rules_security_python/rules/sec-py/no-hardcoded-secret.md +82 -0
- codelaw_rules_security_python/rules/sec-py/no-header-injection.cases.md +45 -0
- codelaw_rules_security_python/rules/sec-py/no-header-injection.md +55 -0
- codelaw_rules_security_python/rules/sec-py/no-insecure-protocol.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/no-insecure-protocol.md +52 -0
- codelaw_rules_security_python/rules/sec-py/no-insecure-random.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/no-insecure-random.md +67 -0
- codelaw_rules_security_python/rules/sec-py/no-insecure-tempfile.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/no-insecure-tempfile.md +62 -0
- codelaw_rules_security_python/rules/sec-py/no-ldap-injection.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/no-ldap-injection.md +51 -0
- codelaw_rules_security_python/rules/sec-py/no-llm-output-to-sink.cases.md +31 -0
- codelaw_rules_security_python/rules/sec-py/no-llm-output-to-sink.md +55 -0
- codelaw_rules_security_python/rules/sec-py/no-log-injection.cases.md +45 -0
- codelaw_rules_security_python/rules/sec-py/no-log-injection.md +52 -0
- codelaw_rules_security_python/rules/sec-py/no-mako.cases.md +37 -0
- codelaw_rules_security_python/rules/sec-py/no-mako.md +52 -0
- codelaw_rules_security_python/rules/sec-py/no-mark-safe.cases.md +51 -0
- codelaw_rules_security_python/rules/sec-py/no-mark-safe.md +54 -0
- codelaw_rules_security_python/rules/sec-py/no-mass-assignment.cases.md +55 -0
- codelaw_rules_security_python/rules/sec-py/no-mass-assignment.md +58 -0
- codelaw_rules_security_python/rules/sec-py/no-nosql-injection.cases.md +34 -0
- codelaw_rules_security_python/rules/sec-py/no-nosql-injection.md +56 -0
- codelaw_rules_security_python/rules/sec-py/no-open-redirect.cases.md +35 -0
- codelaw_rules_security_python/rules/sec-py/no-open-redirect.md +51 -0
- codelaw_rules_security_python/rules/sec-py/no-partial-path.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/no-partial-path.md +61 -0
- codelaw_rules_security_python/rules/sec-py/no-path-traversal.cases.md +46 -0
- codelaw_rules_security_python/rules/sec-py/no-path-traversal.md +54 -0
- codelaw_rules_security_python/rules/sec-py/no-prompt-injection.cases.md +46 -0
- codelaw_rules_security_python/rules/sec-py/no-prompt-injection.md +65 -0
- codelaw_rules_security_python/rules/sec-py/no-pycrypto.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/no-pycrypto.md +47 -0
- codelaw_rules_security_python/rules/sec-py/no-reflected-xss.cases.md +45 -0
- codelaw_rules_security_python/rules/sec-py/no-reflected-xss.md +56 -0
- codelaw_rules_security_python/rules/sec-py/no-regex-injection.cases.md +33 -0
- codelaw_rules_security_python/rules/sec-py/no-regex-injection.md +49 -0
- codelaw_rules_security_python/rules/sec-py/no-secret-in-log.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/no-secret-in-log.md +55 -0
- codelaw_rules_security_python/rules/sec-py/no-shell.cases.md +55 -0
- codelaw_rules_security_python/rules/sec-py/no-shell.md +63 -0
- codelaw_rules_security_python/rules/sec-py/no-silent-except.cases.md +53 -0
- codelaw_rules_security_python/rules/sec-py/no-silent-except.md +54 -0
- codelaw_rules_security_python/rules/sec-py/no-sql-string.cases.md +91 -0
- codelaw_rules_security_python/rules/sec-py/no-sql-string.md +78 -0
- codelaw_rules_security_python/rules/sec-py/no-ssrf.cases.md +34 -0
- codelaw_rules_security_python/rules/sec-py/no-ssrf.md +54 -0
- codelaw_rules_security_python/rules/sec-py/no-stack-trace-response.cases.md +48 -0
- codelaw_rules_security_python/rules/sec-py/no-stack-trace-response.md +67 -0
- codelaw_rules_security_python/rules/sec-py/no-tls-bypass.cases.md +71 -0
- codelaw_rules_security_python/rules/sec-py/no-tls-bypass.md +66 -0
- codelaw_rules_security_python/rules/sec-py/no-trust-remote-code.cases.md +31 -0
- codelaw_rules_security_python/rules/sec-py/no-trust-remote-code.md +50 -0
- codelaw_rules_security_python/rules/sec-py/no-unsafe-deserialize.cases.md +63 -0
- codelaw_rules_security_python/rules/sec-py/no-unsafe-deserialize.md +51 -0
- codelaw_rules_security_python/rules/sec-py/no-weak-cipher.cases.md +51 -0
- codelaw_rules_security_python/rules/sec-py/no-weak-cipher.md +60 -0
- codelaw_rules_security_python/rules/sec-py/no-weak-hash.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/no-weak-hash.md +54 -0
- codelaw_rules_security_python/rules/sec-py/no-wildcard-injection.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/no-wildcard-injection.md +53 -0
- codelaw_rules_security_python/rules/sec-py/no-world-writable.cases.md +51 -0
- codelaw_rules_security_python/rules/sec-py/no-world-writable.md +57 -0
- codelaw_rules_security_python/rules/sec-py/no-xml-unsafe-parser.cases.md +83 -0
- codelaw_rules_security_python/rules/sec-py/no-xml-unsafe-parser.md +86 -0
- codelaw_rules_security_python/rules/sec-py/no-xmlrpc.cases.md +31 -0
- codelaw_rules_security_python/rules/sec-py/no-xmlrpc.md +50 -0
- codelaw_rules_security_python/rules/sec-py/no-xpath-injection.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/no-xpath-injection.md +52 -0
- codelaw_rules_security_python/rules/sec-py/paramiko-host-key.cases.md +21 -0
- codelaw_rules_security_python/rules/sec-py/paramiko-host-key.md +44 -0
- codelaw_rules_security_python/rules/sec-py/password-hash-effort.cases.md +61 -0
- codelaw_rules_security_python/rules/sec-py/password-hash-effort.md +74 -0
- codelaw_rules_security_python/rules/sec-py/requests-timeout.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/requests-timeout.md +63 -0
- codelaw_rules_security_python/rules/sec-py/tarfile-filter.cases.md +51 -0
- codelaw_rules_security_python/rules/sec-py/tarfile-filter.md +71 -0
- codelaw_rules_security_python/rules/sec-py/torch-load-weights-only.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/torch-load-weights-only.md +73 -0
- codelaw_rules_security_python/rules/sec-py/upload-secure-filename.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/upload-secure-filename.md +65 -0
- codelaw_rules_security_python/rules/sec-py/urlopen-scheme.cases.md +41 -0
- codelaw_rules_security_python/rules/sec-py/urlopen-scheme.md +54 -0
- codelaw_rules_security_python/rules/sec-py/weak-key-size.cases.md +51 -0
- codelaw_rules_security_python/rules/sec-py/weak-key-size.md +82 -0
- codelaw_rules_security_python/rules/sec-py/yaml-safe-load.cases.md +67 -0
- codelaw_rules_security_python/rules/sec-py/yaml-safe-load.md +51 -0
- codelaw_rules_security_python-0.3.0.dist-info/METADATA +249 -0
- codelaw_rules_security_python-0.3.0.dist-info/RECORD +136 -0
- codelaw_rules_security_python-0.3.0.dist-info/WHEEL +4 -0
- codelaw_rules_security_python-0.3.0.dist-info/licenses/LICENSE +23 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# sec-py/no-exec-eval — cases
|
|
2
|
+
|
|
3
|
+
### case: a literal string
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
eval("1 + 1")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
(none)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: an f-string
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
exec(f"import {name}")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-exec-eval
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: exec with a namespace
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
exec(code, {})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:1 sec-py/no-exec-eval
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: a method named eval
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
model.eval()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### case: a name
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
compile(source, 'f.py', 'exec')
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
1:1 sec-py/no-exec-eval
|
|
51
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-exec-eval
|
|
3
|
+
title: "`exec`, `eval` and `compile` of a non-literal"
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the safe replacement (`ast.literal_eval`, a lookup table, a parser) depends on what the string is for"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B102, bandit/B307]
|
|
9
|
+
security: { cwe: [94], owasp: ["A05:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`exec`, `eval` and `compile` are not given a value that is not a string literal.
|
|
14
|
+
|
|
15
|
+
**Why:** The string is run as code, so whoever controls it controls the program (CWE-94).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- { pattern: 'exec($X)' }
|
|
21
|
+
- { pattern: 'exec($X, $$$R)' }
|
|
22
|
+
- { pattern: 'eval($X)' }
|
|
23
|
+
- { pattern: 'eval($X, $$$R)' }
|
|
24
|
+
- all:
|
|
25
|
+
- { pattern: 'compile($X, $$$R)' }
|
|
26
|
+
- not:
|
|
27
|
+
inside:
|
|
28
|
+
kind: argument_list
|
|
29
|
+
inside: { kind: call, has: { field: function, regex: '^(exec|eval)$' } }
|
|
30
|
+
constraints: { X: { not: { kind: string, not: { has: { kind: interpolation } } } } }
|
|
31
|
+
message: '`$X` is run as code; use `ast.literal_eval` or a lookup table'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## How to fix
|
|
35
|
+
|
|
36
|
+
1. For a literal (a number, a list, a dict), call `ast.literal_eval`.
|
|
37
|
+
2. For a choice among behaviours, map the input to a function through a dict.
|
|
38
|
+
3. For arithmetic or expressions, use a parser built for it, not the interpreter.
|
|
39
|
+
|
|
40
|
+
## Examples
|
|
41
|
+
|
|
42
|
+
### Python
|
|
43
|
+
|
|
44
|
+
#### Don't
|
|
45
|
+
|
|
46
|
+
```py
|
|
47
|
+
result = eval(user_text)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### Do
|
|
51
|
+
|
|
52
|
+
```py
|
|
53
|
+
import ast
|
|
54
|
+
|
|
55
|
+
result = ast.literal_eval(user_text)
|
|
56
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# sec-py/no-format-string-injection — cases
|
|
2
|
+
|
|
3
|
+
### case: a request template
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
def g(request):
|
|
7
|
+
return request.GET["t"].format(user)
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```expect
|
|
11
|
+
2:12 sec-py/no-format-string-injection
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### case: a literal template
|
|
15
|
+
|
|
16
|
+
```py
|
|
17
|
+
def g(request):
|
|
18
|
+
return "Hello {}".format(request.GET["n"])
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```expect
|
|
22
|
+
(none)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### case: format_map
|
|
26
|
+
|
|
27
|
+
```py
|
|
28
|
+
def g(request):
|
|
29
|
+
t = request.args["t"]
|
|
30
|
+
return t.format_map(d)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```expect
|
|
34
|
+
3:12 sec-py/no-format-string-injection
|
|
35
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-format-string-injection
|
|
3
|
+
title: 'A request value is not a format string'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the template has to be a literal, which needs the format string moved out of the request"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [134], owasp: ["A05:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
A value from the request is not the receiver of `.format`.
|
|
13
|
+
|
|
14
|
+
**Why:** A field such as `{obj.__class__.__init__.__globals__[SECRET]}` reads attributes of the arguments, so the client reads secrets (CWE-134). This rule uses `from_source:`, which is not taint analysis: it sees a request value only in the function that reads it, never through a call or a parameter.
|
|
15
|
+
|
|
16
|
+
```codelaw
|
|
17
|
+
match:
|
|
18
|
+
any: [{ pattern: '$V.format($$$A)' }, { pattern: '$V.format_map($$$A)' }]
|
|
19
|
+
where: { $V: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
|
|
20
|
+
message: '`$V` comes from the request and is used as a format string'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How to fix
|
|
24
|
+
|
|
25
|
+
1. Make the template a literal and pass the request value as an argument: `"Hello {}".format(name)`.
|
|
26
|
+
2. For client-written templates use `string.Template(...).safe_substitute(...)`, which has no attribute access.
|
|
27
|
+
|
|
28
|
+
## Examples
|
|
29
|
+
|
|
30
|
+
### Python
|
|
31
|
+
|
|
32
|
+
#### Don't
|
|
33
|
+
|
|
34
|
+
```py
|
|
35
|
+
def greet(request):
|
|
36
|
+
return request.GET["tpl"].format(user)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### Do
|
|
40
|
+
|
|
41
|
+
```py
|
|
42
|
+
def greet(request):
|
|
43
|
+
return "Hello {}".format(request.GET["name"])
|
|
44
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# sec-py/no-hardcoded-crypto-key — cases
|
|
2
|
+
|
|
3
|
+
### case: Fernet
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
Fernet(b"abc")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-hardcoded-crypto-key
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: AES
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
AES.new(b"0123456789abcdef", AES.MODE_GCM)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-hardcoded-crypto-key
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: a key from the environment
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
Fernet(os.environ["K"])
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: a fixed IV
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
Cipher(alg, modes.CBC(b"0000000000000000"))
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
1:13 sec-py/no-hardcoded-crypto-key
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### case: a keyword key
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
sign(key="secret-key")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
1:1 sec-py/no-hardcoded-crypto-key
|
|
51
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-hardcoded-crypto-key
|
|
3
|
+
title: 'No keys or IVs in the source'
|
|
4
|
+
languages: [python]
|
|
5
|
+
exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
|
|
6
|
+
fix: manual
|
|
7
|
+
fix_reason: "where the key comes from (a key store, an environment variable) is a deployment decision"
|
|
8
|
+
stability: preview
|
|
9
|
+
security: { cwe: [321, 329], owasp: ["A04:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
A cipher key, HMAC key, IV or nonce is not a literal.
|
|
14
|
+
|
|
15
|
+
**Why:** A key in the source is known to everyone who can read the source, and a fixed IV or nonce breaks the cipher's guarantees (CWE-321, CWE-329).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- kind: call
|
|
21
|
+
all:
|
|
22
|
+
- has:
|
|
23
|
+
field: function
|
|
24
|
+
regex: '^((Crypto|Cryptodome)\.Cipher\.)?(AES|DES|DES3|ChaCha20|Blowfish|ARC4|Salsa20)\.new$|^(Fernet|hmac\.new|ChaCha20Poly1305|AESGCM|AESCCM)$'
|
|
25
|
+
- has:
|
|
26
|
+
field: arguments
|
|
27
|
+
has: { nthChild: 1, kind: string, not: { has: { kind: interpolation } } }
|
|
28
|
+
- kind: call
|
|
29
|
+
all:
|
|
30
|
+
- { has: { field: function, regex: '^(modes\.)?(CBC|CTR|GCM|CFB|OFB|CCM)$' } }
|
|
31
|
+
- has:
|
|
32
|
+
field: arguments
|
|
33
|
+
has: { nthChild: 1, kind: string, not: { has: { kind: interpolation } } }
|
|
34
|
+
- all:
|
|
35
|
+
- { kind: call }
|
|
36
|
+
- has:
|
|
37
|
+
field: arguments
|
|
38
|
+
has:
|
|
39
|
+
kind: keyword_argument
|
|
40
|
+
all:
|
|
41
|
+
- { has: { field: name, regex: '^iv$' } }
|
|
42
|
+
- { has: { field: value, kind: string, not: { has: { kind: interpolation } } } }
|
|
43
|
+
- all:
|
|
44
|
+
- { kind: call }
|
|
45
|
+
- has:
|
|
46
|
+
field: arguments
|
|
47
|
+
has:
|
|
48
|
+
kind: keyword_argument
|
|
49
|
+
all:
|
|
50
|
+
- { has: { field: name, regex: '^nonce$' } }
|
|
51
|
+
- { has: { field: value, kind: string, not: { has: { kind: interpolation } } } }
|
|
52
|
+
- all:
|
|
53
|
+
- { kind: call }
|
|
54
|
+
- has:
|
|
55
|
+
field: function
|
|
56
|
+
regex: '(?i)(cipher|crypt|aes|fernet|hmac|jwt|sign|cmac|pbkdf|scrypt)'
|
|
57
|
+
- has:
|
|
58
|
+
field: arguments
|
|
59
|
+
has:
|
|
60
|
+
kind: keyword_argument
|
|
61
|
+
all:
|
|
62
|
+
- { has: { field: name, regex: '^key$' } }
|
|
63
|
+
- { has: { field: value, kind: string, not: { has: { kind: interpolation } } } }
|
|
64
|
+
message: 'A key, IV or nonce is a literal; load the key from a key store and make the IV random'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## How to fix
|
|
68
|
+
|
|
69
|
+
1. Generate keys with `os.urandom` or the library (`Fernet.generate_key()`) once, and store them in a secret manager or an environment variable.
|
|
70
|
+
2. Make a fresh random IV or nonce for every message (`os.urandom(12)`) and store it beside the ciphertext.
|
|
71
|
+
|
|
72
|
+
## Examples
|
|
73
|
+
|
|
74
|
+
### Python
|
|
75
|
+
|
|
76
|
+
#### Don't
|
|
77
|
+
|
|
78
|
+
```py
|
|
79
|
+
from cryptography.fernet import Fernet
|
|
80
|
+
|
|
81
|
+
f = Fernet(b"ZmFrZWtleWZha2VrZXlmYWtla2V5ZmFrZWtleTA9")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### Do
|
|
85
|
+
|
|
86
|
+
```py
|
|
87
|
+
import os
|
|
88
|
+
from cryptography.fernet import Fernet
|
|
89
|
+
|
|
90
|
+
f = Fernet(os.environ["FERNET_KEY"])
|
|
91
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# sec-py/no-hardcoded-secret — cases
|
|
2
|
+
|
|
3
|
+
### case: a password assignment
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
password = "s3cr3tvalue"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-hardcoded-secret
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: a keyword argument
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
connect(host, password="s3cr3tvalue")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:15 sec-py/no-hardcoded-secret
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: a placeholder is short
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
password = ""
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: an environment read
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
password = os.environ["PASSWORD"]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### case: an AWS key
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
key = "AKIAIOSFODNN7EXAMPLE"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
1:7 sec-py/no-hardcoded-secret
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### case: a name that is not a secret
|
|
54
|
+
|
|
55
|
+
```py
|
|
56
|
+
title = "A long title string"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```expect
|
|
60
|
+
(none)
|
|
61
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-hardcoded-secret
|
|
3
|
+
title: 'No secrets in the source'
|
|
4
|
+
languages: [python]
|
|
5
|
+
exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
|
|
6
|
+
fix: manual
|
|
7
|
+
fix_reason: "where the secret lives instead (environment, secret manager) is a decision, and the leaked one must be rotated"
|
|
8
|
+
stability: preview
|
|
9
|
+
ported_from: [bandit/B105, bandit/B106, bandit/B107, ruff/S105, ruff/S106, ruff/S107]
|
|
10
|
+
security: { cwe: [798], owasp: ["A07:2025"], severity: high }
|
|
11
|
+
since: 0.2.0
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
A password, key or token is not a string literal in the code, and a cloud access key is not pasted into it.
|
|
15
|
+
|
|
16
|
+
**Why:** The source goes to every clone, build log and backup, so the secret does too, and revoking it means a release (CWE-798).
|
|
17
|
+
|
|
18
|
+
```codelaw
|
|
19
|
+
match:
|
|
20
|
+
any:
|
|
21
|
+
- all:
|
|
22
|
+
- { kind: assignment }
|
|
23
|
+
- has:
|
|
24
|
+
field: left
|
|
25
|
+
regex: '(?i)^_*[a-z0-9_]*(pass(word|wd)?|secret|api_?key|private_?key|token|credentials?|auth_?key|access_?key)[a-z0-9_]*$'
|
|
26
|
+
not:
|
|
27
|
+
regex: '(?i)(_type|_url|_uri|_path|_file|_header|_name|_field|_class|_backend|_func|_label|_prefix)$'
|
|
28
|
+
- has:
|
|
29
|
+
field: right
|
|
30
|
+
kind: string
|
|
31
|
+
regex: '^[rb]?("[^"]{4,}"|''[^'']{4,}'')$'
|
|
32
|
+
not:
|
|
33
|
+
any:
|
|
34
|
+
- { has: { kind: interpolation } }
|
|
35
|
+
- { regex: '^[rb]?[''"][*x#.\-]+[''"]$' }
|
|
36
|
+
- { regex: '^[rb]?[''"][A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+[''"]$' }
|
|
37
|
+
- all:
|
|
38
|
+
- { kind: keyword_argument }
|
|
39
|
+
- has:
|
|
40
|
+
field: name
|
|
41
|
+
regex: '(?i)^_*[a-z0-9_]*(pass(word|wd)?|secret|api_?key|private_?key|token|credentials?|auth_?key|access_?key)[a-z0-9_]*$'
|
|
42
|
+
not:
|
|
43
|
+
regex: '(?i)(_type|_url|_uri|_path|_file|_header|_name|_field|_class|_backend|_func|_label|_prefix)$'
|
|
44
|
+
- has:
|
|
45
|
+
field: value
|
|
46
|
+
kind: string
|
|
47
|
+
regex: '^[rb]?("[^"]{4,}"|''[^'']{4,}'')$'
|
|
48
|
+
not:
|
|
49
|
+
any:
|
|
50
|
+
- { has: { kind: interpolation } }
|
|
51
|
+
- { regex: '^[rb]?[''"][*x#.\-]+[''"]$' }
|
|
52
|
+
- { regex: '^[rb]?[''"][A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+[''"]$' }
|
|
53
|
+
- { kind: string, regex: '^[''"](AKIA|ASIA)[0-9A-Z]{16}[''"]$' }
|
|
54
|
+
- { kind: string, regex: '^[''"](ghp|gho|ghs|ghu)_[A-Za-z0-9]{36,}[''"]$' }
|
|
55
|
+
- { kind: string, regex: '^[''"]xox[abprs]-[A-Za-z0-9-]{10,}[''"]$' }
|
|
56
|
+
- { kind: string, regex: '^[''"]sk_live_[A-Za-z0-9]{16,}[''"]$' }
|
|
57
|
+
message: 'A secret is written in the source; read it from the environment or a secret manager'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## How to fix
|
|
61
|
+
|
|
62
|
+
1. Rotate the secret: it is in version control now.
|
|
63
|
+
2. Read it at start-up: `os.environ["DB_PASSWORD"]`, or from your secret manager.
|
|
64
|
+
3. Keep local values in an untracked `.env` file.
|
|
65
|
+
|
|
66
|
+
## Examples
|
|
67
|
+
|
|
68
|
+
### Python
|
|
69
|
+
|
|
70
|
+
#### Don't
|
|
71
|
+
|
|
72
|
+
```py
|
|
73
|
+
DB_PASSWORD = "hunter2-prod"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### Do
|
|
77
|
+
|
|
78
|
+
```py
|
|
79
|
+
import os
|
|
80
|
+
|
|
81
|
+
DB_PASSWORD = os.environ["DB_PASSWORD"]
|
|
82
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# sec-py/no-header-injection — cases
|
|
2
|
+
|
|
3
|
+
### case: a request value
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
def v(request):
|
|
7
|
+
resp.headers["X-A"] = request.args["a"]
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```expect
|
|
11
|
+
2:5 sec-py/no-header-injection
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### case: a constant
|
|
15
|
+
|
|
16
|
+
```py
|
|
17
|
+
def v(request):
|
|
18
|
+
resp.headers["X-A"] = "1"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```expect
|
|
22
|
+
(none)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### case: Django
|
|
26
|
+
|
|
27
|
+
```py
|
|
28
|
+
def v(request):
|
|
29
|
+
response["X-A"] = request.GET["a"]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```expect
|
|
33
|
+
2:5 sec-py/no-header-injection
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### case: a dict is not matched
|
|
37
|
+
|
|
38
|
+
```py
|
|
39
|
+
def v(request):
|
|
40
|
+
d["x"] = request.GET["a"]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```expect
|
|
44
|
+
(none)
|
|
45
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-header-injection
|
|
3
|
+
title: 'A request value is not a response header'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "whether to reject, strip or encode the value depends on what the header is for"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [113], owasp: ["A05:2025"], severity: medium }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
A value from the request is not written into a response header or a cookie value without its line breaks removed.
|
|
13
|
+
|
|
14
|
+
**Why:** A CR or LF in the value ends the header and starts a new one, so an attacker can add headers or a second response (CWE-113). This rule uses `from_source:`, which is not taint analysis: it sees a request value only in the function that reads it, never through a call or a parameter.
|
|
15
|
+
|
|
16
|
+
```codelaw
|
|
17
|
+
match:
|
|
18
|
+
any:
|
|
19
|
+
- { pattern: '$R.headers[$K] = $V' }
|
|
20
|
+
- { pattern: '$R.headers.add($K, $V)' }
|
|
21
|
+
- { pattern: '$R.headers.set($K, $V)' }
|
|
22
|
+
- { pattern: '$R[$K] = $V' }
|
|
23
|
+
- { pattern: '$R.set_cookie($K, $V)' }
|
|
24
|
+
constraints: { R: { regex: '^(resp|response|res|r|rv|rsp|self\.response|make_response\(.*\))$' } }
|
|
25
|
+
where: { $V: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
|
|
26
|
+
message: '`$V` comes from the request and is written into a header'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## How to fix
|
|
30
|
+
|
|
31
|
+
1. Reject a value that contains `\r` or `\n`: `if "\n" in value or "\r" in value: abort(400)`.
|
|
32
|
+
2. Or percent-encode it: `urllib.parse.quote(value)`.
|
|
33
|
+
3. Modern servers and frameworks refuse newlines in headers; do not rely on that.
|
|
34
|
+
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
### Python
|
|
38
|
+
|
|
39
|
+
#### Don't
|
|
40
|
+
|
|
41
|
+
```py
|
|
42
|
+
def view(request):
|
|
43
|
+
resp = make_response("ok")
|
|
44
|
+
resp.headers["X-Name"] = request.args["name"]
|
|
45
|
+
return resp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Do
|
|
49
|
+
|
|
50
|
+
```py
|
|
51
|
+
def view(request):
|
|
52
|
+
resp = make_response("ok")
|
|
53
|
+
resp.headers["X-Name"] = quote(request.args["name"])
|
|
54
|
+
return resp
|
|
55
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sec-py/no-insecure-protocol — cases
|
|
2
|
+
|
|
3
|
+
### case: an import
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
import telnetlib
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-insecure-protocol
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: a from import
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
from ftplib import FTP
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-insecure-protocol
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: SNMP
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
CommunityData("public")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:1 sec-py/no-insecure-protocol
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: ssh
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
import paramiko
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-insecure-protocol
|
|
3
|
+
title: 'No Telnet, FTP or SNMP v1/v2'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the replacement (SSH or SFTP, FTPS, SNMPv3) needs the server to support it"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B312, bandit/B321, bandit/B401, bandit/B402, bandit/B508, bandit/B509, ruff/S401, ruff/S402, ruff/S312, ruff/S321, ruff/S508, ruff/S509]
|
|
9
|
+
security: { cwe: [319], owasp: ["A04:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`telnetlib`, `ftplib` and SNMP community strings (v1/v2c) are not used.
|
|
14
|
+
|
|
15
|
+
**Why:** These send credentials and data in clear text, so anyone on the path reads and changes them (CWE-319).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- all: [{ kind: import_statement }, { has: { field: name, regex: '^(telnetlib|ftplib)$' } }]
|
|
21
|
+
- all:
|
|
22
|
+
- { kind: import_from_statement }
|
|
23
|
+
- { has: { field: module_name, regex: '^(telnetlib|ftplib)$' } }
|
|
24
|
+
- kind: call
|
|
25
|
+
all: [{ has: { field: function, regex: '^(pysnmp\.hlapi\.)?CommunityData$' } }]
|
|
26
|
+
message: 'This protocol sends credentials in clear text'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## How to fix
|
|
30
|
+
|
|
31
|
+
1. Use SSH or SFTP (`paramiko`, `asyncssh`) instead of Telnet and FTP, or `ftplib.FTP_TLS` for FTPS.
|
|
32
|
+
2. Use SNMPv3 (`UsmUserData` with authentication and privacy) instead of a community string.
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
### Python
|
|
37
|
+
|
|
38
|
+
#### Don't
|
|
39
|
+
|
|
40
|
+
```py
|
|
41
|
+
import ftplib
|
|
42
|
+
|
|
43
|
+
ftp = ftplib.FTP(host)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Do
|
|
47
|
+
|
|
48
|
+
```py
|
|
49
|
+
import paramiko
|
|
50
|
+
|
|
51
|
+
client = paramiko.SSHClient()
|
|
52
|
+
```
|