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.
Files changed (136) hide show
  1. codelaw_rules_security_python/LICENSE +23 -0
  2. codelaw_rules_security_python/NOTICE +19 -0
  3. codelaw_rules_security_python/README.md +239 -0
  4. codelaw_rules_security_python/codelaw-pack.toml +6 -0
  5. codelaw_rules_security_python/corpus.toml +65 -0
  6. codelaw_rules_security_python/data/sources/django.json +17 -0
  7. codelaw_rules_security_python/data/sources/fastapi.json +10 -0
  8. codelaw_rules_security_python/data/sources/flask.json +14 -0
  9. codelaw_rules_security_python/presets/sec-py/recommended.toml +60 -0
  10. codelaw_rules_security_python/presets/sec-py/strict.toml +8 -0
  11. codelaw_rules_security_python/rules/sec-py/cookie-flags.cases.md +61 -0
  12. codelaw_rules_security_python/rules/sec-py/cookie-flags.md +95 -0
  13. codelaw_rules_security_python/rules/sec-py/django-settings.cases.md +61 -0
  14. codelaw_rules_security_python/rules/sec-py/django-settings.md +68 -0
  15. codelaw_rules_security_python/rules/sec-py/fastapi-cors.cases.md +31 -0
  16. codelaw_rules_security_python/rules/sec-py/fastapi-cors.md +46 -0
  17. codelaw_rules_security_python/rules/sec-py/flask-debug.cases.md +31 -0
  18. codelaw_rules_security_python/rules/sec-py/flask-debug.md +59 -0
  19. codelaw_rules_security_python/rules/sec-py/flask-settings.cases.md +51 -0
  20. codelaw_rules_security_python/rules/sec-py/flask-settings.md +84 -0
  21. codelaw_rules_security_python/rules/sec-py/hf-pin-revision.cases.md +51 -0
  22. codelaw_rules_security_python/rules/sec-py/hf-pin-revision.md +58 -0
  23. codelaw_rules_security_python/rules/sec-py/jinja-autoescape.cases.md +62 -0
  24. codelaw_rules_security_python/rules/sec-py/jinja-autoescape.md +80 -0
  25. codelaw_rules_security_python/rules/sec-py/logging-listen.cases.md +31 -0
  26. codelaw_rules_security_python/rules/sec-py/logging-listen.md +47 -0
  27. codelaw_rules_security_python/rules/sec-py/ml-unsafe-load.cases.md +61 -0
  28. codelaw_rules_security_python/rules/sec-py/ml-unsafe-load.md +68 -0
  29. codelaw_rules_security_python/rules/sec-py/no-assert-for-auth.cases.md +31 -0
  30. codelaw_rules_security_python/rules/sec-py/no-assert-for-auth.md +51 -0
  31. codelaw_rules_security_python/rules/sec-py/no-bind-all-interfaces.cases.md +41 -0
  32. codelaw_rules_security_python/rules/sec-py/no-bind-all-interfaces.md +75 -0
  33. codelaw_rules_security_python/rules/sec-py/no-cleartext-http.cases.md +41 -0
  34. codelaw_rules_security_python/rules/sec-py/no-cleartext-http.md +56 -0
  35. codelaw_rules_security_python/rules/sec-py/no-csrf-exempt.cases.md +49 -0
  36. codelaw_rules_security_python/rules/sec-py/no-csrf-exempt.md +47 -0
  37. codelaw_rules_security_python/rules/sec-py/no-dynamic-import.cases.md +35 -0
  38. codelaw_rules_security_python/rules/sec-py/no-dynamic-import.md +51 -0
  39. codelaw_rules_security_python/rules/sec-py/no-exec-eval.cases.md +51 -0
  40. codelaw_rules_security_python/rules/sec-py/no-exec-eval.md +56 -0
  41. codelaw_rules_security_python/rules/sec-py/no-format-string-injection.cases.md +35 -0
  42. codelaw_rules_security_python/rules/sec-py/no-format-string-injection.md +44 -0
  43. codelaw_rules_security_python/rules/sec-py/no-hardcoded-crypto-key.cases.md +51 -0
  44. codelaw_rules_security_python/rules/sec-py/no-hardcoded-crypto-key.md +91 -0
  45. codelaw_rules_security_python/rules/sec-py/no-hardcoded-secret.cases.md +61 -0
  46. codelaw_rules_security_python/rules/sec-py/no-hardcoded-secret.md +82 -0
  47. codelaw_rules_security_python/rules/sec-py/no-header-injection.cases.md +45 -0
  48. codelaw_rules_security_python/rules/sec-py/no-header-injection.md +55 -0
  49. codelaw_rules_security_python/rules/sec-py/no-insecure-protocol.cases.md +41 -0
  50. codelaw_rules_security_python/rules/sec-py/no-insecure-protocol.md +52 -0
  51. codelaw_rules_security_python/rules/sec-py/no-insecure-random.cases.md +41 -0
  52. codelaw_rules_security_python/rules/sec-py/no-insecure-random.md +67 -0
  53. codelaw_rules_security_python/rules/sec-py/no-insecure-tempfile.cases.md +41 -0
  54. codelaw_rules_security_python/rules/sec-py/no-insecure-tempfile.md +62 -0
  55. codelaw_rules_security_python/rules/sec-py/no-ldap-injection.cases.md +41 -0
  56. codelaw_rules_security_python/rules/sec-py/no-ldap-injection.md +51 -0
  57. codelaw_rules_security_python/rules/sec-py/no-llm-output-to-sink.cases.md +31 -0
  58. codelaw_rules_security_python/rules/sec-py/no-llm-output-to-sink.md +55 -0
  59. codelaw_rules_security_python/rules/sec-py/no-log-injection.cases.md +45 -0
  60. codelaw_rules_security_python/rules/sec-py/no-log-injection.md +52 -0
  61. codelaw_rules_security_python/rules/sec-py/no-mako.cases.md +37 -0
  62. codelaw_rules_security_python/rules/sec-py/no-mako.md +52 -0
  63. codelaw_rules_security_python/rules/sec-py/no-mark-safe.cases.md +51 -0
  64. codelaw_rules_security_python/rules/sec-py/no-mark-safe.md +54 -0
  65. codelaw_rules_security_python/rules/sec-py/no-mass-assignment.cases.md +55 -0
  66. codelaw_rules_security_python/rules/sec-py/no-mass-assignment.md +58 -0
  67. codelaw_rules_security_python/rules/sec-py/no-nosql-injection.cases.md +34 -0
  68. codelaw_rules_security_python/rules/sec-py/no-nosql-injection.md +56 -0
  69. codelaw_rules_security_python/rules/sec-py/no-open-redirect.cases.md +35 -0
  70. codelaw_rules_security_python/rules/sec-py/no-open-redirect.md +51 -0
  71. codelaw_rules_security_python/rules/sec-py/no-partial-path.cases.md +41 -0
  72. codelaw_rules_security_python/rules/sec-py/no-partial-path.md +61 -0
  73. codelaw_rules_security_python/rules/sec-py/no-path-traversal.cases.md +46 -0
  74. codelaw_rules_security_python/rules/sec-py/no-path-traversal.md +54 -0
  75. codelaw_rules_security_python/rules/sec-py/no-prompt-injection.cases.md +46 -0
  76. codelaw_rules_security_python/rules/sec-py/no-prompt-injection.md +65 -0
  77. codelaw_rules_security_python/rules/sec-py/no-pycrypto.cases.md +41 -0
  78. codelaw_rules_security_python/rules/sec-py/no-pycrypto.md +47 -0
  79. codelaw_rules_security_python/rules/sec-py/no-reflected-xss.cases.md +45 -0
  80. codelaw_rules_security_python/rules/sec-py/no-reflected-xss.md +56 -0
  81. codelaw_rules_security_python/rules/sec-py/no-regex-injection.cases.md +33 -0
  82. codelaw_rules_security_python/rules/sec-py/no-regex-injection.md +49 -0
  83. codelaw_rules_security_python/rules/sec-py/no-secret-in-log.cases.md +41 -0
  84. codelaw_rules_security_python/rules/sec-py/no-secret-in-log.md +55 -0
  85. codelaw_rules_security_python/rules/sec-py/no-shell.cases.md +55 -0
  86. codelaw_rules_security_python/rules/sec-py/no-shell.md +63 -0
  87. codelaw_rules_security_python/rules/sec-py/no-silent-except.cases.md +53 -0
  88. codelaw_rules_security_python/rules/sec-py/no-silent-except.md +54 -0
  89. codelaw_rules_security_python/rules/sec-py/no-sql-string.cases.md +91 -0
  90. codelaw_rules_security_python/rules/sec-py/no-sql-string.md +78 -0
  91. codelaw_rules_security_python/rules/sec-py/no-ssrf.cases.md +34 -0
  92. codelaw_rules_security_python/rules/sec-py/no-ssrf.md +54 -0
  93. codelaw_rules_security_python/rules/sec-py/no-stack-trace-response.cases.md +48 -0
  94. codelaw_rules_security_python/rules/sec-py/no-stack-trace-response.md +67 -0
  95. codelaw_rules_security_python/rules/sec-py/no-tls-bypass.cases.md +71 -0
  96. codelaw_rules_security_python/rules/sec-py/no-tls-bypass.md +66 -0
  97. codelaw_rules_security_python/rules/sec-py/no-trust-remote-code.cases.md +31 -0
  98. codelaw_rules_security_python/rules/sec-py/no-trust-remote-code.md +50 -0
  99. codelaw_rules_security_python/rules/sec-py/no-unsafe-deserialize.cases.md +63 -0
  100. codelaw_rules_security_python/rules/sec-py/no-unsafe-deserialize.md +51 -0
  101. codelaw_rules_security_python/rules/sec-py/no-weak-cipher.cases.md +51 -0
  102. codelaw_rules_security_python/rules/sec-py/no-weak-cipher.md +60 -0
  103. codelaw_rules_security_python/rules/sec-py/no-weak-hash.cases.md +41 -0
  104. codelaw_rules_security_python/rules/sec-py/no-weak-hash.md +54 -0
  105. codelaw_rules_security_python/rules/sec-py/no-wildcard-injection.cases.md +41 -0
  106. codelaw_rules_security_python/rules/sec-py/no-wildcard-injection.md +53 -0
  107. codelaw_rules_security_python/rules/sec-py/no-world-writable.cases.md +51 -0
  108. codelaw_rules_security_python/rules/sec-py/no-world-writable.md +57 -0
  109. codelaw_rules_security_python/rules/sec-py/no-xml-unsafe-parser.cases.md +83 -0
  110. codelaw_rules_security_python/rules/sec-py/no-xml-unsafe-parser.md +86 -0
  111. codelaw_rules_security_python/rules/sec-py/no-xmlrpc.cases.md +31 -0
  112. codelaw_rules_security_python/rules/sec-py/no-xmlrpc.md +50 -0
  113. codelaw_rules_security_python/rules/sec-py/no-xpath-injection.cases.md +41 -0
  114. codelaw_rules_security_python/rules/sec-py/no-xpath-injection.md +52 -0
  115. codelaw_rules_security_python/rules/sec-py/paramiko-host-key.cases.md +21 -0
  116. codelaw_rules_security_python/rules/sec-py/paramiko-host-key.md +44 -0
  117. codelaw_rules_security_python/rules/sec-py/password-hash-effort.cases.md +61 -0
  118. codelaw_rules_security_python/rules/sec-py/password-hash-effort.md +74 -0
  119. codelaw_rules_security_python/rules/sec-py/requests-timeout.cases.md +41 -0
  120. codelaw_rules_security_python/rules/sec-py/requests-timeout.md +63 -0
  121. codelaw_rules_security_python/rules/sec-py/tarfile-filter.cases.md +51 -0
  122. codelaw_rules_security_python/rules/sec-py/tarfile-filter.md +71 -0
  123. codelaw_rules_security_python/rules/sec-py/torch-load-weights-only.cases.md +41 -0
  124. codelaw_rules_security_python/rules/sec-py/torch-load-weights-only.md +73 -0
  125. codelaw_rules_security_python/rules/sec-py/upload-secure-filename.cases.md +41 -0
  126. codelaw_rules_security_python/rules/sec-py/upload-secure-filename.md +65 -0
  127. codelaw_rules_security_python/rules/sec-py/urlopen-scheme.cases.md +41 -0
  128. codelaw_rules_security_python/rules/sec-py/urlopen-scheme.md +54 -0
  129. codelaw_rules_security_python/rules/sec-py/weak-key-size.cases.md +51 -0
  130. codelaw_rules_security_python/rules/sec-py/weak-key-size.md +82 -0
  131. codelaw_rules_security_python/rules/sec-py/yaml-safe-load.cases.md +67 -0
  132. codelaw_rules_security_python/rules/sec-py/yaml-safe-load.md +51 -0
  133. codelaw_rules_security_python-0.3.0.dist-info/METADATA +249 -0
  134. codelaw_rules_security_python-0.3.0.dist-info/RECORD +136 -0
  135. codelaw_rules_security_python-0.3.0.dist-info/WHEEL +4 -0
  136. codelaw_rules_security_python-0.3.0.dist-info/licenses/LICENSE +23 -0
