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,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-weak-hash
|
|
3
|
+
title: 'No MD5 or SHA-1 for security'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: suggest
|
|
6
|
+
fix_reason: "whether the digest is used for security (use SHA-256) or only as a checksum (say so) is the author's call"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B303, bandit/B324, ruff/S324]
|
|
9
|
+
security: { cwe: [327], owasp: ["A04:2025"], severity: medium }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`hashlib.md5` and `hashlib.sha1` pass `usedforsecurity=False`, or are replaced by SHA-256.
|
|
14
|
+
|
|
15
|
+
**Why:** Both can be forged by collision, so they prove nothing about who wrote the data (CWE-327).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
all:
|
|
20
|
+
- { pattern: 'hashlib.$H($$$A)' }
|
|
21
|
+
- not:
|
|
22
|
+
has:
|
|
23
|
+
field: arguments
|
|
24
|
+
has: { kind: keyword_argument, has: { field: name, regex: '^usedforsecurity$' } }
|
|
25
|
+
constraints: { H: { regex: '^(md5|sha1)$' } }
|
|
26
|
+
message: '`hashlib.$H` is not collision resistant; use SHA-256, or pass `usedforsecurity=False` for a checksum'
|
|
27
|
+
fix: 'hashlib.sha256($$$A)'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## How to fix
|
|
31
|
+
|
|
32
|
+
1. If the digest guards integrity or identity, use `hashlib.sha256` or `hashlib.blake2b`.
|
|
33
|
+
2. If it is only a cache key or checksum, pass `usedforsecurity=False` and say so in a comment.
|
|
34
|
+
3. For passwords see `sec-py/password-hash-effort`.
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
### Python
|
|
39
|
+
|
|
40
|
+
#### Don't
|
|
41
|
+
|
|
42
|
+
```py
|
|
43
|
+
import hashlib
|
|
44
|
+
|
|
45
|
+
digest = hashlib.md5(data).hexdigest()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Do
|
|
49
|
+
|
|
50
|
+
```py
|
|
51
|
+
import hashlib
|
|
52
|
+
|
|
53
|
+
digest = hashlib.sha256(data).hexdigest()
|
|
54
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sec-py/no-wildcard-injection — cases
|
|
2
|
+
|
|
3
|
+
### case: tar *
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
os.system("tar cf a.tar *")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-wildcard-injection
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: chmod *
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
subprocess.run("chmod 600 *", shell=True)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-wildcard-injection
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: ls *
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
os.system("ls *")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: a list
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
subprocess.run(["tar", "cf", "a"])
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-wildcard-injection
|
|
3
|
+
title: 'A shell wildcard after `tar`, `chown`, `chmod` or `rsync`'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the files have to be listed (`glob`) and passed after `--`, which changes the call"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B609, ruff/S609]
|
|
9
|
+
security: { cwe: [155], owasp: ["A05:2025"], severity: medium }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
A command that runs `tar`, `chown`, `chmod` or `rsync` on `*` is not started through a shell.
|
|
14
|
+
|
|
15
|
+
**Why:** A file named `--checkpoint-action=exec=sh x.sh` in the folder is expanded by the shell into an option, so whoever can create files there runs commands (CWE-155).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
kind: call
|
|
20
|
+
all:
|
|
21
|
+
- has:
|
|
22
|
+
field: function
|
|
23
|
+
regex: '^(os\.system|os\.popen|subprocess\.(run|call|check_call|check_output|Popen))$'
|
|
24
|
+
- has:
|
|
25
|
+
field: arguments
|
|
26
|
+
has: { nthChild: 1, kind: string, regex: '\b(tar|chown|chmod|rsync)\b[^''\"]*\*' }
|
|
27
|
+
message: '`*` is expanded by the shell, so a file named like an option changes the command'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## How to fix
|
|
31
|
+
|
|
32
|
+
1. List the files in Python: `files = sorted(glob.glob("*.txt"))`.
|
|
33
|
+
2. Pass them as a list after `--`: `subprocess.run(["tar", "cf", out, "--", *files])`.
|
|
34
|
+
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
### Python
|
|
38
|
+
|
|
39
|
+
#### Don't
|
|
40
|
+
|
|
41
|
+
```py
|
|
42
|
+
import os
|
|
43
|
+
|
|
44
|
+
os.system("tar cf backup.tar *")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
#### Do
|
|
48
|
+
|
|
49
|
+
```py
|
|
50
|
+
import subprocess
|
|
51
|
+
|
|
52
|
+
subprocess.run(["tar", "cf", "backup.tar", "--", *files], check=True)
|
|
53
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# sec-py/no-world-writable — cases
|
|
2
|
+
|
|
3
|
+
### case: 777
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
os.chmod(path, 0o777)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-world-writable
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: 666
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
os.chmod(path, 0o666)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-world-writable
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: 600
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
os.chmod(path, 0o600)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: 755
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
os.chmod(path, 0o755)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### case: S_IWOTH
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
os.chmod(path, stat.S_IWOTH)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
1:1 sec-py/no-world-writable
|
|
51
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-world-writable
|
|
3
|
+
title: 'No world-writable files'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: suggest
|
|
6
|
+
fix_reason: "the right mode depends on who has to write the file"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B103, ruff/S103]
|
|
9
|
+
security: { cwe: [732], owasp: ["A01:2025"], severity: medium }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`os.chmod` and `os.umask` do not give write access to everyone.
|
|
14
|
+
|
|
15
|
+
**Why:** Any local user can then change the file, which for a script, config or socket means running code as its owner (CWE-732).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match: { all: [{ pattern: 'os.chmod($P, $M)' }] }
|
|
19
|
+
constraints:
|
|
20
|
+
M:
|
|
21
|
+
regex: '^(0o?[0-7]*[2367]|0o?[0-7]*7[0-7]?[2367]|stat\.S_IWOTH|stat\.S_IRWXO|0o?[0-7]{0,2}77[0-7]|0o?[0-7]*[2367])$'
|
|
22
|
+
message: '`$M` makes the file writable by everyone'
|
|
23
|
+
fix: 'os.chmod($P, 0o600)'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## How to fix
|
|
27
|
+
|
|
28
|
+
1. Use `0o600` for a private file, `0o644` for one others read, `0o755` for a program.
|
|
29
|
+
2. Never set the others' write bit.
|
|
30
|
+
|
|
31
|
+
## Examples
|
|
32
|
+
|
|
33
|
+
### Python
|
|
34
|
+
|
|
35
|
+
#### Don't
|
|
36
|
+
|
|
37
|
+
```py
|
|
38
|
+
import os
|
|
39
|
+
|
|
40
|
+
os.chmod(path, 0o777)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### Do
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
import os
|
|
47
|
+
|
|
48
|
+
os.chmod(path, 0o600)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### Fixed
|
|
52
|
+
|
|
53
|
+
```py
|
|
54
|
+
import os
|
|
55
|
+
|
|
56
|
+
os.chmod(path, 0o600)
|
|
57
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# sec-py/no-xml-unsafe-parser — cases
|
|
2
|
+
|
|
3
|
+
### case: ElementTree through an alias
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
import xml.etree.ElementTree as ET
|
|
7
|
+
|
|
8
|
+
ET.fromstring(text)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```expect
|
|
12
|
+
3:1 sec-py/no-xml-unsafe-parser
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### case: ElementTree imported from xml.etree
|
|
16
|
+
|
|
17
|
+
```py
|
|
18
|
+
from xml.etree import ElementTree
|
|
19
|
+
|
|
20
|
+
ElementTree.parse(p)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```expect
|
|
24
|
+
3:1 sec-py/no-xml-unsafe-parser
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### case: defusedxml
|
|
28
|
+
|
|
29
|
+
```py
|
|
30
|
+
import defusedxml.ElementTree as ET
|
|
31
|
+
|
|
32
|
+
ET.parse(p)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```expect
|
|
36
|
+
(none)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### case: minidom
|
|
40
|
+
|
|
41
|
+
```py
|
|
42
|
+
from xml.dom import minidom
|
|
43
|
+
|
|
44
|
+
minidom.parseString(text)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```expect
|
|
48
|
+
3:1 sec-py/no-xml-unsafe-parser
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### case: lxml
|
|
52
|
+
|
|
53
|
+
```py
|
|
54
|
+
from lxml import etree
|
|
55
|
+
|
|
56
|
+
etree.fromstring(text)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```expect
|
|
60
|
+
3:1 sec-py/no-xml-unsafe-parser
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### case: an unrelated etree
|
|
64
|
+
|
|
65
|
+
```py
|
|
66
|
+
from mylib import etree
|
|
67
|
+
|
|
68
|
+
etree.parse(text)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```expect
|
|
72
|
+
(none)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### case: an unrelated parse
|
|
76
|
+
|
|
77
|
+
```py
|
|
78
|
+
parser.parse(text)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```expect
|
|
82
|
+
(none)
|
|
83
|
+
```
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-xml-unsafe-parser
|
|
3
|
+
title: 'XML is parsed with `defusedxml`'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: suggest
|
|
6
|
+
fix_reason: "`defusedxml` has to be installed and imported, and it refuses documents the standard parser accepts"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B313, bandit/B314, bandit/B315, bandit/B316, bandit/B317, bandit/B318, bandit/B319, bandit/B320, bandit/B405, bandit/B406, bandit/B407, bandit/B408, bandit/B409, bandit/B410]
|
|
9
|
+
security: { cwe: [611], owasp: ["A05:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
XML from outside the program is parsed with `defusedxml`, not with `xml.etree`, `minidom`, `sax` or `lxml`.
|
|
14
|
+
|
|
15
|
+
**Why:** The standard parsers expand entities, so a crafted document can read local files, reach the network or exhaust memory (CWE-611).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- all:
|
|
21
|
+
- { pattern: '$M.$F($$$A)' }
|
|
22
|
+
- has:
|
|
23
|
+
field: function
|
|
24
|
+
has:
|
|
25
|
+
field: object
|
|
26
|
+
regex: '^(ET|ElementTree|cElementTree|xml\.etree\.(c)?ElementTree)$'
|
|
27
|
+
pattern: $ET
|
|
28
|
+
- { pattern: '$M.$F($$$A)' }
|
|
29
|
+
constraints:
|
|
30
|
+
F:
|
|
31
|
+
regex: '^(parse|parseString|iterparse|fromstring|fromstringlist|XML|XMLParser|make_parser)$'
|
|
32
|
+
where:
|
|
33
|
+
$M:
|
|
34
|
+
any:
|
|
35
|
+
- imported-from:
|
|
36
|
+
- xml.etree.ElementTree
|
|
37
|
+
- xml.etree.cElementTree
|
|
38
|
+
- xml.dom.minidom
|
|
39
|
+
- xml.dom.pulldom
|
|
40
|
+
- xml.sax
|
|
41
|
+
- xml.dom.expatbuilder
|
|
42
|
+
- lxml.etree
|
|
43
|
+
- { imported-from: { module: xml.etree, name: ElementTree } }
|
|
44
|
+
- { imported-from: { module: xml.etree, name: cElementTree } }
|
|
45
|
+
- { imported-from: { module: xml.dom, name: minidom } }
|
|
46
|
+
- { imported-from: { module: xml.dom, name: pulldom } }
|
|
47
|
+
- { imported-from: { module: xml, name: sax } }
|
|
48
|
+
- { imported-from: { module: lxml, name: etree } }
|
|
49
|
+
message: 'This parser expands XML entities; use `defusedxml`'
|
|
50
|
+
fix:
|
|
51
|
+
- { replace: { target: $ET, with: defusedxml.ElementTree } }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## How to fix
|
|
55
|
+
|
|
56
|
+
1. Install `defusedxml` and import it: `import defusedxml.ElementTree as ET`.
|
|
57
|
+
2. Call the same function on it: `ET.fromstring(text)`.
|
|
58
|
+
3. For `minidom`, `sax` or `pulldom`, import the module of the same name from `defusedxml`; for `lxml`, build the parser with `resolve_entities=False, no_network=True`.
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
### Python
|
|
63
|
+
|
|
64
|
+
#### Don't
|
|
65
|
+
|
|
66
|
+
```py
|
|
67
|
+
import xml.etree.ElementTree as ET
|
|
68
|
+
|
|
69
|
+
tree = ET.parse(path)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### Do
|
|
73
|
+
|
|
74
|
+
```py
|
|
75
|
+
import defusedxml.ElementTree as ET
|
|
76
|
+
|
|
77
|
+
tree = ET.parse(path)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### Fixed
|
|
81
|
+
|
|
82
|
+
```py
|
|
83
|
+
import xml.etree.ElementTree as ET
|
|
84
|
+
|
|
85
|
+
tree = defusedxml.ElementTree.parse(path)
|
|
86
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# sec-py/no-xmlrpc — cases
|
|
2
|
+
|
|
3
|
+
### case: ServerProxy
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
xmlrpc.client.ServerProxy(url)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-xmlrpc
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: a server
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
SimpleXMLRPCServer(("localhost", 8000))
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-xmlrpc
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: requests
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
requests.post(url)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-xmlrpc
|
|
3
|
+
title: 'No `xmlrpc` without `defusedxml`'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the patch has to run once before any xmlrpc object exists, and where that is depends on the program's start-up"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B411, ruff/S411]
|
|
9
|
+
security: { cwe: [776], owasp: ["A05:2025"], severity: medium }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
An `xmlrpc` client or server is not created without `defusedxml.xmlrpc.monkey_patch()` in the program.
|
|
14
|
+
|
|
15
|
+
**Why:** `xmlrpc` parses XML with the standard parser, so a crafted request or response carries an entity expansion bomb or reads files (CWE-776).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
kind: call
|
|
20
|
+
all:
|
|
21
|
+
- has:
|
|
22
|
+
field: function
|
|
23
|
+
regex: '^(xmlrpc\.(client|server)\.)?(ServerProxy|Server|SimpleXMLRPCServer|SimpleXMLRPCRequestHandler|MultiCall|DocXMLRPCServer|CGIXMLRPCRequestHandler)$'
|
|
24
|
+
message: '`xmlrpc` parses XML unsafely; call `defusedxml.xmlrpc.monkey_patch()` at start-up, or use JSON'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## How to fix
|
|
28
|
+
|
|
29
|
+
1. Add `import defusedxml.xmlrpc` and call `defusedxml.xmlrpc.monkey_patch()` once, before any `xmlrpc` object is made.
|
|
30
|
+
2. Or move the API to JSON over HTTP.
|
|
31
|
+
|
|
32
|
+
## Examples
|
|
33
|
+
|
|
34
|
+
### Python
|
|
35
|
+
|
|
36
|
+
#### Don't
|
|
37
|
+
|
|
38
|
+
```py
|
|
39
|
+
from xmlrpc.client import ServerProxy
|
|
40
|
+
|
|
41
|
+
proxy = ServerProxy(url)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
#### Do
|
|
45
|
+
|
|
46
|
+
```py
|
|
47
|
+
import requests
|
|
48
|
+
|
|
49
|
+
resp = requests.post(url, json=payload, timeout=10)
|
|
50
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sec-py/no-xpath-injection — cases
|
|
2
|
+
|
|
3
|
+
### case: an f-string
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
tree.xpath(f"//user[@name='{name}']")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-xpath-injection
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: variables
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
tree.xpath("//user[@name=$n]", n=name)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
(none)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: a concatenation
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
root.findall("./user[@id='" + uid + "']")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:1 sec-py/no-xpath-injection
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: a plain path
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
root.findall("./user")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-xpath-injection
|
|
3
|
+
title: 'XPath expressions are not built from strings'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the expression has to be rewritten around `$name` variables, and which parts are values is the author's call"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [643], owasp: ["A05:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
An XPath expression is not made with an f-string, `%`, `+` or `.format`; values go in as XPath variables.
|
|
13
|
+
|
|
14
|
+
**Why:** A quote in the value ends the string in the expression and the rest of the value is read as XPath (CWE-643).
|
|
15
|
+
|
|
16
|
+
```codelaw
|
|
17
|
+
match:
|
|
18
|
+
kind: call
|
|
19
|
+
all:
|
|
20
|
+
- { has: { field: function, regex: '\.(xpath|findall|find|findtext|iterfind)$' } }
|
|
21
|
+
- has:
|
|
22
|
+
field: arguments
|
|
23
|
+
has:
|
|
24
|
+
nthChild: 1
|
|
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
|
+
regex: '(//|/|\[@)'
|
|
30
|
+
message: 'This XPath is built from a string; pass the values as variables'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## How to fix
|
|
34
|
+
|
|
35
|
+
1. Write the expression with variables: `tree.xpath("//user[@name=$name]", name=name)` (lxml).
|
|
36
|
+
2. ElementTree has no variables: select by a simple path and compare the attribute in Python.
|
|
37
|
+
|
|
38
|
+
## Examples
|
|
39
|
+
|
|
40
|
+
### Python
|
|
41
|
+
|
|
42
|
+
#### Don't
|
|
43
|
+
|
|
44
|
+
```py
|
|
45
|
+
nodes = tree.xpath(f"//user[@name='{name}']")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Do
|
|
49
|
+
|
|
50
|
+
```py
|
|
51
|
+
nodes = tree.xpath("//user[@name=$name]", name=name)
|
|
52
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# sec-py/paramiko-host-key — cases
|
|
2
|
+
|
|
3
|
+
### case: auto add
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/paramiko-host-key
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: reject
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
client.set_missing_host_key_policy(paramiko.RejectPolicy())
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
(none)
|
|
21
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/paramiko-host-key
|
|
3
|
+
title: 'SSH host keys are checked'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "where the known host keys come from is deployment-specific"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B507, ruff/S507]
|
|
9
|
+
security: { cwe: [295], owasp: ["A07:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`paramiko` is not told to accept unknown host keys with `AutoAddPolicy` or `WarningPolicy`.
|
|
14
|
+
|
|
15
|
+
**Why:** A connection to an impostor goes through unnoticed, and the credentials are handed to it (CWE-295).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match: { any: [{ pattern: '$C.set_missing_host_key_policy($P)' }] }
|
|
19
|
+
constraints: { P: { regex: '(AutoAddPolicy|WarningPolicy)' } }
|
|
20
|
+
message: 'This accepts any SSH host key; load known hosts and reject unknown ones'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How to fix
|
|
24
|
+
|
|
25
|
+
1. Call `client.load_system_host_keys()` or `load_host_keys(path)`.
|
|
26
|
+
2. Use `client.set_missing_host_key_policy(paramiko.RejectPolicy())`.
|
|
27
|
+
3. Put the server's key in `known_hosts` ahead of time.
|
|
28
|
+
|
|
29
|
+
## Examples
|
|
30
|
+
|
|
31
|
+
### Python
|
|
32
|
+
|
|
33
|
+
#### Don't
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### Do
|
|
40
|
+
|
|
41
|
+
```py
|
|
42
|
+
client.load_system_host_keys()
|
|
43
|
+
client.set_missing_host_key_policy(paramiko.RejectPolicy())
|
|
44
|
+
```
|