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,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
+ ```