@@ -0,0 +1,31 @@
1
+ # sec-py/logging-listen — cases
2
+
3
+ ### case: listen
4
+
5
+ ```py
6
+ logging.config.listen(9999)
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/logging-listen
11
+ ```
12
+
13
+ ### case: dictConfig
14
+
15
+ ```py
16
+ logging.config.dictConfig(c)
17
+ ```
18
+
19
+ ```expect
20
+ (none)
21
+ ```
22
+
23
+ ### case: another listen
24
+
25
+ ```py
26
+ server.listen(5)
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
@@ -0,0 +1,47 @@
1
+ ---
2
+ id: sec-py/logging-listen
3
+ title: "`logging.config.listen` is not used"
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "reconfiguring a running process needs another route (a signal handler, a config reload) the author has to choose"
7
+ stability: preview
8
+ ported_from: [bandit/B612, ruff/S612]
9
+ security: { cwe: [94], owasp: ["A08:2025"], severity: high }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ `logging.config.listen()` is not called.
14
+
15
+ **Why:** It opens a socket that accepts a logging configuration, which can name any callable to run, so anyone who can connect runs code (CWE-94).
16
+
17
+ ```codelaw
18
+ match:
19
+ kind: call
20
+ all: [{ has: { field: function, regex: '^(logging\.)?config\.listen$' } }]
21
+ message: 'This opens a socket that accepts logging configuration, which can run code'
22
+ ```
23
+
24
+ ## How to fix
25
+
26
+ 1. Remove the call; load the configuration from a file at start-up with `logging.config.dictConfig`.
27
+ 2. To change levels at run time use a signal handler that re-reads the file.
28
+
29
+ ## Examples
30
+
31
+ ### Python
32
+
33
+ #### Don't
34
+
35
+ ```py
36
+ import logging.config
37
+
38
+ t = logging.config.listen(9999)
39
+ ```
40
+
41
+ #### Do
42
+
43
+ ```py
44
+ import logging.config
45
+
46
+ logging.config.dictConfig(CONFIG)
47
+ ```
@@ -0,0 +1,61 @@
1
+ # sec-py/ml-unsafe-load — cases
2
+
3
+ ### case: allow_pickle
4
+
5
+ ```py
6
+ np.load(path, allow_pickle=True)
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/ml-unsafe-load
11
+ ```
12
+
13
+ ### case: no pickle
14
+
15
+ ```py
16
+ np.load(path)
17
+ ```
18
+
19
+ ```expect
20
+ (none)
21
+ ```
22
+
23
+ ### case: joblib
24
+
25
+ ```py
26
+ joblib.load(path)
27
+ ```
28
+
29
+ ```expect
30
+ 1:1 sec-py/ml-unsafe-load
31
+ ```
32
+
33
+ ### case: pandas
34
+
35
+ ```py
36
+ pd.read_pickle(path)
37
+ ```
38
+
39
+ ```expect
40
+ 1:1 sec-py/ml-unsafe-load
41
+ ```
42
+
43
+ ### case: keras
44
+
45
+ ```py
46
+ keras.models.load_model(p, safe_mode=False)
47
+ ```
48
+
49
+ ```expect
50
+ 1:1 sec-py/ml-unsafe-load
51
+ ```
52
+
53
+ ### case: keras safe
54
+
55
+ ```py
56
+ keras.models.load_model(p)
57
+ ```
58
+
59
+ ```expect
60
+ (none)
61
+ ```
@@ -0,0 +1,68 @@
1
+ ---
2
+ id: sec-py/ml-unsafe-load
3
+ title: 'Model and array files are not unpickled'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "the replacement format (`safetensors`, `.npy` without pickle, Parquet) means converting the stored files"
7
+ stability: preview
8
+ security: { cwe: [502], owasp: ["A08:2025"], severity: high }
9
+ since: 0.2.0
10
+ ---
11
+
12
+ `numpy.load(allow_pickle=True)`, `joblib.load`, `pandas.read_pickle` and Keras `load_model(safe_mode=False)` are not used.
13
+
14
+ **Why:** Each one unpickles the file, so a model or dataset from anywhere else runs code on load (CWE-502).
15
+
16
+ ```codelaw
17
+ match:
18
+ any:
19
+ - kind: call
20
+ all:
21
+ - { has: { field: function, regex: '^(np|numpy)\.load$' } }
22
+ - has:
23
+ field: arguments
24
+ has:
25
+ kind: keyword_argument
26
+ all:
27
+ - { has: { field: name, regex: '^allow_pickle$' } }
28
+ - { has: { field: value, kind: 'true' } }
29
+ - kind: call
30
+ all: [{ has: { field: function, regex: '^joblib\.load$' } }]
31
+ - kind: call
32
+ all: [{ has: { field: function, regex: '^(pd|pandas)\.read_pickle$' } }]
33
+ - kind: call
34
+ all:
35
+ - { has: { field: function, regex: '(^|\.)load_model$' } }
36
+ - has:
37
+ field: arguments
38
+ has:
39
+ kind: keyword_argument
40
+ all: [{ has: { field: name, regex: '^safe_mode$' } }, { has: { field: value, kind: 'false' } }]
41
+ message: 'This loads a pickle, which can run code; use `safetensors` or a data-only format'
42
+ ```
43
+
44
+ ## How to fix
45
+
46
+ 1. Save and load arrays with `np.save` / `np.load` without `allow_pickle`, and tensors with `safetensors`.
47
+ 2. Store tables as Parquet or CSV, not pickle.
48
+ 3. If the file must be a pickle, load only files your own pipeline wrote and check a signature first.
49
+
50
+ ## Examples
51
+
52
+ ### Python
53
+
54
+ #### Don't
55
+
56
+ ```py
57
+ import numpy as np
58
+
59
+ arr = np.load(path, allow_pickle=True)
60
+ ```
61
+
62
+ #### Do
63
+
64
+ ```py
65
+ import numpy as np
66
+
67
+ arr = np.load(path)
68
+ ```
@@ -0,0 +1,31 @@
1
+ # sec-py/no-assert-for-auth — cases
2
+
3
+ ### case: a permission
4
+
5
+ ```py
6
+ assert user.has_perm("x")
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-assert-for-auth
11
+ ```
12
+
13
+ ### case: an invariant
14
+
15
+ ```py
16
+ assert len(items) > 0
17
+ ```
18
+
19
+ ```expect
20
+ (none)
21
+ ```
22
+
23
+ ### case: current_user
24
+
25
+ ```py
26
+ assert current_user.id == owner_id
27
+ ```
28
+
29
+ ```expect
30
+ 1:1 sec-py/no-assert-for-auth
31
+ ```
@@ -0,0 +1,51 @@
1
+ ---
2
+ id: sec-py/no-assert-for-auth
3
+ title: "`assert` is not an access check"
4
+ languages: [python]
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"
8
+ stability: preview
9
+ ported_from: [bandit/B101, ruff/S101]
10
+ security: { cwe: [703], owasp: ["A10:2025"], severity: medium }
11
+ since: 0.2.0
12
+ ---
13
+
14
+ A permission, role or authentication test is not written as an `assert`.
15
+
16
+ **Why:** `python -O` removes every `assert`, so the check disappears in an optimised run (CWE-703).
17
+
18
+ ```codelaw
19
+ match:
20
+ all:
21
+ - { kind: assert_statement }
22
+ - 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
+ message: 'An `assert` is removed by `python -O`; raise an exception for an access check'
24
+ ```
25
+
26
+ ## How to fix
27
+
28
+ 1. Replace it with `if not user.is_authenticated: raise PermissionDenied`.
29
+ 2. Use your framework's decorator (`@login_required`, `Depends(...)`) where one fits.
30
+
31
+ ## Examples
32
+
33
+ ### Python
34
+
35
+ #### Don't
36
+
37
+ ```py
38
+ def delete(request, item):
39
+ assert request.user.is_staff
40
+ ```
41
+
42
+ #### Do
43
+
44
+ ```py
45
+ from django.core.exceptions import PermissionDenied
46
+
47
+
48
+ def delete(request, item):
49
+ if not request.user.is_staff:
50
+ raise PermissionDenied
51
+ ```
@@ -0,0 +1,41 @@
1
+ # sec-py/no-bind-all-interfaces — cases
2
+
3
+ ### case: uvicorn
4
+
5
+ ```py
6
+ uvicorn.run(app, host="0.0.0.0")
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-bind-all-interfaces
11
+ ```
12
+
13
+ ### case: socket
14
+
15
+ ```py
16
+ s.bind(("0.0.0.0", 80))
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/no-bind-all-interfaces
21
+ ```
22
+
23
+ ### case: loopback
24
+
25
+ ```py
26
+ s.bind(("127.0.0.1", 80))
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
32
+
33
+ ### case: from the environment
34
+
35
+ ```py
36
+ app.run(host=HOST)
37
+ ```
38
+
39
+ ```expect
40
+ (none)
41
+ ```
@@ -0,0 +1,75 @@
1
+ ---
2
+ id: sec-py/no-bind-all-interfaces
3
+ title: 'A server does not bind to every interface in code'
4
+ languages: [python]
5
+ severity: warn
6
+ fix: manual
7
+ fix_reason: "the address belongs in configuration, with a loopback default"
8
+ stability: preview
9
+ ported_from: [bandit/B104, ruff/S104]
10
+ security: { cwe: [605], owasp: ["A02:2025"], severity: low }
11
+ since: 0.2.0
12
+ ---
13
+
14
+ A socket or server is not bound to `0.0.0.0` in the source.
15
+
16
+ **Why:** The service is then reachable from every network the machine is on, including ones that were not meant to reach it (CWE-605).
17
+
18
+ ```codelaw
19
+ match:
20
+ any:
21
+ - all:
22
+ - { kind: call }
23
+ - { has: { field: function, regex: '\.(bind|connect_ex)$' } }
24
+ - has:
25
+ field: arguments
26
+ has:
27
+ nthChild: 1
28
+ kind: tuple
29
+ has: { nthChild: 1, kind: string, regex: '^[''"](0\.0\.0\.0|::)[''"]$' }
30
+ - all:
31
+ - { kind: call }
32
+ - has:
33
+ field: arguments
34
+ has:
35
+ kind: keyword_argument
36
+ all:
37
+ - { has: { field: name, regex: '^host$' } }
38
+ - { has: { field: value, kind: string, regex: '^[''"](0\.0\.0\.0|::)[''"]$' } }
39
+ - all:
40
+ - { kind: call }
41
+ - has:
42
+ field: arguments
43
+ has:
44
+ kind: keyword_argument
45
+ all:
46
+ - { has: { field: name, regex: '^bind$' } }
47
+ - { has: { field: value, kind: string, regex: '^[''"](0\.0\.0\.0|\[::\]):[0-9]+[''"]$' } }
48
+ message: 'This binds to every network interface; read the address from configuration'
49
+ ```
50
+
51
+ ## How to fix
52
+
53
+ 1. Bind to `127.0.0.1` by default and take the address from an environment variable.
54
+ 2. A container that must listen on every interface sets it in its deployment settings, not in the code.
55
+
56
+ ## Examples
57
+
58
+ ### Python
59
+
60
+ #### Don't
61
+
62
+ ```py
63
+ import uvicorn
64
+
65
+ uvicorn.run(app, host="0.0.0.0")
66
+ ```
67
+
68
+ #### Do
69
+
70
+ ```py
71
+ import os
72
+ import uvicorn
73
+
74
+ uvicorn.run(app, host=os.environ.get("HOST", "127.0.0.1"))
75
+ ```
@@ -0,0 +1,41 @@
1
+ # sec-py/no-cleartext-http — cases
2
+
3
+ ### case: plain http
4
+
5
+ ```py
6
+ requests.get("http://example.com", timeout=5)
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-cleartext-http
11
+ ```
12
+
13
+ ### case: https
14
+
15
+ ```py
16
+ requests.get("https://example.com")
17
+ ```
18
+
19
+ ```expect
20
+ (none)
21
+ ```
22
+
23
+ ### case: localhost
24
+
25
+ ```py
26
+ requests.get("http://localhost:8000/x")
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
32
+
33
+ ### case: an f-string
34
+
35
+ ```py
36
+ httpx.post(f"http://example.com/{p}/x")
37
+ ```
38
+
39
+ ```expect
40
+ 1:1 sec-py/no-cleartext-http
41
+ ```
@@ -0,0 +1,56 @@
1
+ ---
2
+ id: sec-py/no-cleartext-http
3
+ title: 'Requests go over HTTPS'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "the server must serve HTTPS first, and the URL change has to follow it"
7
+ stability: preview
8
+ security: { cwe: [319], owasp: ["A04:2025"], severity: medium }
9
+ since: 0.2.0
10
+ ---
11
+
12
+ `requests` and `httpx` are not called with an `http://` URL, except for localhost.
13
+
14
+ **Why:** The data, and any token in it, crosses the network readable and changeable by anyone on the path (CWE-319).
15
+
16
+ ```codelaw
17
+ match:
18
+ kind: call
19
+ all:
20
+ - has:
21
+ field: function
22
+ regex: '^(requests|httpx)\.(get|post|put|patch|delete|head|options|request|stream)$'
23
+ - has:
24
+ field: arguments
25
+ has:
26
+ nthChild: 1
27
+ kind: string
28
+ regex: '^[rbf]?[''\"]http://'
29
+ not: { regex: '^[rbf]?[''\"]http://(localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0)' }
30
+ message: 'This URL is plain HTTP; use `https://`'
31
+ ```
32
+
33
+ ## How to fix
34
+
35
+ 1. Change the URL to `https://`.
36
+ 2. If the server cannot do HTTPS, put it behind a proxy that can, or reach it over a private tunnel.
37
+
38
+ ## Examples
39
+
40
+ ### Python
41
+
42
+ #### Don't
43
+
44
+ ```py
45
+ import requests
46
+
47
+ resp = requests.get("http://api.example.com/data", timeout=5)
48
+ ```
49
+
50
+ #### Do
51
+
52
+ ```py
53
+ import requests
54
+
55
+ resp = requests.get("https://api.example.com/data", timeout=5)
56
+ ```
@@ -0,0 +1,49 @@
1
+ # sec-py/no-csrf-exempt — cases
2
+
3
+ ### case: exempt
4
+
5
+ ```py
6
+ @csrf_exempt
7
+ def v(request):
8
+ pass
9
+ ```
10
+
11
+ ```expect
12
+ 1:1 sec-py/no-csrf-exempt
13
+ ```
14
+
15
+ ### case: qualified
16
+
17
+ ```py
18
+ @csrf.csrf_exempt
19
+ def v(request):
20
+ pass
21
+ ```
22
+
23
+ ```expect
24
+ 1:1 sec-py/no-csrf-exempt
25
+ ```
26
+
27
+ ### case: another decorator
28
+
29
+ ```py
30
+ @login_required
31
+ def v(request):
32
+ pass
33
+ ```
34
+
35
+ ```expect
36
+ (none)
37
+ ```
38
+
39
+ ### case: csrf_protect
40
+
41
+ ```py
42
+ @csrf_protect
43
+ def v(request):
44
+ pass
45
+ ```
46
+
47
+ ```expect
48
+ (none)
49
+ ```
@@ -0,0 +1,47 @@
1
+ ---
2
+ id: sec-py/no-csrf-exempt
3
+ title: 'No `@csrf_exempt`'
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"
7
+ stability: preview
8
+ security: { cwe: [352], owasp: ["A01:2025"], severity: high }
9
+ since: 0.2.0
10
+ ---
11
+
12
+ A view is not decorated with `@csrf_exempt`.
13
+
14
+ **Why:** Without the token check any page the user visits can submit the form as the user (CWE-352).
15
+
16
+ ```codelaw
17
+ match:
18
+ kind: decorator
19
+ regex: '^@\s*((django\.views\.decorators\.csrf\.)|(csrf\.))?csrf_exempt\b'
20
+ message: 'This view skips the CSRF check'
21
+ ```
22
+
23
+ ## How to fix
24
+
25
+ 1. Remove `@csrf_exempt` and send the token (`{% csrf_token %}`, or the `X-CSRFToken` header).
26
+ 2. For a webhook, authenticate the sender (a signed header) and keep the exemption on that one view with a comment saying why.
27
+
28
+ ## Examples
29
+
30
+ ### Python
31
+
32
+ #### Don't
33
+
34
+ ```py
35
+ from django.views.decorators.csrf import csrf_exempt
36
+
37
+ @csrf_exempt
38
+ def update(request):
39
+ ...
40
+ ```
41
+
42
+ #### Do
43
+
44
+ ```py
45
+ def update(request):
46
+ ...
47
+ ```
@@ -0,0 +1,35 @@
1
+ # sec-py/no-dynamic-import — cases
2
+
3
+ ### case: getattr
4
+
5
+ ```py
6
+ def v(request):
7
+ n = request.GET["a"]
8
+ return getattr(handlers, n)()
9
+ ```
10
+
11
+ ```expect
12
+ 3:12 sec-py/no-dynamic-import
13
+ ```
14
+
15
+ ### case: import_module
16
+
17
+ ```py
18
+ def v(request):
19
+ return importlib.import_module(request.args["m"])
20
+ ```
21
+
22
+ ```expect
23
+ 2:12 sec-py/no-dynamic-import
24
+ ```
25
+
26
+ ### case: a constant
27
+
28
+ ```py
29
+ def v(request):
30
+ return getattr(handlers, "run")
31
+ ```
32
+
33
+ ```expect
34
+ (none)
35
+ ```
@@ -0,0 +1,51 @@
1
+ ---
2
+ id: sec-py/no-dynamic-import
3
+ title: 'A request value does not choose a module or attribute'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "the allow-list of names is the author's"
7
+ stability: preview
8
+ security: { cwe: [470], owasp: ["A05:2025"], severity: high }
9
+ since: 0.2.0
10
+ ---
11
+
12
+ A value from the request is not passed to `__import__`, `importlib.import_module` or `getattr`.
13
+
14
+ **Why:** The client then picks which module is loaded or which attribute is read or called, including ones that run commands (CWE-470). 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
+ any:
19
+ - { pattern: '__import__($X)' }
20
+ - { pattern: 'importlib.import_module($X)' }
21
+ - { pattern: 'import_module($X)' }
22
+ - { pattern: 'getattr($O, $X)' }
23
+ - { pattern: 'getattr($O, $X, $D)' }
24
+ - { pattern: 'importlib.import_module($X, $P)' }
25
+ where: { $X: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
26
+ message: '`$X` comes from the request and chooses a module or attribute'
27
+ ```
28
+
29
+ ## How to fix
30
+
31
+ 1. Map the client's value to a fixed dict of allowed handlers: `HANDLERS = {"csv": export_csv}`; `HANDLERS[name]`.
32
+ 2. Reject a name that is not a key.
33
+
34
+ ## Examples
35
+
36
+ ### Python
37
+
38
+ #### Don't
39
+
40
+ ```py
41
+ def run(request):
42
+ action = request.GET["action"]
43
+ return getattr(handlers, action)()
44
+ ```
45
+
46
+ #### Do
47
+
48
+ ```py
49
+ def run(request):
50
+ return HANDLERS[request.GET["action"]]()
51
+ ```