codelaw-rules-security-python 0.4.2__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.
Files changed (69) hide show
  1. codelaw_rules_security_python/README.md +12 -0
  2. codelaw_rules_security_python/codelaw-pack.toml +1 -1
  3. codelaw_rules_security_python/corpus.toml +5 -0
  4. codelaw_rules_security_python/rules/sec-py/django-settings.cases.md +14 -0
  5. codelaw_rules_security_python/rules/sec-py/django-settings.md +7 -9
  6. codelaw_rules_security_python/rules/sec-py/fastapi-cors.md +1 -0
  7. codelaw_rules_security_python/rules/sec-py/flask-settings.cases.md +28 -0
  8. codelaw_rules_security_python/rules/sec-py/flask-settings.md +9 -9
  9. codelaw_rules_security_python/rules/sec-py/hf-pin-revision.md +1 -0
  10. codelaw_rules_security_python/rules/sec-py/logging-listen.md +1 -0
  11. codelaw_rules_security_python/rules/sec-py/ml-unsafe-load.cases.md +14 -0
  12. codelaw_rules_security_python/rules/sec-py/ml-unsafe-load.md +5 -2
  13. codelaw_rules_security_python/rules/sec-py/no-assert-for-auth.cases.md +16 -0
  14. codelaw_rules_security_python/rules/sec-py/no-assert-for-auth.md +21 -2
  15. codelaw_rules_security_python/rules/sec-py/no-bind-all-interfaces.cases.md +38 -0
  16. codelaw_rules_security_python/rules/sec-py/no-bind-all-interfaces.md +25 -4
  17. codelaw_rules_security_python/rules/sec-py/no-cleartext-http.md +1 -0
  18. codelaw_rules_security_python/rules/sec-py/no-csrf-exempt.cases.md +17 -0
  19. codelaw_rules_security_python/rules/sec-py/no-csrf-exempt.md +13 -2
  20. codelaw_rules_security_python/rules/sec-py/no-dynamic-import.md +1 -0
  21. codelaw_rules_security_python/rules/sec-py/no-exec-eval.md +1 -0
  22. codelaw_rules_security_python/rules/sec-py/no-format-string-injection.md +1 -0
  23. codelaw_rules_security_python/rules/sec-py/no-hardcoded-crypto-key.md +1 -0
  24. codelaw_rules_security_python/rules/sec-py/no-hardcoded-secret.md +1 -0
  25. codelaw_rules_security_python/rules/sec-py/no-header-injection.cases.md +16 -0
  26. codelaw_rules_security_python/rules/sec-py/no-header-injection.md +29 -2
  27. codelaw_rules_security_python/rules/sec-py/no-insecure-protocol.md +2 -1
  28. codelaw_rules_security_python/rules/sec-py/no-ldap-injection.md +1 -0
  29. codelaw_rules_security_python/rules/sec-py/no-llm-output-to-sink.md +1 -0
  30. codelaw_rules_security_python/rules/sec-py/no-log-injection.cases.md +16 -0
  31. codelaw_rules_security_python/rules/sec-py/no-log-injection.md +11 -2
  32. codelaw_rules_security_python/rules/sec-py/no-mako.cases.md +30 -0
  33. codelaw_rules_security_python/rules/sec-py/no-mako.md +18 -3
  34. codelaw_rules_security_python/rules/sec-py/no-mark-safe.md +1 -0
  35. codelaw_rules_security_python/rules/sec-py/no-mass-assignment.md +1 -0
  36. codelaw_rules_security_python/rules/sec-py/no-nosql-injection.md +1 -0
  37. codelaw_rules_security_python/rules/sec-py/no-open-redirect.md +1 -0
  38. codelaw_rules_security_python/rules/sec-py/no-partial-path.md +1 -0
  39. codelaw_rules_security_python/rules/sec-py/no-path-traversal.md +2 -1
  40. codelaw_rules_security_python/rules/sec-py/no-prompt-injection.md +1 -0
  41. codelaw_rules_security_python/rules/sec-py/no-pycrypto.md +1 -0
  42. codelaw_rules_security_python/rules/sec-py/no-reflected-xss.md +2 -1
  43. codelaw_rules_security_python/rules/sec-py/no-secret-in-log.cases.md +27 -0
  44. codelaw_rules_security_python/rules/sec-py/no-secret-in-log.md +14 -6
  45. codelaw_rules_security_python/rules/sec-py/no-silent-except.md +1 -0
  46. codelaw_rules_security_python/rules/sec-py/no-sql-string.md +1 -0
  47. codelaw_rules_security_python/rules/sec-py/no-ssrf.md +1 -0
  48. codelaw_rules_security_python/rules/sec-py/no-stack-trace-response.md +1 -0
  49. codelaw_rules_security_python/rules/sec-py/no-trust-remote-code.cases.md +14 -0
  50. codelaw_rules_security_python/rules/sec-py/no-trust-remote-code.md +11 -2
  51. codelaw_rules_security_python/rules/sec-py/no-unsafe-deserialize.md +1 -0
  52. codelaw_rules_security_python/rules/sec-py/no-weak-cipher.md +1 -0
  53. codelaw_rules_security_python/rules/sec-py/no-wildcard-injection.md +1 -0
  54. codelaw_rules_security_python/rules/sec-py/no-xmlrpc.md +1 -0
  55. codelaw_rules_security_python/rules/sec-py/no-xpath-injection.md +1 -0
  56. codelaw_rules_security_python/rules/sec-py/paramiko-call.md +51 -0
  57. codelaw_rules_security_python/rules/sec-py/paramiko-host-key.md +10 -8
  58. codelaw_rules_security_python/rules/sec-py/password-hash-effort.md +1 -0
  59. codelaw_rules_security_python/rules/sec-py/start-process-with-no-shell.cases.md +25 -0
  60. codelaw_rules_security_python/rules/sec-py/start-process-with-no-shell.md +50 -0
  61. codelaw_rules_security_python/rules/sec-py/suspicious-httpoxy-import.md +50 -0
  62. codelaw_rules_security_python/rules/sec-py/suspicious-pyghmi-import.md +50 -0
  63. codelaw_rules_security_python/rules/sec-py/suspicious-subprocess-import.cases.md +24 -0
  64. codelaw_rules_security_python/rules/sec-py/suspicious-subprocess-import.md +51 -0
  65. codelaw_rules_security_python/rules/sec-py/urlopen-scheme.md +1 -0
  66. {codelaw_rules_security_python-0.4.2.dist-info → codelaw_rules_security_python-0.5.1.dist-info}/METADATA +13 -1
  67. {codelaw_rules_security_python-0.4.2.dist-info → codelaw_rules_security_python-0.5.1.dist-info}/RECORD +69 -62
  68. {codelaw_rules_security_python-0.4.2.dist-info → codelaw_rules_security_python-0.5.1.dist-info}/WHEEL +0 -0
  69. {codelaw_rules_security_python-0.4.2.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
 
@@ -1,5 +1,5 @@
1
1
  name = "codelaw-rules-security-python"
2
- version = "0.4.2"
2
+ version = "0.5.1"
3
3
  min_codelaw = "0.2.0"
4
4
  languages = ["python"]
5
5
  presets = ["sec-py/recommended", "sec-py/strict"]
@@ -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: manual
8
- fix_reason: "the production value comes from the deployment (environment variables, a secret manager)"
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
- DEBUG = True
58
- ALLOWED_HOSTS = ["*"]
59
+ SESSION_COOKIE_SECURE = False
59
60
  ```
60
61
 
61
62
  #### Do
62
63
 
63
64
  ```py path=settings.py
64
- import os
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: manual
7
- fix_reason: "the production values come from the deployment (environment variables, a secret manager)"
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["SECRET_KEY"] = "dev-secret-key"
77
+ app.config["SESSION_COOKIE_SECURE"] = False
76
78
  ```
77
79
 
78
80
  #### Do
79
81
 
80
82
  ```py
81
- import os
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: manual
6
- fix_reason: "the replacement format (`safetensors`, `.npy` without pickle, Parquet) means converting the stored files"
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: manual
7
- fix_reason: "the replacement raises the right exception for the framework, which only the author knows"
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: manual
7
- fix_reason: "the address belongs in configuration, with a loopback default"
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: { nthChild: 1, kind: string, regex: '^[''"](0\.0\.0\.0|::)[''"]$' }
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
- - { has: { field: value, kind: string, regex: '^[''"](0\.0\.0\.0|::)[''"]$' } }
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: manual
6
- fix_reason: "an endpoint that needs to be exempt (a webhook) needs its own authentication, which only the author can choose"
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: manual
6
- fix_reason: "whether to reject, strip or encode the value depends on what the header is for"
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