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,34 @@
|
|
|
1
|
+
# sec-py/no-ssrf — cases
|
|
2
|
+
|
|
3
|
+
### case: a request URL
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
def f(request):
|
|
7
|
+
return requests.get(request.args["u"], timeout=5)
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```expect
|
|
11
|
+
2:12 sec-py/no-ssrf
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### case: an f-string
|
|
15
|
+
|
|
16
|
+
```py
|
|
17
|
+
def f(request):
|
|
18
|
+
return requests.get(f"https://{request.GET['h']}/x", timeout=5)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```expect
|
|
22
|
+
2:12 sec-py/no-ssrf
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### case: a constant
|
|
26
|
+
|
|
27
|
+
```py
|
|
28
|
+
def f(request):
|
|
29
|
+
return requests.get("https://api.example.com", timeout=5)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```expect
|
|
33
|
+
(none)
|
|
34
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-ssrf
|
|
3
|
+
title: 'A request value is not a URL to fetch'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "which hosts are allowed is a policy only the author knows"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [918], owasp: ["A01:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
A value from the request is not the URL given to `requests`, `httpx` or `urllib`.
|
|
13
|
+
|
|
14
|
+
**Why:** The server fetches whatever address the client names, including internal services and cloud metadata (CWE-918). 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
|
+
all:
|
|
19
|
+
- { kind: call }
|
|
20
|
+
- has:
|
|
21
|
+
field: function
|
|
22
|
+
regex: '^(requests|httpx)\.(get|post|put|patch|delete|head|options|request|stream)$|^(urllib\.request\.)?urlopen$|^urllib\.request\.urlretrieve$|^(session|client)\.(get|post|put|delete|request)$'
|
|
23
|
+
- { has: { field: arguments, has: { nthChild: 1, pattern: $U } } }
|
|
24
|
+
where: { $U: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
|
|
25
|
+
message: '`$U` comes from the request and is fetched by the server'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## How to fix
|
|
29
|
+
|
|
30
|
+
1. Take an id or a key from the client and map it to a URL you wrote.
|
|
31
|
+
2. Or parse the URL and check the scheme is `https` and the host is on an allow-list, then connect to the address you resolved (not the name again).
|
|
32
|
+
3. Refuse private, loopback and link-local addresses.
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
### Python
|
|
37
|
+
|
|
38
|
+
#### Don't
|
|
39
|
+
|
|
40
|
+
```py
|
|
41
|
+
def fetch(request):
|
|
42
|
+
url = request.args["url"]
|
|
43
|
+
return requests.get(url, timeout=5)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Do
|
|
47
|
+
|
|
48
|
+
```py
|
|
49
|
+
def fetch(request):
|
|
50
|
+
host = urlparse(request.args["url"]).hostname
|
|
51
|
+
if host not in ALLOWED_HOSTS:
|
|
52
|
+
abort(400)
|
|
53
|
+
return requests.get(ALLOWED_HOSTS[host], timeout=5)
|
|
54
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# sec-py/no-stack-trace-response — cases
|
|
2
|
+
|
|
3
|
+
### case: str(e)
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
def v():
|
|
7
|
+
try:
|
|
8
|
+
run()
|
|
9
|
+
except Exception as e:
|
|
10
|
+
return jsonify(error=str(e))
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```expect
|
|
14
|
+
5:9 sec-py/no-stack-trace-response
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### case: format_exc
|
|
18
|
+
|
|
19
|
+
```py
|
|
20
|
+
def v():
|
|
21
|
+
return traceback.format_exc()
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```expect
|
|
25
|
+
2:5 sec-py/no-stack-trace-response
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### case: generic
|
|
29
|
+
|
|
30
|
+
```py
|
|
31
|
+
def v():
|
|
32
|
+
return jsonify(error="internal error")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```expect
|
|
36
|
+
(none)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### case: a number
|
|
40
|
+
|
|
41
|
+
```py
|
|
42
|
+
def v(e):
|
|
43
|
+
return len(e)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```expect
|
|
47
|
+
(none)
|
|
48
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-stack-trace-response
|
|
3
|
+
title: 'A stack trace is not sent to the client'
|
|
4
|
+
languages: [python]
|
|
5
|
+
exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
|
|
6
|
+
fix: manual
|
|
7
|
+
fix_reason: "what the client sees instead (a generic message and an error id) is a product decision"
|
|
8
|
+
stability: preview
|
|
9
|
+
security: { cwe: [209], owasp: ["A10:2025"], severity: medium }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
A response does not contain `traceback.format_exc()` or the text of the exception.
|
|
14
|
+
|
|
15
|
+
**Why:** The text names files, queries, library versions and sometimes values, which tells an attacker where to push (CWE-209).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- all:
|
|
21
|
+
- { kind: return_statement }
|
|
22
|
+
- has:
|
|
23
|
+
stopBy: end
|
|
24
|
+
kind: call
|
|
25
|
+
regex: '^(str|repr)\((e|exc|err|ex|error|exception)\)$|^traceback\.format_exc\(\)$|^(e|exc|err|ex|error|exception)\.(args|message)$'
|
|
26
|
+
- all:
|
|
27
|
+
- { kind: raise_statement }
|
|
28
|
+
- has:
|
|
29
|
+
stopBy: end
|
|
30
|
+
kind: call
|
|
31
|
+
regex: '^(str|repr)\((e|exc|err|ex|error|exception)\)$|^traceback\.format_exc\(\)$|^(e|exc|err|ex|error|exception)\.(args|message)$'
|
|
32
|
+
- has:
|
|
33
|
+
stopBy: end
|
|
34
|
+
kind: identifier
|
|
35
|
+
regex: '^(HTTPException|abort|HttpResponseServerError)$'
|
|
36
|
+
message: 'The text of an exception is sent to the client; log it and send a generic message'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## How to fix
|
|
40
|
+
|
|
41
|
+
1. Log it: `logger.exception("upload failed")`.
|
|
42
|
+
2. Return a generic message and an id the client can quote: `{"error": "internal error", "id": error_id}`.
|
|
43
|
+
|
|
44
|
+
## Examples
|
|
45
|
+
|
|
46
|
+
### Python
|
|
47
|
+
|
|
48
|
+
#### Don't
|
|
49
|
+
|
|
50
|
+
```py
|
|
51
|
+
def view():
|
|
52
|
+
try:
|
|
53
|
+
run()
|
|
54
|
+
except Exception as e:
|
|
55
|
+
return jsonify(error=str(e))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### Do
|
|
59
|
+
|
|
60
|
+
```py
|
|
61
|
+
def view():
|
|
62
|
+
try:
|
|
63
|
+
run()
|
|
64
|
+
except Exception:
|
|
65
|
+
logger.exception("run failed")
|
|
66
|
+
return jsonify(error="internal error")
|
|
67
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# sec-py/no-tls-bypass — cases
|
|
2
|
+
|
|
3
|
+
### case: verify=False
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
requests.post(url, data=d, verify=False, timeout=5)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-tls-bypass
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: verify with a bundle
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
requests.get(url, verify="/etc/ca.pem")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
(none)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: unverified context
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
ctx = ssl._create_unverified_context()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:7 sec-py/no-tls-bypass
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: check_hostname
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
ctx.check_hostname = False
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
1:1 sec-py/no-tls-bypass
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### case: CERT_NONE
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
1:1 sec-py/no-tls-bypass
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### case: old protocol
|
|
54
|
+
|
|
55
|
+
```py
|
|
56
|
+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```expect
|
|
60
|
+
1:22 sec-py/no-tls-bypass
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### case: default context
|
|
64
|
+
|
|
65
|
+
```py
|
|
66
|
+
ctx = ssl.create_default_context()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```expect
|
|
70
|
+
(none)
|
|
71
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-tls-bypass
|
|
3
|
+
title: 'TLS certificate checks stay on'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: suggest
|
|
6
|
+
fix_reason: "the server may use a private CA, which needs the CA bundle passed as `verify=` rather than the check removed"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B501, bandit/B502, bandit/B503, bandit/B504, bandit/B323, ruff/S501, ruff/S502, ruff/S503, ruff/S504, ruff/S323]
|
|
9
|
+
security: { cwe: [295], owasp: ["A04:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Certificate verification is not switched off (`verify=False`, `_create_unverified_context`), and an SSL context is not pinned to an old protocol.
|
|
14
|
+
|
|
15
|
+
**Why:** Without the check anyone on the network path can pose as the server and read or change the traffic (CWE-295).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- kind: call
|
|
21
|
+
all:
|
|
22
|
+
- has:
|
|
23
|
+
field: function
|
|
24
|
+
regex: '^(requests|httpx|session|client|s|self\.session|self\.client)\.(get|post|put|patch|delete|head|options|request|stream|send)$|^(requests|httpx)\.(Client|AsyncClient|Session)$'
|
|
25
|
+
- has:
|
|
26
|
+
field: arguments
|
|
27
|
+
has:
|
|
28
|
+
kind: keyword_argument
|
|
29
|
+
all: [{ has: { field: name, regex: '^verify$' } }, { has: { field: value, kind: 'false' } }]
|
|
30
|
+
pattern: $KW
|
|
31
|
+
- kind: call
|
|
32
|
+
all:
|
|
33
|
+
- has: { field: function, regex: '^ssl\._create_unverified_context$|^ssl\.wrap_socket$' }
|
|
34
|
+
- { kind: attribute, regex: '^ssl\.PROTOCOL_(SSLv2|SSLv3|SSLv23|TLSv1|TLSv1_1)$' }
|
|
35
|
+
- { pattern: '$C.check_hostname = False' }
|
|
36
|
+
- { pattern: '$C.verify_mode = ssl.CERT_NONE' }
|
|
37
|
+
message: 'TLS verification is off or pinned to an old protocol'
|
|
38
|
+
fix:
|
|
39
|
+
- { remove: $KW }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## How to fix
|
|
43
|
+
|
|
44
|
+
1. Delete `verify=False`; if the server uses a private CA, pass its bundle: `verify="/etc/ssl/corp-ca.pem"`.
|
|
45
|
+
2. Use `ssl.create_default_context()` instead of `_create_unverified_context()`.
|
|
46
|
+
3. Never set `check_hostname = False` or `verify_mode = ssl.CERT_NONE`; set `minimum_version = ssl.TLSVersion.TLSv1_2` instead of an old `PROTOCOL_*`.
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
|
|
50
|
+
### Python
|
|
51
|
+
|
|
52
|
+
#### Don't
|
|
53
|
+
|
|
54
|
+
```py
|
|
55
|
+
import requests
|
|
56
|
+
|
|
57
|
+
resp = requests.get(url, verify=False, timeout=5)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### Do
|
|
61
|
+
|
|
62
|
+
```py
|
|
63
|
+
import requests
|
|
64
|
+
|
|
65
|
+
resp = requests.get(url, timeout=5)
|
|
66
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# sec-py/no-trust-remote-code — cases
|
|
2
|
+
|
|
3
|
+
### case: from_pretrained
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
AutoModel.from_pretrained(n, trust_remote_code=True)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-trust-remote-code
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: false
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
AutoModel.from_pretrained(n, trust_remote_code=False)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
(none)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: a dataset
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
load_dataset("x", trust_remote_code=True)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:1 sec-py/no-trust-remote-code
|
|
31
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-trust-remote-code
|
|
3
|
+
title: 'No `trust_remote_code=True`'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "using a model that needs custom code means reviewing and vendoring that code, which is the author's call"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [94], owasp: ["A03:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
`from_pretrained`, `load_dataset` and `pipeline` are not called with `trust_remote_code=True`.
|
|
13
|
+
|
|
14
|
+
**Why:** The repository's Python files are downloaded and run on your machine, so whoever can push to it runs code in your process (CWE-94).
|
|
15
|
+
|
|
16
|
+
```codelaw
|
|
17
|
+
match:
|
|
18
|
+
all:
|
|
19
|
+
- { kind: call }
|
|
20
|
+
- has:
|
|
21
|
+
field: arguments
|
|
22
|
+
has:
|
|
23
|
+
kind: keyword_argument
|
|
24
|
+
all:
|
|
25
|
+
- { has: { field: name, regex: '^trust_remote_code$' } }
|
|
26
|
+
- { has: { field: value, kind: 'true' } }
|
|
27
|
+
message: 'This runs code from the model repository; review it and vendor it, or use a model without custom code'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## How to fix
|
|
31
|
+
|
|
32
|
+
1. Remove `trust_remote_code=True`; use a model whose architecture is built into the library.
|
|
33
|
+
2. If you need the code, read it, copy it into your repository at a pinned commit, and load that instead.
|
|
34
|
+
3. At least pin `revision=` to a commit you reviewed.
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
### Python
|
|
39
|
+
|
|
40
|
+
#### Don't
|
|
41
|
+
|
|
42
|
+
```py
|
|
43
|
+
model = AutoModel.from_pretrained(name, trust_remote_code=True)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Do
|
|
47
|
+
|
|
48
|
+
```py
|
|
49
|
+
model = AutoModel.from_pretrained(name, revision=REVISION)
|
|
50
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# sec-py/no-unsafe-deserialize — cases
|
|
2
|
+
|
|
3
|
+
### case: pickle.load
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
import pickle
|
|
7
|
+
|
|
8
|
+
pickle.load(f)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```expect
|
|
12
|
+
3:1 sec-py/no-unsafe-deserialize
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### case: dill
|
|
16
|
+
|
|
17
|
+
```py
|
|
18
|
+
dill.loads(b)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```expect
|
|
22
|
+
1:1 sec-py/no-unsafe-deserialize
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### case: marshal
|
|
26
|
+
|
|
27
|
+
```py
|
|
28
|
+
marshal.loads(b)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```expect
|
|
32
|
+
1:1 sec-py/no-unsafe-deserialize
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### case: shelve.open
|
|
36
|
+
|
|
37
|
+
```py
|
|
38
|
+
shelve.open(path)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```expect
|
|
42
|
+
1:1 sec-py/no-unsafe-deserialize
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### case: pickle.dumps is fine
|
|
46
|
+
|
|
47
|
+
```py
|
|
48
|
+
pickle.dumps(obj)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```expect
|
|
52
|
+
(none)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### case: json is fine
|
|
56
|
+
|
|
57
|
+
```py
|
|
58
|
+
json.load(f)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```expect
|
|
62
|
+
(none)
|
|
63
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-unsafe-deserialize
|
|
3
|
+
title: 'Deserialising with `pickle` and its relatives'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the replacement format (JSON, a schema, a signed payload) is a design choice"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B301, bandit/B302, bandit/B403]
|
|
9
|
+
security: { cwe: [502], owasp: ["A08:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Data is not loaded with `pickle`, `cPickle`, `dill`, `marshal`, `shelve` or `jsonpickle`.
|
|
14
|
+
|
|
15
|
+
**Why:** Loading runs code chosen by whoever wrote the bytes, so untrusted input is remote code execution (CWE-502).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
kind: call
|
|
20
|
+
all:
|
|
21
|
+
- has:
|
|
22
|
+
field: function
|
|
23
|
+
regex: '^((c|_)?[Pp]ickle|dill|cloudpickle)\.(loads?|Unpickler)$|^marshal\.loads?$|^shelve\.open$|^jsonpickle\.(decode|loads)$'
|
|
24
|
+
message: 'This loads data with a format that runs code on load'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## How to fix
|
|
28
|
+
|
|
29
|
+
1. Use JSON (`json.loads`) or another data-only format, and validate the result against a schema.
|
|
30
|
+
2. If the bytes must be a pickle, sign them (`hmac`) and verify the signature before loading, or load only files the program itself wrote.
|
|
31
|
+
3. For model weights use `safetensors`.
|
|
32
|
+
|
|
33
|
+
## Examples
|
|
34
|
+
|
|
35
|
+
### Python
|
|
36
|
+
|
|
37
|
+
#### Don't
|
|
38
|
+
|
|
39
|
+
```py
|
|
40
|
+
import pickle
|
|
41
|
+
|
|
42
|
+
obj = pickle.loads(payload)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
#### Do
|
|
46
|
+
|
|
47
|
+
```py
|
|
48
|
+
import json
|
|
49
|
+
|
|
50
|
+
obj = json.loads(payload)
|
|
51
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# sec-py/no-weak-cipher — cases
|
|
2
|
+
|
|
3
|
+
### case: ECB
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
cipher = AES.new(key, AES.MODE_ECB)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:23 sec-py/no-weak-cipher
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: DES
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
DES.new(key, DES.MODE_CBC, iv)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-weak-cipher
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: modes.ECB
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
modes.ECB()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:1 sec-py/no-weak-cipher
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: GCM
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
AES.new(key, AES.MODE_GCM)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### case: TripleDES
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
algorithms.TripleDES(key)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
1:1 sec-py/no-weak-cipher
|
|
51
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-weak-cipher
|
|
3
|
+
title: 'No DES, RC4, Blowfish or ECB'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the new cipher changes key size, nonce handling and the stored format"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B304, bandit/B305, ruff/S304, ruff/S305]
|
|
9
|
+
security: { cwe: [327], owasp: ["A04:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
A weak cipher (DES, 3DES, ARC2, ARC4, Blowfish, CAST5, IDEA) or ECB mode is not used.
|
|
14
|
+
|
|
15
|
+
**Why:** The ciphers have small blocks or known attacks, and ECB encrypts equal blocks to equal output, so the data's structure shows through (CWE-327).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- kind: call
|
|
21
|
+
all:
|
|
22
|
+
- has:
|
|
23
|
+
field: function
|
|
24
|
+
regex: '^((Crypto|Cryptodome)\.Cipher\.)?(DES|DES3|ARC2|ARC4|Blowfish|CAST|XOR)\.new$'
|
|
25
|
+
- kind: call
|
|
26
|
+
all:
|
|
27
|
+
- has:
|
|
28
|
+
field: function
|
|
29
|
+
regex: '^(algorithms\.)(TripleDES|ARC4|ARC2|Blowfish|CAST5|IDEA|SEED)$'
|
|
30
|
+
- kind: call
|
|
31
|
+
all: [{ has: { field: function, regex: '^(modes\.)?ECB$' } }]
|
|
32
|
+
- { kind: attribute, regex: '\.MODE_ECB$' }
|
|
33
|
+
message: 'This cipher or mode is weak; use AES-GCM or ChaCha20-Poly1305'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## How to fix
|
|
37
|
+
|
|
38
|
+
1. Use `cryptography`'s `AESGCM` or `ChaCha20Poly1305`: they encrypt and authenticate.
|
|
39
|
+
2. A new random nonce for every message; never reuse one with the same key.
|
|
40
|
+
3. Re-encrypt stored data under the new scheme.
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
### Python
|
|
45
|
+
|
|
46
|
+
#### Don't
|
|
47
|
+
|
|
48
|
+
```py
|
|
49
|
+
from Crypto.Cipher import AES
|
|
50
|
+
|
|
51
|
+
cipher = AES.new(key, AES.MODE_ECB)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
#### Do
|
|
55
|
+
|
|
56
|
+
```py
|
|
57
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
58
|
+
|
|
59
|
+
cipher = AESGCM(key)
|
|
60
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sec-py/no-weak-hash — cases
|
|
2
|
+
|
|
3
|
+
### case: md5
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
hashlib.md5(data)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-weak-hash
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: sha1
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
hashlib.sha1(data)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-weak-hash
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: not for security
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
hashlib.md5(data, usedforsecurity=False)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: sha256
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
hashlib.sha256(data)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|