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,41 @@
|
|
|
1
|
+
# sec-py/no-insecure-random — cases
|
|
2
|
+
|
|
3
|
+
### case: a token
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
session_token = random.randint(0, 10**9)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-insecure-random
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: a password
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
password = random.choice(chars)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-insecure-random
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: a dice roll
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
roll = random.randint(1, 6)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: secrets
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
token = secrets.token_hex(16)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-insecure-random
|
|
3
|
+
title: "`secrets` for tokens, not `random`"
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: suggest
|
|
6
|
+
fix_reason: "`secrets` has different functions (`token_hex`, `choice`, `randbelow`), so the call has to be chosen, not renamed"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B311, ruff/S311]
|
|
9
|
+
security: { cwe: [338], owasp: ["A04:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
A password, token, key, nonce or code is not made with the `random` module.
|
|
14
|
+
|
|
15
|
+
**Why:** `random` is a predictable generator, so a few outputs reveal the rest (CWE-338).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
all:
|
|
20
|
+
- { kind: assignment }
|
|
21
|
+
- has:
|
|
22
|
+
field: left
|
|
23
|
+
regex: '(?i)(pass(word|wd)?|secret|token|api_?key|auth|otp|nonce|salt|session_?id|csrf|reset_?code|verification_?code)'
|
|
24
|
+
- has:
|
|
25
|
+
field: right
|
|
26
|
+
kind: call
|
|
27
|
+
pattern: $R
|
|
28
|
+
has:
|
|
29
|
+
field: function
|
|
30
|
+
regex: '^(random\.)?(random|randint|randrange|choice|choices|getrandbits|sample|randbytes|uniform)$'
|
|
31
|
+
message: 'This secret comes from `random`; use `secrets`'
|
|
32
|
+
fix:
|
|
33
|
+
- { replace: { target: $R, with: 'secrets.token_hex(16)' } }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## How to fix
|
|
37
|
+
|
|
38
|
+
1. Use `secrets.token_urlsafe(32)` or `secrets.token_hex(16)` for a token.
|
|
39
|
+
2. Use `secrets.choice` for a character picked from an alphabet, `secrets.randbelow(n)` for a number.
|
|
40
|
+
|
|
41
|
+
## Examples
|
|
42
|
+
|
|
43
|
+
### Python
|
|
44
|
+
|
|
45
|
+
#### Don't
|
|
46
|
+
|
|
47
|
+
```py
|
|
48
|
+
import random
|
|
49
|
+
|
|
50
|
+
reset_token = random.getrandbits(64)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### Do
|
|
54
|
+
|
|
55
|
+
```py
|
|
56
|
+
import secrets
|
|
57
|
+
|
|
58
|
+
reset_token = secrets.token_hex(16)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### Fixed
|
|
62
|
+
|
|
63
|
+
```py
|
|
64
|
+
import random
|
|
65
|
+
|
|
66
|
+
reset_token = secrets.token_hex(16)
|
|
67
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sec-py/no-insecure-tempfile — cases
|
|
2
|
+
|
|
3
|
+
### case: mktemp
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
tempfile.mktemp(suffix=".txt")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-insecure-tempfile
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: a fixed /tmp path
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
open("/tmp/report.txt", "w")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:6 sec-py/no-insecure-tempfile
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: mkstemp
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
tempfile.mkstemp()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: a path outside /tmp
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
open("/var/data/report.txt", "w")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-insecure-tempfile
|
|
3
|
+
title: "`mkstemp` instead of `mktemp`"
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: suggest
|
|
6
|
+
fix_reason: "`mkstemp` returns an open file descriptor and a path, not just a name, so the surrounding code changes"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B306, bandit/B108, bandit/B325, ruff/S306, ruff/S108]
|
|
9
|
+
security: { cwe: [377], owasp: ["A01:2025"], severity: medium }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Temporary files are made with `tempfile.mkstemp`, `NamedTemporaryFile` or `TemporaryDirectory`, not `mktemp`, and not at a fixed path under `/tmp`.
|
|
14
|
+
|
|
15
|
+
**Why:** `mktemp` returns a name without creating the file, so another user can create it first and the program then writes into a file they control (CWE-377).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- { pattern: 'tempfile.mktemp($$$A)' }
|
|
21
|
+
- { pattern: 'os.tempnam($$$A)' }
|
|
22
|
+
- { pattern: 'os.tmpnam()' }
|
|
23
|
+
- kind: string
|
|
24
|
+
regex: '^[''"]/(var/)?tmp/[^''"{}]*[''"]$'
|
|
25
|
+
inside: { kind: call, stopBy: end }
|
|
26
|
+
message: 'A predictable temporary file name; use `tempfile.mkstemp` or `NamedTemporaryFile`'
|
|
27
|
+
fix: 'tempfile.mkstemp($$$A)[1]'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## How to fix
|
|
31
|
+
|
|
32
|
+
1. Use `with tempfile.NamedTemporaryFile(delete=False) as f:` and write through `f`.
|
|
33
|
+
2. Use `tempfile.mkdtemp()` or `TemporaryDirectory()` for a directory.
|
|
34
|
+
3. Never build a path under `/tmp` by hand.
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
### Python
|
|
39
|
+
|
|
40
|
+
#### Don't
|
|
41
|
+
|
|
42
|
+
```py
|
|
43
|
+
import tempfile
|
|
44
|
+
|
|
45
|
+
path = tempfile.mktemp()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Do
|
|
49
|
+
|
|
50
|
+
```py
|
|
51
|
+
import tempfile
|
|
52
|
+
|
|
53
|
+
path = tempfile.mkstemp()[1]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### Fixed
|
|
57
|
+
|
|
58
|
+
```py
|
|
59
|
+
import tempfile
|
|
60
|
+
|
|
61
|
+
path = tempfile.mkstemp()[1]
|
|
62
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sec-py/no-ldap-injection — cases
|
|
2
|
+
|
|
3
|
+
### case: an f-string filter
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
conn.search_s(base, 2, f"(uid={name})")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-ldap-injection
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: ldap3
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
conn.search(base, "(cn=" + name + ")")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-ldap-injection
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: a fixed filter
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
conn.search(base, "(objectClass=*)")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: a re.search is not matched
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
re.search("a", text)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-ldap-injection
|
|
3
|
+
title: 'LDAP filters are not built from strings'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the escaping function belongs to the LDAP library in use"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [90], owasp: ["A05:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
An LDAP search filter is not made with an f-string, `%`, `+` or `.format`.
|
|
13
|
+
|
|
14
|
+
**Why:** A value with `*`, `)` or `(` in it changes which entries the filter matches, which can turn a login check into a match-all (CWE-90).
|
|
15
|
+
|
|
16
|
+
```codelaw
|
|
17
|
+
match:
|
|
18
|
+
kind: call
|
|
19
|
+
all:
|
|
20
|
+
- has: { field: function, regex: '\.(search|search_s|search_st|search_ext|search_ext_s)$' }
|
|
21
|
+
- has:
|
|
22
|
+
field: arguments
|
|
23
|
+
has:
|
|
24
|
+
regex: '\(.*='
|
|
25
|
+
any:
|
|
26
|
+
- { kind: string, has: { kind: interpolation } }
|
|
27
|
+
- { kind: binary_operator, has: { field: operator, regex: '^(%|\+)$' } }
|
|
28
|
+
- { kind: call, has: { field: function, regex: '\.format$' } }
|
|
29
|
+
message: 'This LDAP filter is built from a string; escape the values'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## How to fix
|
|
33
|
+
|
|
34
|
+
1. Escape each value: `ldap3.utils.conv.escape_filter_chars(value)` or `ldap.filter.escape_filter_chars(value)`.
|
|
35
|
+
2. Or build the filter with `ldap.filter.filter_format("(uid=%s)", [value])`.
|
|
36
|
+
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
39
|
+
### Python
|
|
40
|
+
|
|
41
|
+
#### Don't
|
|
42
|
+
|
|
43
|
+
```py
|
|
44
|
+
conn.search_s(base, ldap.SCOPE_SUBTREE, f"(uid={name})")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
#### Do
|
|
48
|
+
|
|
49
|
+
```py
|
|
50
|
+
conn.search_s(base, ldap.SCOPE_SUBTREE, filter_format("(uid=%s)", [name]))
|
|
51
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# sec-py/no-llm-output-to-sink — cases
|
|
2
|
+
|
|
3
|
+
### case: exec of a completion
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
exec(completion.choices[0].message.content)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-llm-output-to-sink
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: a subprocess
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
subprocess.run(llm_output, shell=True)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-llm-output-to-sink
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: a lookup
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
ACTIONS[reply]()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-llm-output-to-sink
|
|
3
|
+
title: 'Model output is not run or rendered'
|
|
4
|
+
languages: [python]
|
|
5
|
+
severity: warn
|
|
6
|
+
fix: manual
|
|
7
|
+
fix_reason: "what to do with the output instead (parse it against a schema, pick from a fixed list of actions) is a design choice"
|
|
8
|
+
stability: preview
|
|
9
|
+
security: { cwe: [94], owasp: ["A05:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
The reply of an OpenAI, Anthropic or LangChain call does not reach `exec`, `eval`, a subprocess, SQL or `mark_safe`.
|
|
14
|
+
|
|
15
|
+
**Why:** A model's output is shaped by its input, so a prompt written by an attacker becomes code, a command, a query or a script (CWE-94).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
all:
|
|
20
|
+
- { kind: call }
|
|
21
|
+
- has:
|
|
22
|
+
field: function
|
|
23
|
+
regex: '^(exec|eval|os\.system|os\.popen|subprocess\.(run|call|check_call|check_output|Popen)|mark_safe|Markup|(.*\.)?(execute|executescript))$'
|
|
24
|
+
- has:
|
|
25
|
+
field: arguments
|
|
26
|
+
has:
|
|
27
|
+
regex: '(?i)(\.choices\[0\]\.message\.content|\.content\[0\]\.text|\.content\b|\.text\b|llm_output|completion|model_output|llm_response|ai_response)'
|
|
28
|
+
message: 'The output of a language model reaches a code, command, query or HTML sink'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## How to fix
|
|
32
|
+
|
|
33
|
+
1. Ask the model for structured output and validate it against a schema.
|
|
34
|
+
2. Map the answer to a fixed list of actions the program already has; never pass it to a sink as text.
|
|
35
|
+
3. Escape it before it goes into HTML; use parameters for SQL.
|
|
36
|
+
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
39
|
+
### Python
|
|
40
|
+
|
|
41
|
+
#### Don't
|
|
42
|
+
|
|
43
|
+
```py
|
|
44
|
+
reply = client.chat.completions.create(model=m, messages=msgs)
|
|
45
|
+
exec(reply.choices[0].message.content)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Do
|
|
49
|
+
|
|
50
|
+
```py
|
|
51
|
+
reply = client.chat.completions.create(model=m, messages=msgs)
|
|
52
|
+
action = reply.choices[0].message.content.strip()
|
|
53
|
+
if action in ACTIONS:
|
|
54
|
+
ACTIONS[action]()
|
|
55
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# sec-py/no-log-injection — cases
|
|
2
|
+
|
|
3
|
+
### case: a request value
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
def v(request):
|
|
7
|
+
logger.info("login %s", request.POST["u"])
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```expect
|
|
11
|
+
2:5 sec-py/no-log-injection
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### case: a message only
|
|
15
|
+
|
|
16
|
+
```py
|
|
17
|
+
def v(request):
|
|
18
|
+
logger.warning(request.GET["u"])
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```expect
|
|
22
|
+
2:5 sec-py/no-log-injection
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### case: a constant
|
|
26
|
+
|
|
27
|
+
```py
|
|
28
|
+
def v(request):
|
|
29
|
+
logger.info("login %s", "x")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```expect
|
|
33
|
+
(none)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### case: not a logger
|
|
37
|
+
|
|
38
|
+
```py
|
|
39
|
+
def v(request):
|
|
40
|
+
other.info("login %s", request.POST["u"])
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```expect
|
|
44
|
+
(none)
|
|
45
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-log-injection
|
|
3
|
+
title: 'A request value is not logged raw'
|
|
4
|
+
languages: [python]
|
|
5
|
+
severity: warn
|
|
6
|
+
fix: manual
|
|
7
|
+
fix_reason: "whether to strip, escape or reject the value depends on how the log is read"
|
|
8
|
+
stability: preview
|
|
9
|
+
security: { cwe: [117], owasp: ["A09:2025"], severity: low }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
A value from the request is not passed to a logger without its line breaks removed.
|
|
14
|
+
|
|
15
|
+
**Why:** A CR or LF in the value writes a line that looks like another log entry, so an attacker can forge audit records (CWE-117). 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.
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match: { any: [{ pattern: '$L($V)' }, { pattern: '$L($A, $V)' }] }
|
|
19
|
+
constraints:
|
|
20
|
+
L:
|
|
21
|
+
regex: '^(logger|logging|log|self\.logger|self\.log|app\.logger|_logger|_log)\.(debug|info|warning|warn|error|exception|critical|log)$'
|
|
22
|
+
A: { regex: '^[rbf]?[''\"]' }
|
|
23
|
+
where: { $V: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
|
|
24
|
+
message: '`$V` comes from the request and is logged as it is'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## How to fix
|
|
28
|
+
|
|
29
|
+
1. Pass the value through a helper that strips the line breaks, such as `clean(value)` with `text.replace("\r", "").replace("\n", " ")`; a method called on the request value itself is still seen as the request value.
|
|
30
|
+
2. Or log it as structured data (`extra={"name": value}`) and use a JSON formatter that escapes it.
|
|
31
|
+
|
|
32
|
+
## Examples
|
|
33
|
+
|
|
34
|
+
### Python
|
|
35
|
+
|
|
36
|
+
#### Don't
|
|
37
|
+
|
|
38
|
+
```py
|
|
39
|
+
def view(request):
|
|
40
|
+
logger.info("login %s", request.POST["user"])
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### Do
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
def clean(text):
|
|
47
|
+
return text.replace("\r", "").replace("\n", " ")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def view(request):
|
|
51
|
+
logger.info("login %s", clean(request.POST["user"]))
|
|
52
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# sec-py/no-mako — cases
|
|
2
|
+
|
|
3
|
+
### case: Template
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
from mako.template import Template
|
|
7
|
+
|
|
8
|
+
Template(src)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```expect
|
|
12
|
+
3:1 sec-py/no-mako
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### case: qualified
|
|
16
|
+
|
|
17
|
+
```py
|
|
18
|
+
import mako.template
|
|
19
|
+
|
|
20
|
+
mako.template.Template(src)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```expect
|
|
24
|
+
3:1 sec-py/no-mako
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### case: jinja Template
|
|
28
|
+
|
|
29
|
+
```py
|
|
30
|
+
from jinja2 import Template
|
|
31
|
+
|
|
32
|
+
Template(src)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```expect
|
|
36
|
+
(none)
|
|
37
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-mako
|
|
3
|
+
title: 'No Mako templates'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "moving to another engine (or turning on `default_filters`) changes every template"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B702, ruff/S702]
|
|
9
|
+
security: { cwe: [79], owasp: ["A05:2025"], severity: medium }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`mako.template.Template` is not used.
|
|
14
|
+
|
|
15
|
+
**Why:** Mako does not escape output unless told to, so every `${value}` is a script waiting for a hostile value (CWE-79).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match: { any: [{ pattern: '$T($$$A)' }] }
|
|
19
|
+
constraints: { T: { regex: '^(Template|[A-Za-z_]+\.Template|mako\.template\.Template)$' } }
|
|
20
|
+
where:
|
|
21
|
+
$T:
|
|
22
|
+
any:
|
|
23
|
+
- { imported-from: { module: mako.template, name: Template } }
|
|
24
|
+
- { imported-from: mako.template }
|
|
25
|
+
- { imported-from: { module: mako, name: template } }
|
|
26
|
+
message: 'Mako does not escape by default; use Jinja2 with autoescape, or set `default_filters=[''h'']`'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## How to fix
|
|
30
|
+
|
|
31
|
+
1. Use Jinja2 with `autoescape=True`.
|
|
32
|
+
2. If you keep Mako, pass `default_filters=["h"]` to `Template` / `TemplateLookup`, and review each `|n`.
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
### Python
|
|
37
|
+
|
|
38
|
+
#### Don't
|
|
39
|
+
|
|
40
|
+
```py
|
|
41
|
+
from mako.template import Template
|
|
42
|
+
|
|
43
|
+
t = Template(source)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Do
|
|
47
|
+
|
|
48
|
+
```py
|
|
49
|
+
from jinja2 import Environment
|
|
50
|
+
|
|
51
|
+
env = Environment(autoescape=True)
|
|
52
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# sec-py/no-mark-safe — cases
|
|
2
|
+
|
|
3
|
+
### case: a concatenation
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
mark_safe("<b>" + name + "</b>")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-mark-safe
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: an f-string
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
mark_safe(f"<b>{name}</b>")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-mark-safe
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: a literal
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
mark_safe("<br>")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: Markup
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
Markup(user_text)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
1:1 sec-py/no-mark-safe
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### case: format_html
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
format_html("<b>{}</b>", name)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
(none)
|
|
51
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-mark-safe
|
|
3
|
+
title: 'No `mark_safe` of dynamic text'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the right fix is escaping the parts (`format_html`, `escape`) or rendering through a template, not removing the call"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B308, bandit/B703, bandit/B704, ruff/S308, ruff/S703, ruff/S704]
|
|
9
|
+
security: { cwe: [79], owasp: ["A05:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`mark_safe`, `SafeString`, `Markup` and `format_html` are not given a value that is not a literal.
|
|
14
|
+
|
|
15
|
+
**Why:** They switch HTML escaping off for that value, so any part of it that came from a user is a script (CWE-79).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
all:
|
|
20
|
+
- { kind: call }
|
|
21
|
+
- has:
|
|
22
|
+
field: function
|
|
23
|
+
regex: '^((.*\.)?(mark_safe|SafeString|SafeText|SafeData)|(markupsafe\.)?Markup)$'
|
|
24
|
+
- has:
|
|
25
|
+
field: arguments
|
|
26
|
+
has: { nthChild: 1, not: { kind: string, not: { has: { kind: interpolation } } }, pattern: $X }
|
|
27
|
+
message: '`$X` is marked safe, so it is not escaped; escape the parts with `format_html` or `escape`'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## How to fix
|
|
31
|
+
|
|
32
|
+
1. Build HTML with `format_html("<b>{}</b>", value)`: it escapes `value` and trusts only the literal.
|
|
33
|
+
2. Or render a template with autoescape on.
|
|
34
|
+
3. Use `mark_safe` only on a literal, or on text you produced by escaping.
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
### Python
|
|
39
|
+
|
|
40
|
+
#### Don't
|
|
41
|
+
|
|
42
|
+
```py
|
|
43
|
+
from django.utils.safestring import mark_safe
|
|
44
|
+
|
|
45
|
+
html = mark_safe("<b>" + name + "</b>")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Do
|
|
49
|
+
|
|
50
|
+
```py
|
|
51
|
+
from django.utils.html import format_html
|
|
52
|
+
|
|
53
|
+
html = format_html("<b>{}</b>", name)
|
|
54
|
+
```
|