codelaw-rules-security-python 0.4.3__py3-none-any.whl → 0.5.1__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/README.md +12 -0
- codelaw_rules_security_python/codelaw-pack.toml +1 -1
- codelaw_rules_security_python/corpus.toml +5 -0
- codelaw_rules_security_python/rules/sec-py/django-settings.cases.md +14 -0
- codelaw_rules_security_python/rules/sec-py/django-settings.md +7 -9
- codelaw_rules_security_python/rules/sec-py/fastapi-cors.md +1 -0
- codelaw_rules_security_python/rules/sec-py/flask-settings.cases.md +28 -0
- codelaw_rules_security_python/rules/sec-py/flask-settings.md +9 -9
- codelaw_rules_security_python/rules/sec-py/hf-pin-revision.md +1 -0
- codelaw_rules_security_python/rules/sec-py/logging-listen.md +1 -0
- codelaw_rules_security_python/rules/sec-py/ml-unsafe-load.cases.md +14 -0
- codelaw_rules_security_python/rules/sec-py/ml-unsafe-load.md +5 -2
- codelaw_rules_security_python/rules/sec-py/no-assert-for-auth.cases.md +16 -0
- codelaw_rules_security_python/rules/sec-py/no-assert-for-auth.md +21 -2
- codelaw_rules_security_python/rules/sec-py/no-bind-all-interfaces.cases.md +38 -0
- codelaw_rules_security_python/rules/sec-py/no-bind-all-interfaces.md +25 -4
- codelaw_rules_security_python/rules/sec-py/no-cleartext-http.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-csrf-exempt.cases.md +17 -0
- codelaw_rules_security_python/rules/sec-py/no-csrf-exempt.md +13 -2
- codelaw_rules_security_python/rules/sec-py/no-dynamic-import.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-exec-eval.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-format-string-injection.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-hardcoded-crypto-key.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-hardcoded-secret.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-header-injection.cases.md +16 -0
- codelaw_rules_security_python/rules/sec-py/no-header-injection.md +29 -2
- codelaw_rules_security_python/rules/sec-py/no-insecure-protocol.md +2 -1
- codelaw_rules_security_python/rules/sec-py/no-ldap-injection.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-llm-output-to-sink.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-log-injection.cases.md +16 -0
- codelaw_rules_security_python/rules/sec-py/no-log-injection.md +11 -2
- codelaw_rules_security_python/rules/sec-py/no-mako.cases.md +30 -0
- codelaw_rules_security_python/rules/sec-py/no-mako.md +18 -3
- codelaw_rules_security_python/rules/sec-py/no-mark-safe.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-mass-assignment.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-nosql-injection.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-open-redirect.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-partial-path.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-path-traversal.md +2 -1
- codelaw_rules_security_python/rules/sec-py/no-prompt-injection.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-pycrypto.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-reflected-xss.md +2 -1
- codelaw_rules_security_python/rules/sec-py/no-secret-in-log.cases.md +27 -0
- codelaw_rules_security_python/rules/sec-py/no-secret-in-log.md +14 -6
- codelaw_rules_security_python/rules/sec-py/no-silent-except.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-sql-string.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-ssrf.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-stack-trace-response.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-trust-remote-code.cases.md +14 -0
- codelaw_rules_security_python/rules/sec-py/no-trust-remote-code.md +11 -2
- codelaw_rules_security_python/rules/sec-py/no-unsafe-deserialize.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-weak-cipher.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-wildcard-injection.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-xmlrpc.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-xpath-injection.md +1 -0
- codelaw_rules_security_python/rules/sec-py/paramiko-call.md +51 -0
- codelaw_rules_security_python/rules/sec-py/paramiko-host-key.md +10 -8
- codelaw_rules_security_python/rules/sec-py/password-hash-effort.md +1 -0
- codelaw_rules_security_python/rules/sec-py/start-process-with-no-shell.cases.md +25 -0
- codelaw_rules_security_python/rules/sec-py/start-process-with-no-shell.md +50 -0
- codelaw_rules_security_python/rules/sec-py/suspicious-httpoxy-import.md +50 -0
- codelaw_rules_security_python/rules/sec-py/suspicious-pyghmi-import.md +50 -0
- codelaw_rules_security_python/rules/sec-py/suspicious-subprocess-import.cases.md +24 -0
- codelaw_rules_security_python/rules/sec-py/suspicious-subprocess-import.md +51 -0
- codelaw_rules_security_python/rules/sec-py/urlopen-scheme.md +1 -0
- {codelaw_rules_security_python-0.4.3.dist-info → codelaw_rules_security_python-0.5.1.dist-info}/METADATA +13 -1
- {codelaw_rules_security_python-0.4.3.dist-info → codelaw_rules_security_python-0.5.1.dist-info}/RECORD +69 -62
- {codelaw_rules_security_python-0.4.3.dist-info → codelaw_rules_security_python-0.5.1.dist-info}/WHEEL +0 -0
- {codelaw_rules_security_python-0.4.3.dist-info → codelaw_rules_security_python-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -22,6 +22,18 @@ preview = true # every rule is `preview` until a corpus run promotes it
|
|
|
22
22
|
|
|
23
23
|
<!-- /docs-sync:presets -->
|
|
24
24
|
|
|
25
|
+
## The fix ladder
|
|
26
|
+
|
|
27
|
+
Each rule's fix class ([SR-LAW-4](../../records/0006_LAW-4-every-rule-has-a-fix.md)): `manual` rules by their named exception. `codelaw rules --fix-stats` prints the same table.
|
|
28
|
+
|
|
29
|
+
<!-- rule-docs:fix-ladder -->
|
|
30
|
+
|
|
31
|
+
| auto | suggest | manual | value | restructure | authored | security | waiting to move up |
|
|
32
|
+
|---|---|---|---|---|---|---|---|
|
|
33
|
+
| 0 | 29 | 37 | 10 | 9 | 1 | 17 | 0 |
|
|
34
|
+
|
|
35
|
+
<!-- /rule-docs:fix-ladder -->
|
|
36
|
+
|
|
25
37
|
`recommended` holds only high-confidence rules, at `error`; `strict` adds the
|
|
26
38
|
heuristic ones, at `warn`, so a team can start without noise.
|
|
27
39
|
|
|
@@ -54,9 +54,14 @@
|
|
|
54
54
|
"sec-py/no-xml-unsafe-parser" = 0
|
|
55
55
|
"sec-py/no-xmlrpc" = 0
|
|
56
56
|
"sec-py/no-xpath-injection" = 0
|
|
57
|
+
"sec-py/paramiko-call" = 0
|
|
57
58
|
"sec-py/paramiko-host-key" = 0
|
|
58
59
|
"sec-py/password-hash-effort" = 0
|
|
59
60
|
"sec-py/requests-timeout" = 4
|
|
61
|
+
"sec-py/start-process-with-no-shell" = 0
|
|
62
|
+
"sec-py/suspicious-httpoxy-import" = 0
|
|
63
|
+
"sec-py/suspicious-pyghmi-import" = 0
|
|
64
|
+
"sec-py/suspicious-subprocess-import" = 3
|
|
60
65
|
"sec-py/tarfile-filter" = 0
|
|
61
66
|
"sec-py/torch-load-weights-only" = 0
|
|
62
67
|
"sec-py/upload-secure-filename" = 0
|
|
@@ -59,3 +59,17 @@ CSRF_COOKIE_SECURE = True
|
|
|
59
59
|
```expect
|
|
60
60
|
(none)
|
|
61
61
|
```
|
|
62
|
+
|
|
63
|
+
### case: the insecure cookie is turned on
|
|
64
|
+
|
|
65
|
+
```py path=settings.py
|
|
66
|
+
SESSION_COOKIE_SECURE = False
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```expect
|
|
70
|
+
1:1 sec-py/django-settings
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```fixed
|
|
74
|
+
SESSION_COOKIE_SECURE = True
|
|
75
|
+
```
|
|
@@ -4,8 +4,8 @@ title: 'Django settings are safe'
|
|
|
4
4
|
languages: [python]
|
|
5
5
|
files: ["**/settings.py", "**/settings/**/*.py", "**/*_settings.py", "**/settings_*.py"]
|
|
6
6
|
exclude: ["**/*dev*.py", "**/*local*.py", "**/*test*.py"]
|
|
7
|
-
fix:
|
|
8
|
-
fix_reason: "the
|
|
7
|
+
fix: suggest
|
|
8
|
+
fix_reason: "the reviewer confirms each cookie or redirect flag turned on suits the deployment; DEBUG, SECRET_KEY and ALLOWED_HOSTS get no edit (the value comes from the deployment)"
|
|
9
9
|
stability: preview
|
|
10
10
|
ported_from: [ruff/S105]
|
|
11
11
|
security: { cwe: [489, 798, 614], owasp: ["A02:2025"], severity: high }
|
|
@@ -36,8 +36,10 @@ match:
|
|
|
36
36
|
- has:
|
|
37
37
|
field: left
|
|
38
38
|
regex: '^(SESSION_COOKIE_SECURE|CSRF_COOKIE_SECURE|SECURE_SSL_REDIRECT|SESSION_COOKIE_HTTPONLY)$'
|
|
39
|
-
- { has: { field: right, kind: 'false' } }
|
|
39
|
+
- { has: { field: right, kind: 'false', pattern: $B } }
|
|
40
40
|
message: 'This Django setting is unsafe outside development'
|
|
41
|
+
fix:
|
|
42
|
+
- replace: { target: $B, with: 'True' }
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
## How to fix
|
|
@@ -54,15 +56,11 @@ message: 'This Django setting is unsafe outside development'
|
|
|
54
56
|
#### Don't
|
|
55
57
|
|
|
56
58
|
```py path=settings.py
|
|
57
|
-
|
|
58
|
-
ALLOWED_HOSTS = ["*"]
|
|
59
|
+
SESSION_COOKIE_SECURE = False
|
|
59
60
|
```
|
|
60
61
|
|
|
61
62
|
#### Do
|
|
62
63
|
|
|
63
64
|
```py path=settings.py
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
DEBUG = os.environ.get("DJANGO_DEBUG") == "1"
|
|
67
|
-
ALLOWED_HOSTS = ["app.example.com"]
|
|
65
|
+
SESSION_COOKIE_SECURE = True
|
|
68
66
|
```
|
|
@@ -4,6 +4,7 @@ title: 'CORS does not allow every origin with credentials'
|
|
|
4
4
|
languages: [python]
|
|
5
5
|
fix: manual
|
|
6
6
|
fix_reason: "the list of trusted origins is the application's own"
|
|
7
|
+
fix_exception: value
|
|
7
8
|
stability: preview
|
|
8
9
|
security: { cwe: [942], owasp: ["A02:2025"], severity: high }
|
|
9
10
|
since: 0.2.0
|
|
@@ -49,3 +49,31 @@ app.config["SESSION_COOKIE_SECURE"] = True
|
|
|
49
49
|
```expect
|
|
50
50
|
(none)
|
|
51
51
|
```
|
|
52
|
+
|
|
53
|
+
### case: a keyword flag is turned on
|
|
54
|
+
|
|
55
|
+
```py
|
|
56
|
+
app.config.update(SESSION_COOKIE_SECURE=False)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```expect
|
|
60
|
+
1:1 sec-py/flask-settings
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```fixed
|
|
64
|
+
app.config.update(SESSION_COOKIE_SECURE=True)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### case: an assigned flag is turned on
|
|
68
|
+
|
|
69
|
+
```py
|
|
70
|
+
app.config["WTF_CSRF_ENABLED"] = False
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```expect
|
|
74
|
+
1:1 sec-py/flask-settings
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```fixed
|
|
78
|
+
app.config["WTF_CSRF_ENABLED"] = True
|
|
79
|
+
```
|
|
@@ -3,8 +3,8 @@ id: sec-py/flask-settings
|
|
|
3
3
|
title: 'Flask configuration is safe'
|
|
4
4
|
languages: [python]
|
|
5
5
|
exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
|
|
6
|
-
fix:
|
|
7
|
-
fix_reason: "the
|
|
6
|
+
fix: suggest
|
|
7
|
+
fix_reason: "the reviewer confirms each flag turned on suits the deployment; a literal SECRET_KEY gets no edit (the value comes from the deployment)"
|
|
8
8
|
stability: preview
|
|
9
9
|
security: { cwe: [798, 352, 614], owasp: ["A02:2025"], severity: high }
|
|
10
10
|
since: 0.2.0
|
|
@@ -30,7 +30,7 @@ match:
|
|
|
30
30
|
- has:
|
|
31
31
|
field: left
|
|
32
32
|
regex: 'config\[\s*[''"](WTF_CSRF_ENABLED|WTF_CSRF_CHECK_DEFAULT|SESSION_COOKIE_SECURE|SESSION_COOKIE_HTTPONLY)[''"]\s*\]$'
|
|
33
|
-
- { has: { field: right, kind: 'false' } }
|
|
33
|
+
- { has: { field: right, kind: 'false', pattern: $B } }
|
|
34
34
|
- kind: call
|
|
35
35
|
all:
|
|
36
36
|
- { has: { field: function, regex: '\.config\.(update|from_mapping)$' } }
|
|
@@ -48,15 +48,17 @@ match:
|
|
|
48
48
|
kind: keyword_argument
|
|
49
49
|
all:
|
|
50
50
|
- { has: { field: name, regex: '^WTF_CSRF_ENABLED$' } }
|
|
51
|
-
- { has: { field: value, kind: 'false' } }
|
|
51
|
+
- { has: { field: value, kind: 'false', pattern: $B } }
|
|
52
52
|
- has:
|
|
53
53
|
field: arguments
|
|
54
54
|
has:
|
|
55
55
|
kind: keyword_argument
|
|
56
56
|
all:
|
|
57
57
|
- { has: { field: name, regex: '^SESSION_COOKIE_SECURE$' } }
|
|
58
|
-
- { has: { field: value, kind: 'false' } }
|
|
58
|
+
- { has: { field: value, kind: 'false', pattern: $B } }
|
|
59
59
|
message: 'This Flask setting is unsafe outside development'
|
|
60
|
+
fix:
|
|
61
|
+
- replace: { target: $B, with: 'True' }
|
|
60
62
|
```
|
|
61
63
|
|
|
62
64
|
## How to fix
|
|
@@ -72,13 +74,11 @@ message: 'This Flask setting is unsafe outside development'
|
|
|
72
74
|
#### Don't
|
|
73
75
|
|
|
74
76
|
```py
|
|
75
|
-
app.config["
|
|
77
|
+
app.config["SESSION_COOKIE_SECURE"] = False
|
|
76
78
|
```
|
|
77
79
|
|
|
78
80
|
#### Do
|
|
79
81
|
|
|
80
82
|
```py
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
app.config["SECRET_KEY"] = os.environ["FLASK_SECRET_KEY"]
|
|
83
|
+
app.config["SESSION_COOKIE_SECURE"] = True
|
|
84
84
|
```
|
|
@@ -4,6 +4,7 @@ title: 'Hugging Face downloads pin a revision'
|
|
|
4
4
|
languages: [python]
|
|
5
5
|
fix: manual
|
|
6
6
|
fix_reason: "the commit hash to pin is a choice the author makes after reviewing the model"
|
|
7
|
+
fix_exception: value
|
|
7
8
|
stability: preview
|
|
8
9
|
ported_from: [bandit/B615]
|
|
9
10
|
security: { cwe: [494], owasp: ["A03:2025"], severity: medium }
|
|
@@ -4,6 +4,7 @@ title: "`logging.config.listen` is not used"
|
|
|
4
4
|
languages: [python]
|
|
5
5
|
fix: manual
|
|
6
6
|
fix_reason: "reconfiguring a running process needs another route (a signal handler, a config reload) the author has to choose"
|
|
7
|
+
fix_exception: restructure
|
|
7
8
|
stability: preview
|
|
8
9
|
ported_from: [bandit/B612, ruff/S612]
|
|
9
10
|
security: { cwe: [94], owasp: ["A08:2025"], severity: high }
|
|
@@ -59,3 +59,17 @@ keras.models.load_model(p)
|
|
|
59
59
|
```expect
|
|
60
60
|
(none)
|
|
61
61
|
```
|
|
62
|
+
|
|
63
|
+
### case: allow_pickle is dropped with its comma
|
|
64
|
+
|
|
65
|
+
```py
|
|
66
|
+
arr = np.load(path, allow_pickle=True, mmap_mode="r")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```expect
|
|
70
|
+
1:7 sec-py/ml-unsafe-load
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```fixed
|
|
74
|
+
arr = np.load(path, mmap_mode="r")
|
|
75
|
+
```
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
id: sec-py/ml-unsafe-load
|
|
3
3
|
title: 'Model and array files are not unpickled'
|
|
4
4
|
languages: [python]
|
|
5
|
-
fix:
|
|
6
|
-
fix_reason: "the
|
|
5
|
+
fix: suggest
|
|
6
|
+
fix_reason: "the reviewer confirms the arrays are not object arrays, which stop loading once allow_pickle is dropped; the pickle loaders (joblib, read_pickle) and safe_mode=False get no edit (the files must be converted)"
|
|
7
7
|
stability: preview
|
|
8
8
|
security: { cwe: [502], owasp: ["A08:2025"], severity: high }
|
|
9
9
|
since: 0.2.0
|
|
@@ -23,6 +23,7 @@ match:
|
|
|
23
23
|
field: arguments
|
|
24
24
|
has:
|
|
25
25
|
kind: keyword_argument
|
|
26
|
+
pattern: $K
|
|
26
27
|
all:
|
|
27
28
|
- { has: { field: name, regex: '^allow_pickle$' } }
|
|
28
29
|
- { has: { field: value, kind: 'true' } }
|
|
@@ -39,6 +40,8 @@ match:
|
|
|
39
40
|
kind: keyword_argument
|
|
40
41
|
all: [{ has: { field: name, regex: '^safe_mode$' } }, { has: { field: value, kind: 'false' } }]
|
|
41
42
|
message: 'This loads a pickle, which can run code; use `safetensors` or a data-only format'
|
|
43
|
+
fix:
|
|
44
|
+
- remove: $K
|
|
42
45
|
```
|
|
43
46
|
|
|
44
47
|
## How to fix
|
|
@@ -29,3 +29,19 @@ assert current_user.id == owner_id
|
|
|
29
29
|
```expect
|
|
30
30
|
1:1 sec-py/no-assert-for-auth
|
|
31
31
|
```
|
|
32
|
+
|
|
33
|
+
### case: the message goes into the exception
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
def delete(request):
|
|
37
|
+
assert request.user.is_staff, "staff only"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```expect
|
|
41
|
+
2:5 sec-py/no-assert-for-auth
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```fixed
|
|
45
|
+
def delete(request):
|
|
46
|
+
if not (request.user.is_staff): raise AssertionError("staff only")
|
|
47
|
+
```
|
|
@@ -3,8 +3,8 @@ id: sec-py/no-assert-for-auth
|
|
|
3
3
|
title: "`assert` is not an access check"
|
|
4
4
|
languages: [python]
|
|
5
5
|
exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
|
|
6
|
-
fix:
|
|
7
|
-
fix_reason: "the
|
|
6
|
+
fix: suggest
|
|
7
|
+
fix_reason: "the edit raises AssertionError so the check survives `python -O`, which changes behaviour on purpose; the reviewer confirms it and may pick the framework's exception (PermissionDenied, 403) instead"
|
|
8
8
|
stability: preview
|
|
9
9
|
ported_from: [bandit/B101, ruff/S101]
|
|
10
10
|
security: { cwe: [703], owasp: ["A10:2025"], severity: medium }
|
|
@@ -19,8 +19,20 @@ A permission, role or authentication test is not written as an `assert`.
|
|
|
19
19
|
match:
|
|
20
20
|
all:
|
|
21
21
|
- { kind: assert_statement }
|
|
22
|
+
- any:
|
|
23
|
+
- all:
|
|
24
|
+
- { pattern: $S1 }
|
|
25
|
+
- { has: { nthChild: 1, pattern: $X } }
|
|
26
|
+
- not: { has: { nthChild: 2 } }
|
|
27
|
+
- all:
|
|
28
|
+
- { pattern: $S2 }
|
|
29
|
+
- { has: { nthChild: 1, pattern: $X } }
|
|
30
|
+
- { has: { nthChild: 2, pattern: $M } }
|
|
22
31
|
- regex: '(?i)(is_authenticated|is_admin|is_staff|is_superuser|has_perm|has_permission|permission|authorized|authenticated|is_owner|\.role\b|\.roles\b|current_user|user\.id\s*==)'
|
|
23
32
|
message: 'An `assert` is removed by `python -O`; raise an exception for an access check'
|
|
33
|
+
fix:
|
|
34
|
+
- replace: { target: $S1, with: 'if not ($X): raise AssertionError' }
|
|
35
|
+
- replace: { target: $S2, with: 'if not ($X): raise AssertionError($M)' }
|
|
24
36
|
```
|
|
25
37
|
|
|
26
38
|
## How to fix
|
|
@@ -39,6 +51,13 @@ def delete(request, item):
|
|
|
39
51
|
assert request.user.is_staff
|
|
40
52
|
```
|
|
41
53
|
|
|
54
|
+
#### Fixed
|
|
55
|
+
|
|
56
|
+
```py
|
|
57
|
+
def delete(request, item):
|
|
58
|
+
if not (request.user.is_staff): raise AssertionError
|
|
59
|
+
```
|
|
60
|
+
|
|
42
61
|
#### Do
|
|
43
62
|
|
|
44
63
|
```py
|
|
@@ -39,3 +39,41 @@ app.run(host=HOST)
|
|
|
39
39
|
```expect
|
|
40
40
|
(none)
|
|
41
41
|
```
|
|
42
|
+
|
|
43
|
+
### case: the IPv4 wildcard becomes loopback
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
s.bind(("0.0.0.0", 80))
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
1:1 sec-py/no-bind-all-interfaces
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```fixed
|
|
54
|
+
s.bind(("127.0.0.1", 80))
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### case: the IPv6 wildcard becomes its loopback
|
|
58
|
+
|
|
59
|
+
```py
|
|
60
|
+
app.run(host="::")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```expect
|
|
64
|
+
1:1 sec-py/no-bind-all-interfaces
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```fixed
|
|
68
|
+
app.run(host="::1")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### case: a host and port string has no edit
|
|
72
|
+
|
|
73
|
+
```py
|
|
74
|
+
server.run(bind="0.0.0.0:8000")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```expect
|
|
78
|
+
1:1 sec-py/no-bind-all-interfaces
|
|
79
|
+
```
|
|
@@ -3,8 +3,8 @@ id: sec-py/no-bind-all-interfaces
|
|
|
3
3
|
title: 'A server does not bind to every interface in code'
|
|
4
4
|
languages: [python]
|
|
5
5
|
severity: warn
|
|
6
|
-
fix:
|
|
7
|
-
fix_reason: "the address
|
|
6
|
+
fix: suggest
|
|
7
|
+
fix_reason: "the reviewer confirms the loopback address suits the deployment (a container needs the address from its configuration); a host:port string for bind= gets no edit"
|
|
8
8
|
stability: preview
|
|
9
9
|
ported_from: [bandit/B104, ruff/S104]
|
|
10
10
|
security: { cwe: [605], owasp: ["A02:2025"], severity: low }
|
|
@@ -26,7 +26,12 @@ match:
|
|
|
26
26
|
has:
|
|
27
27
|
nthChild: 1
|
|
28
28
|
kind: tuple
|
|
29
|
-
has:
|
|
29
|
+
has:
|
|
30
|
+
nthChild: 1
|
|
31
|
+
kind: string
|
|
32
|
+
any:
|
|
33
|
+
- { regex: '^[''"]0\.0\.0\.0[''"]$', pattern: $A }
|
|
34
|
+
- { regex: '^[''"]::[''"]$', pattern: $B }
|
|
30
35
|
- all:
|
|
31
36
|
- { kind: call }
|
|
32
37
|
- has:
|
|
@@ -35,7 +40,12 @@ match:
|
|
|
35
40
|
kind: keyword_argument
|
|
36
41
|
all:
|
|
37
42
|
- { has: { field: name, regex: '^host$' } }
|
|
38
|
-
-
|
|
43
|
+
- has:
|
|
44
|
+
field: value
|
|
45
|
+
kind: string
|
|
46
|
+
any:
|
|
47
|
+
- { regex: '^[''"]0\.0\.0\.0[''"]$', pattern: $A }
|
|
48
|
+
- { regex: '^[''"]::[''"]$', pattern: $B }
|
|
39
49
|
- all:
|
|
40
50
|
- { kind: call }
|
|
41
51
|
- has:
|
|
@@ -46,6 +56,9 @@ match:
|
|
|
46
56
|
- { has: { field: name, regex: '^bind$' } }
|
|
47
57
|
- { has: { field: value, kind: string, regex: '^[''"](0\.0\.0\.0|\[::\]):[0-9]+[''"]$' } }
|
|
48
58
|
message: 'This binds to every network interface; read the address from configuration'
|
|
59
|
+
fix:
|
|
60
|
+
- replace: { target: $A, with: '"127.0.0.1"' }
|
|
61
|
+
- replace: { target: $B, with: '"::1"' }
|
|
49
62
|
```
|
|
50
63
|
|
|
51
64
|
## How to fix
|
|
@@ -65,6 +78,14 @@ import uvicorn
|
|
|
65
78
|
uvicorn.run(app, host="0.0.0.0")
|
|
66
79
|
```
|
|
67
80
|
|
|
81
|
+
#### Fixed
|
|
82
|
+
|
|
83
|
+
```py
|
|
84
|
+
import uvicorn
|
|
85
|
+
|
|
86
|
+
uvicorn.run(app, host="127.0.0.1")
|
|
87
|
+
```
|
|
88
|
+
|
|
68
89
|
#### Do
|
|
69
90
|
|
|
70
91
|
```py
|
|
@@ -4,6 +4,7 @@ title: 'Requests go over HTTPS'
|
|
|
4
4
|
languages: [python]
|
|
5
5
|
fix: manual
|
|
6
6
|
fix_reason: "the server must serve HTTPS first, and the URL change has to follow it"
|
|
7
|
+
fix_exception: security
|
|
7
8
|
stability: preview
|
|
8
9
|
security: { cwe: [319], owasp: ["A04:2025"], severity: medium }
|
|
9
10
|
since: 0.2.0
|
|
@@ -47,3 +47,20 @@ def v(request):
|
|
|
47
47
|
```expect
|
|
48
48
|
(none)
|
|
49
49
|
```
|
|
50
|
+
|
|
51
|
+
### case: the decorator line is removed
|
|
52
|
+
|
|
53
|
+
```py
|
|
54
|
+
@csrf_exempt
|
|
55
|
+
def update(request):
|
|
56
|
+
...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```expect
|
|
60
|
+
1:1 sec-py/no-csrf-exempt
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```fixed
|
|
64
|
+
def update(request):
|
|
65
|
+
...
|
|
66
|
+
```
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
id: sec-py/no-csrf-exempt
|
|
3
3
|
title: 'No `@csrf_exempt`'
|
|
4
4
|
languages: [python]
|
|
5
|
-
fix:
|
|
6
|
-
fix_reason: "
|
|
5
|
+
fix: suggest
|
|
6
|
+
fix_reason: "removing the decorator makes the view check CSRF, so a webhook stops working until it has its own authentication; the reviewer confirms the view is not one, and removes the import if nothing else uses it"
|
|
7
7
|
stability: preview
|
|
8
8
|
security: { cwe: [352], owasp: ["A01:2025"], severity: high }
|
|
9
9
|
since: 0.2.0
|
|
@@ -18,6 +18,8 @@ match:
|
|
|
18
18
|
kind: decorator
|
|
19
19
|
regex: '^@\s*((django\.views\.decorators\.csrf\.)|(csrf\.))?csrf_exempt\b'
|
|
20
20
|
message: 'This view skips the CSRF check'
|
|
21
|
+
fix:
|
|
22
|
+
- remove: $$MATCH
|
|
21
23
|
```
|
|
22
24
|
|
|
23
25
|
## How to fix
|
|
@@ -39,6 +41,15 @@ def update(request):
|
|
|
39
41
|
...
|
|
40
42
|
```
|
|
41
43
|
|
|
44
|
+
#### Fixed
|
|
45
|
+
|
|
46
|
+
```py
|
|
47
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
48
|
+
|
|
49
|
+
def update(request):
|
|
50
|
+
...
|
|
51
|
+
```
|
|
52
|
+
|
|
42
53
|
#### Do
|
|
43
54
|
|
|
44
55
|
```py
|
|
@@ -4,6 +4,7 @@ title: 'A request value does not choose a module or attribute'
|
|
|
4
4
|
languages: [python]
|
|
5
5
|
fix: manual
|
|
6
6
|
fix_reason: "the allow-list of names is the author's"
|
|
7
|
+
fix_exception: value
|
|
7
8
|
stability: preview
|
|
8
9
|
security: { cwe: [470], owasp: ["A05:2025"], severity: high }
|
|
9
10
|
since: 0.2.0
|
|
@@ -4,6 +4,7 @@ title: "`exec`, `eval` and `compile` of a non-literal"
|
|
|
4
4
|
languages: [python]
|
|
5
5
|
fix: manual
|
|
6
6
|
fix_reason: "the safe replacement (`ast.literal_eval`, a lookup table, a parser) depends on what the string is for"
|
|
7
|
+
fix_exception: security
|
|
7
8
|
stability: preview
|
|
8
9
|
ported_from: [bandit/B102, bandit/B307]
|
|
9
10
|
security: { cwe: [94], owasp: ["A05:2025"], severity: high }
|
|
@@ -4,6 +4,7 @@ title: 'A request value is not a format string'
|
|
|
4
4
|
languages: [python]
|
|
5
5
|
fix: manual
|
|
6
6
|
fix_reason: "the template has to be a literal, which needs the format string moved out of the request"
|
|
7
|
+
fix_exception: authored
|
|
7
8
|
stability: preview
|
|
8
9
|
security: { cwe: [134], owasp: ["A05:2025"], severity: high }
|
|
9
10
|
since: 0.2.0
|
|
@@ -5,6 +5,7 @@ languages: [python]
|
|
|
5
5
|
exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
|
|
6
6
|
fix: manual
|
|
7
7
|
fix_reason: "where the key comes from (a key store, an environment variable) is a deployment decision"
|
|
8
|
+
fix_exception: value
|
|
8
9
|
stability: preview
|
|
9
10
|
security: { cwe: [321, 329], owasp: ["A04:2025"], severity: high }
|
|
10
11
|
since: 0.2.0
|
|
@@ -5,6 +5,7 @@ languages: [python]
|
|
|
5
5
|
exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
|
|
6
6
|
fix: manual
|
|
7
7
|
fix_reason: "where the secret lives instead (environment, secret manager) is a decision, and the leaked one must be rotated"
|
|
8
|
+
fix_exception: value
|
|
8
9
|
stability: preview
|
|
9
10
|
ported_from: [bandit/B105, bandit/B106, bandit/B107, ruff/S105, ruff/S106, ruff/S107]
|
|
10
11
|
security: { cwe: [798], owasp: ["A07:2025"], severity: high }
|
|
@@ -43,3 +43,19 @@ def v(request):
|
|
|
43
43
|
```expect
|
|
44
44
|
(none)
|
|
45
45
|
```
|
|
46
|
+
|
|
47
|
+
### case: the default choice strips line breaks
|
|
48
|
+
|
|
49
|
+
```py
|
|
50
|
+
def v(request):
|
|
51
|
+
resp.headers["X-A"] = request.args["a"]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```expect
|
|
55
|
+
2:5 sec-py/no-header-injection
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```fixed
|
|
59
|
+
def v(request):
|
|
60
|
+
resp.headers["X-A"] = str(request.args["a"]).replace("\r", "").replace("\n", "")
|
|
61
|
+
```
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
id: sec-py/no-header-injection
|
|
3
3
|
title: 'A request value is not a response header'
|
|
4
4
|
languages: [python]
|
|
5
|
-
fix:
|
|
6
|
-
fix_reason: "
|
|
5
|
+
fix: suggest
|
|
6
|
+
fix_reason: "stripping the line breaks is one of two safe forms; the reviewer picks between them, or rejects the value or encodes it, and confirms the value is meant to be one header line"
|
|
7
7
|
stability: preview
|
|
8
8
|
security: { cwe: [113], owasp: ["A05:2025"], severity: medium }
|
|
9
9
|
since: 0.2.0
|
|
@@ -24,6 +24,15 @@ match:
|
|
|
24
24
|
constraints: { R: { regex: '^(resp|response|res|r|rv|rsp|self\.response|make_response\(.*\))$' } }
|
|
25
25
|
where: { $V: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
|
|
26
26
|
message: '`$V` comes from the request and is written into a header'
|
|
27
|
+
fix:
|
|
28
|
+
choices:
|
|
29
|
+
- label: strip line breaks
|
|
30
|
+
default: true
|
|
31
|
+
fix:
|
|
32
|
+
- replace: { target: $V, with: 'str($V).replace("\r", "").replace("\n", "")' }
|
|
33
|
+
- label: join lines with a space
|
|
34
|
+
fix:
|
|
35
|
+
- replace: { target: $V, with: '" ".join(str($V).splitlines())' }
|
|
27
36
|
```
|
|
28
37
|
|
|
29
38
|
## How to fix
|
|
@@ -45,6 +54,24 @@ def view(request):
|
|
|
45
54
|
return resp
|
|
46
55
|
```
|
|
47
56
|
|
|
57
|
+
#### Fixed (strip line breaks)
|
|
58
|
+
|
|
59
|
+
```py
|
|
60
|
+
def view(request):
|
|
61
|
+
resp = make_response("ok")
|
|
62
|
+
resp.headers["X-Name"] = str(request.args["name"]).replace("\r", "").replace("\n", "")
|
|
63
|
+
return resp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Fixed (join lines with a space)
|
|
67
|
+
|
|
68
|
+
```py
|
|
69
|
+
def view(request):
|
|
70
|
+
resp = make_response("ok")
|
|
71
|
+
resp.headers["X-Name"] = " ".join(str(request.args["name"]).splitlines())
|
|
72
|
+
return resp
|
|
73
|
+
```
|
|
74
|
+
|
|
48
75
|
#### Do
|
|
49
76
|
|
|
50
77
|
```py
|
|
@@ -3,7 +3,8 @@ id: sec-py/no-insecure-protocol
|
|
|
3
3
|
title: 'No Telnet, FTP or SNMP v1/v2'
|
|
4
4
|
languages: [python]
|
|
5
5
|
fix: manual
|
|
6
|
-
fix_reason: "the replacement (SSH or SFTP, FTPS, SNMPv3) needs the server to support it"
|
|
6
|
+
fix_reason: "the finding is the import or the community string; the replacement (SSH or SFTP, FTPS, SNMPv3) is a different client and needs the server to support it"
|
|
7
|
+
fix_exception: restructure
|
|
7
8
|
stability: preview
|
|
8
9
|
ported_from: [bandit/B312, bandit/B321, bandit/B401, bandit/B402, bandit/B508, bandit/B509, ruff/S401, ruff/S402, ruff/S312, ruff/S321, ruff/S508, ruff/S509]
|
|
9
10
|
security: { cwe: [319], owasp: ["A04:2025"], severity: high }
|
|
@@ -4,6 +4,7 @@ title: 'LDAP filters are not built from strings'
|
|
|
4
4
|
languages: [python]
|
|
5
5
|
fix: manual
|
|
6
6
|
fix_reason: "the escaping function belongs to the LDAP library in use"
|
|
7
|
+
fix_exception: security
|
|
7
8
|
stability: preview
|
|
8
9
|
security: { cwe: [90], owasp: ["A05:2025"], severity: high }
|
|
9
10
|
since: 0.2.0
|
|
@@ -5,6 +5,7 @@ languages: [python]
|
|
|
5
5
|
severity: warn
|
|
6
6
|
fix: manual
|
|
7
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
|
+
fix_exception: restructure
|
|
8
9
|
stability: preview
|
|
9
10
|
security: { cwe: [94], owasp: ["A05:2025"], severity: high }
|
|
10
11
|
since: 0.2.0
|