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,61 @@
|
|
|
1
|
+
# sec-py/password-hash-effort — cases
|
|
2
|
+
|
|
3
|
+
### case: sha256 of a password
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
hashlib.sha256(password.encode())
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/password-hash-effort
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: few iterations
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
hashlib.pbkdf2_hmac("sha256", pw, salt, iterations=100000)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/password-hash-effort
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: positional few iterations
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
hashlib.pbkdf2_hmac("sha256", pw, salt, 1000)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:1 sec-py/password-hash-effort
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: enough iterations
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
hashlib.pbkdf2_hmac("sha256", pw, salt, 600000)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### case: bcrypt rounds
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
bcrypt.gensalt(rounds=4)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
1:1 sec-py/password-hash-effort
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### case: sha256 of a file
|
|
54
|
+
|
|
55
|
+
```py
|
|
56
|
+
hashlib.sha256(data)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```expect
|
|
60
|
+
(none)
|
|
61
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/password-hash-effort
|
|
3
|
+
title: 'Passwords are hashed with a slow function'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "moving to Argon2 or bcrypt means rehashing at the next login and changing the stored format"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [916], owasp: ["A04:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
A password is not hashed with a plain `hashlib` digest, `pbkdf2_hmac` runs at least 600 000 iterations, and `bcrypt` uses at least 10 rounds.
|
|
13
|
+
|
|
14
|
+
**Why:** A fast hash lets an attacker who has the table test billions of guesses per second (CWE-916).
|
|
15
|
+
|
|
16
|
+
```codelaw
|
|
17
|
+
match:
|
|
18
|
+
any:
|
|
19
|
+
- kind: call
|
|
20
|
+
all:
|
|
21
|
+
- { has: { field: function, regex: '^hashlib\.(md5|sha1|sha224|sha256|sha384|sha512)$' } }
|
|
22
|
+
- { has: { field: arguments, has: { nthChild: 1, regex: '(?i)pass(word|wd)?' } } }
|
|
23
|
+
- kind: call
|
|
24
|
+
all:
|
|
25
|
+
- { has: { field: function, regex: '^(hashlib\.)?pbkdf2_hmac$' } }
|
|
26
|
+
- any:
|
|
27
|
+
- has:
|
|
28
|
+
field: arguments
|
|
29
|
+
has:
|
|
30
|
+
kind: keyword_argument
|
|
31
|
+
all:
|
|
32
|
+
- { has: { field: name, regex: '^iterations$' } }
|
|
33
|
+
- { has: { field: value, kind: integer, regex: '^([0-9]{1,5}|[1-5][0-9]{5})$' } }
|
|
34
|
+
- has:
|
|
35
|
+
field: arguments
|
|
36
|
+
has: { nthChild: 4, kind: integer, regex: '^([0-9]{1,5}|[1-5][0-9]{5})$' }
|
|
37
|
+
- kind: call
|
|
38
|
+
all:
|
|
39
|
+
- { has: { field: function, regex: '^(bcrypt\.)?gensalt$' } }
|
|
40
|
+
- has:
|
|
41
|
+
field: arguments
|
|
42
|
+
has:
|
|
43
|
+
kind: keyword_argument
|
|
44
|
+
all:
|
|
45
|
+
- { has: { field: name, regex: '^rounds$' } }
|
|
46
|
+
- { has: { field: value, kind: integer, regex: '^[0-9]$' } }
|
|
47
|
+
message: 'This password hash is too fast; use Argon2 or bcrypt, or more iterations'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## How to fix
|
|
51
|
+
|
|
52
|
+
1. Use `argon2-cffi` (`PasswordHasher().hash(password)`) or `bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))`.
|
|
53
|
+
2. If you must use PBKDF2, use SHA-256 with at least 600 000 iterations (OWASP).
|
|
54
|
+
3. Rehash old entries when a user logs in.
|
|
55
|
+
|
|
56
|
+
## Examples
|
|
57
|
+
|
|
58
|
+
### Python
|
|
59
|
+
|
|
60
|
+
#### Don't
|
|
61
|
+
|
|
62
|
+
```py
|
|
63
|
+
import hashlib
|
|
64
|
+
|
|
65
|
+
stored = hashlib.sha256(password.encode()).hexdigest()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### Do
|
|
69
|
+
|
|
70
|
+
```py
|
|
71
|
+
from argon2 import PasswordHasher
|
|
72
|
+
|
|
73
|
+
stored = PasswordHasher().hash(password)
|
|
74
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sec-py/requests-timeout — cases
|
|
2
|
+
|
|
3
|
+
### case: no timeout
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
requests.get(url)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/requests-timeout
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: with timeout
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
requests.get(url, timeout=3)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
(none)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: a session is not matched
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
session.get(url)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: post with data
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
requests.post(url, json=body)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
1:1 sec-py/requests-timeout
|
|
41
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/requests-timeout
|
|
3
|
+
title: 'HTTP requests have a timeout'
|
|
4
|
+
languages: [python]
|
|
5
|
+
exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
|
|
6
|
+
fix: suggest
|
|
7
|
+
fix_reason: "the right timeout depends on the service; the suggested value is a starting point"
|
|
8
|
+
stability: preview
|
|
9
|
+
ported_from: [bandit/B113, ruff/S113]
|
|
10
|
+
security: { cwe: [400], owasp: ["A06:2025"], severity: medium }
|
|
11
|
+
since: 0.2.0
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
`requests` and `httpx` calls pass `timeout=`.
|
|
15
|
+
|
|
16
|
+
**Why:** `requests` waits forever by default, so one stuck server holds a worker for good and a flood of slow responses exhausts them (CWE-400).
|
|
17
|
+
|
|
18
|
+
```codelaw
|
|
19
|
+
match:
|
|
20
|
+
all:
|
|
21
|
+
- { pattern: '$F($$$A)' }
|
|
22
|
+
- { kind: call }
|
|
23
|
+
- not:
|
|
24
|
+
has:
|
|
25
|
+
field: arguments
|
|
26
|
+
has: { kind: keyword_argument, has: { field: name, regex: '^timeout$' } }
|
|
27
|
+
constraints: { F: { regex: '^requests\.(get|post|put|patch|delete|head|options|request)$' } }
|
|
28
|
+
message: 'This request has no timeout; pass `timeout=`'
|
|
29
|
+
fix: '$F($$$A, timeout=10)'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## How to fix
|
|
33
|
+
|
|
34
|
+
1. Pass `timeout=` in seconds, or a `(connect, read)` tuple.
|
|
35
|
+
2. Keep the value in one setting if several calls share it.
|
|
36
|
+
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
39
|
+
### Python
|
|
40
|
+
|
|
41
|
+
#### Don't
|
|
42
|
+
|
|
43
|
+
```py
|
|
44
|
+
import requests
|
|
45
|
+
|
|
46
|
+
resp = requests.get(url)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### Do
|
|
50
|
+
|
|
51
|
+
```py
|
|
52
|
+
import requests
|
|
53
|
+
|
|
54
|
+
resp = requests.get(url, timeout=10)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Fixed
|
|
58
|
+
|
|
59
|
+
```py
|
|
60
|
+
import requests
|
|
61
|
+
|
|
62
|
+
resp = requests.get(url, timeout=10)
|
|
63
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# sec-py/tarfile-filter — cases
|
|
2
|
+
|
|
3
|
+
### case: no filter
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
tar.extractall(dest)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/tarfile-filter
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: a filter
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
tar.extractall(dest, filter="data")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
(none)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: a zip file
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
zf.extract(name)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: unpack_archive
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
shutil.unpack_archive(path, dest)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
1:1 sec-py/tarfile-filter
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### case: unpack_archive with a filter
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
shutil.unpack_archive(path, dest, filter="data")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
(none)
|
|
51
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/tarfile-filter
|
|
3
|
+
title: 'Archive extraction uses `filter="data"`'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: suggest
|
|
6
|
+
fix_reason: "`filter=\"data\"` refuses members that carry device files or special permissions some archives need"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B202, ruff/S202]
|
|
9
|
+
security: { cwe: [22], owasp: ["A01:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`TarFile.extractall` and `shutil.unpack_archive` pass `filter="data"`.
|
|
14
|
+
|
|
15
|
+
**Why:** A member named `../../etc/cron.d/x`, an absolute path or a symlink writes outside the target folder (CWE-22).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- all:
|
|
21
|
+
- { pattern: '$T.extractall($$$A)' }
|
|
22
|
+
- not:
|
|
23
|
+
has:
|
|
24
|
+
field: arguments
|
|
25
|
+
has: { kind: keyword_argument, has: { field: name, regex: '^filter$' } }
|
|
26
|
+
- all:
|
|
27
|
+
- { pattern: 'shutil.unpack_archive($$$B)' }
|
|
28
|
+
- not:
|
|
29
|
+
has:
|
|
30
|
+
field: arguments
|
|
31
|
+
has: { kind: keyword_argument, has: { field: name, regex: '^filter$' } }
|
|
32
|
+
message: '`extractall` follows member paths out of the folder; pass `filter="data"`'
|
|
33
|
+
fix: '$T.extractall($$$A, filter="data")'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## How to fix
|
|
37
|
+
|
|
38
|
+
1. Pass `filter="data"` (Python 3.12, and the 3.8–3.11 security releases).
|
|
39
|
+
2. Pass it to `shutil.unpack_archive(…, filter="data")` too.
|
|
40
|
+
3. Extract into a fresh folder, and never run as root.
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
### Python
|
|
45
|
+
|
|
46
|
+
#### Don't
|
|
47
|
+
|
|
48
|
+
```py
|
|
49
|
+
import tarfile
|
|
50
|
+
|
|
51
|
+
with tarfile.open(path) as tar:
|
|
52
|
+
tar.extractall(dest)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Do
|
|
56
|
+
|
|
57
|
+
```py
|
|
58
|
+
import tarfile
|
|
59
|
+
|
|
60
|
+
with tarfile.open(path) as tar:
|
|
61
|
+
tar.extractall(dest, filter="data")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Fixed
|
|
65
|
+
|
|
66
|
+
```py
|
|
67
|
+
import tarfile
|
|
68
|
+
|
|
69
|
+
with tarfile.open(path) as tar:
|
|
70
|
+
tar.extractall(dest, filter="data")
|
|
71
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sec-py/torch-load-weights-only — cases
|
|
2
|
+
|
|
3
|
+
### case: a bare load
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
torch.load(path)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/torch-load-weights-only
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: map_location only
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
torch.load(path, map_location="cpu")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/torch-load-weights-only
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: weights_only=False
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
torch.load(path, weights_only=False)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:1 sec-py/torch-load-weights-only
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: weights_only=True
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
torch.load(path, weights_only=True)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/torch-load-weights-only
|
|
3
|
+
title: "`torch.load` with `weights_only=True`"
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: suggest
|
|
6
|
+
fix_reason: "`weights_only=True` refuses checkpoints that hold arbitrary objects; those need a conversion to `safetensors`"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B614]
|
|
9
|
+
security: { cwe: [502], owasp: ["A08:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`torch.load` is called with `weights_only=True`.
|
|
14
|
+
|
|
15
|
+
**Why:** Without it the file is unpickled, so a model file from anywhere else runs code on load (CWE-502).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- all:
|
|
21
|
+
- { pattern: 'torch.load($$$A)' }
|
|
22
|
+
- not:
|
|
23
|
+
has:
|
|
24
|
+
field: arguments
|
|
25
|
+
has:
|
|
26
|
+
kind: keyword_argument
|
|
27
|
+
all: [{ has: { field: name, regex: '^weights_only$' } }]
|
|
28
|
+
- kind: call
|
|
29
|
+
all:
|
|
30
|
+
- { has: { field: function, regex: '^torch\.load$' } }
|
|
31
|
+
- has:
|
|
32
|
+
field: arguments
|
|
33
|
+
has:
|
|
34
|
+
kind: keyword_argument
|
|
35
|
+
all:
|
|
36
|
+
- { has: { field: name, regex: '^weights_only$' } }
|
|
37
|
+
- { has: { field: value, kind: 'false' } }
|
|
38
|
+
message: '`torch.load` unpickles the file; pass `weights_only=True`'
|
|
39
|
+
fix: 'torch.load($$$A, weights_only=True)'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## How to fix
|
|
43
|
+
|
|
44
|
+
1. Add `weights_only=True`.
|
|
45
|
+
2. If the checkpoint holds more than tensors, convert it once with a trusted environment and keep it as `safetensors`.
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
### Python
|
|
50
|
+
|
|
51
|
+
#### Don't
|
|
52
|
+
|
|
53
|
+
```py
|
|
54
|
+
import torch
|
|
55
|
+
|
|
56
|
+
model = torch.load(path)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### Do
|
|
60
|
+
|
|
61
|
+
```py
|
|
62
|
+
import torch
|
|
63
|
+
|
|
64
|
+
model = torch.load(path, weights_only=True)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### Fixed
|
|
68
|
+
|
|
69
|
+
```py
|
|
70
|
+
import torch
|
|
71
|
+
|
|
72
|
+
model = torch.load(path, weights_only=True)
|
|
73
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sec-py/upload-secure-filename — cases
|
|
2
|
+
|
|
3
|
+
### case: the raw name
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
f.save(f.filename)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/upload-secure-filename
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: joined
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
file.save(os.path.join(UP, file.filename))
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/upload-secure-filename
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: secured
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
f.save(os.path.join(UP, secure_filename(f.filename)))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: a generated name
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
f.save(uuid4().hex)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/upload-secure-filename
|
|
3
|
+
title: 'Uploaded files are saved under a safe name'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: suggest
|
|
6
|
+
fix_reason: "`secure_filename` removes characters a name may legitimately have, and returns an empty string for some names, which needs handling"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [434, 22], owasp: ["A01:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
A file from a request is not saved under the name the client gave it.
|
|
13
|
+
|
|
14
|
+
**Why:** The name can be `../../app.py` or an absolute path, so the upload overwrites a file outside the upload folder (CWE-434, CWE-22).
|
|
15
|
+
|
|
16
|
+
```codelaw
|
|
17
|
+
match:
|
|
18
|
+
all:
|
|
19
|
+
- { kind: call }
|
|
20
|
+
- { has: { field: function, regex: '\.save$' } }
|
|
21
|
+
- has:
|
|
22
|
+
field: arguments
|
|
23
|
+
has:
|
|
24
|
+
any:
|
|
25
|
+
- { kind: attribute, regex: '\.filename$', pattern: $NAME }
|
|
26
|
+
- kind: call
|
|
27
|
+
has: { field: arguments, has: { kind: attribute, regex: '\.filename$', pattern: $NAME } }
|
|
28
|
+
message: 'The upload is saved under the client''s file name; wrap it in `secure_filename`'
|
|
29
|
+
fix:
|
|
30
|
+
- { replace: { target: $NAME, with: 'secure_filename($NAME)' } }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## How to fix
|
|
34
|
+
|
|
35
|
+
1. Import `from werkzeug.utils import secure_filename`.
|
|
36
|
+
2. Use `secure_filename(file.filename)`, and refuse an empty result.
|
|
37
|
+
3. Better: store the file under a name you generate (a UUID) and keep the client's name as data.
|
|
38
|
+
|
|
39
|
+
## Examples
|
|
40
|
+
|
|
41
|
+
### Python
|
|
42
|
+
|
|
43
|
+
#### Don't
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
from werkzeug.utils import secure_filename
|
|
47
|
+
|
|
48
|
+
file.save(os.path.join(UPLOADS, file.filename))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### Do
|
|
52
|
+
|
|
53
|
+
```py
|
|
54
|
+
from werkzeug.utils import secure_filename
|
|
55
|
+
|
|
56
|
+
file.save(os.path.join(UPLOADS, secure_filename(file.filename)))
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### Fixed
|
|
60
|
+
|
|
61
|
+
```py
|
|
62
|
+
from werkzeug.utils import secure_filename
|
|
63
|
+
|
|
64
|
+
file.save(os.path.join(UPLOADS, secure_filename(file.filename)))
|
|
65
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sec-py/urlopen-scheme — cases
|
|
2
|
+
|
|
3
|
+
### case: a name
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
urlopen(url)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/urlopen-scheme
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: an https literal
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
urlopen("https://example.com")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
(none)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: qualified
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
urllib.request.urlopen(u, timeout=5)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:1 sec-py/urlopen-scheme
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: an f-string with a fixed prefix
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
urlopen(f"https://example.com/{x}")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/urlopen-scheme
|
|
3
|
+
title: "`urlopen` of a value that may be `file://`"
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the allowed schemes and hosts are policy"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B310, ruff/S310]
|
|
9
|
+
security: { cwe: [22, 918], owasp: ["A01:2025"], severity: medium }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`urllib.request.urlopen` and `urlretrieve` are not given a value that is not an `http(s)` literal.
|
|
14
|
+
|
|
15
|
+
**Why:** `urlopen` also opens `file://` and `ftp://` URLs, so a value that is not checked can read local files (CWE-22, CWE-918).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- { pattern: 'urlopen($X)' }
|
|
21
|
+
- { pattern: 'urlopen($X, $$$R)' }
|
|
22
|
+
- { pattern: 'urllib.request.urlopen($X)' }
|
|
23
|
+
- { pattern: 'urllib.request.urlopen($X, $$$R)' }
|
|
24
|
+
- { pattern: 'urllib.request.urlretrieve($X)' }
|
|
25
|
+
- { pattern: 'urllib.request.urlretrieve($X, $$$R)' }
|
|
26
|
+
- { pattern: 'urlretrieve($X, $$$R)' }
|
|
27
|
+
constraints: { X: { not: { kind: string, regex: '^[rbf]?[''\"]https?://' } } }
|
|
28
|
+
message: '`$X` may be a `file://` URL; check the scheme first'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## How to fix
|
|
32
|
+
|
|
33
|
+
1. Parse the URL and check `scheme in ("http", "https")` before opening it.
|
|
34
|
+
2. Or use `requests`, which supports only HTTP(S), with a `timeout`.
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
### Python
|
|
39
|
+
|
|
40
|
+
#### Don't
|
|
41
|
+
|
|
42
|
+
```py
|
|
43
|
+
from urllib.request import urlopen
|
|
44
|
+
|
|
45
|
+
data = urlopen(url).read()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Do
|
|
49
|
+
|
|
50
|
+
```py
|
|
51
|
+
from urllib.request import urlopen
|
|
52
|
+
|
|
53
|
+
data = urlopen("https://example.com/data").read()
|
|
54
|
+
```
|