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,61 @@
1
+ # sec-py/password-hash-effort — cases
2
+
3
+ ### case: sha256 of a password
4
+
5
+ ```py
6
+ hashlib.sha256(password.encode())
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/password-hash-effort
11
+ ```
12
+
13
+ ### case: few iterations
14
+
15
+ ```py
16
+ hashlib.pbkdf2_hmac("sha256", pw, salt, iterations=100000)
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/password-hash-effort
21
+ ```
22
+
23
+ ### case: positional few iterations
24
+
25
+ ```py
26
+ hashlib.pbkdf2_hmac("sha256", pw, salt, 1000)
27
+ ```
28
+
29
+ ```expect
30
+ 1:1 sec-py/password-hash-effort
31
+ ```
32
+
33
+ ### case: enough iterations
34
+
35
+ ```py
36
+ hashlib.pbkdf2_hmac("sha256", pw, salt, 600000)
37
+ ```
38
+
39
+ ```expect
40
+ (none)
41
+ ```
42
+
43
+ ### case: bcrypt rounds
44
+
45
+ ```py
46
+ bcrypt.gensalt(rounds=4)
47
+ ```
48
+
49
+ ```expect
50
+ 1:1 sec-py/password-hash-effort
51
+ ```
52
+
53
+ ### case: sha256 of a file
54
+
55
+ ```py
56
+ hashlib.sha256(data)
57
+ ```
58
+
59
+ ```expect
60
+ (none)
61
+ ```
@@ -0,0 +1,74 @@
1
+ ---
2
+ id: sec-py/password-hash-effort
3
+ title: 'Passwords are hashed with a slow function'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "moving to Argon2 or bcrypt means rehashing at the next login and changing the stored format"
7
+ stability: preview
8
+ security: { cwe: [916], owasp: ["A04:2025"], severity: high }
9
+ since: 0.2.0
10
+ ---
11
+
12
+ A password is not hashed with a plain `hashlib` digest, `pbkdf2_hmac` runs at least 600 000 iterations, and `bcrypt` uses at least 10 rounds.
13
+
14
+ **Why:** A fast hash lets an attacker who has the table test billions of guesses per second (CWE-916).
15
+
16
+ ```codelaw
17
+ match:
18
+ any:
19
+ - kind: call
20
+ all:
21
+ - { has: { field: function, regex: '^hashlib\.(md5|sha1|sha224|sha256|sha384|sha512)$' } }
22
+ - { has: { field: arguments, has: { nthChild: 1, regex: '(?i)pass(word|wd)?' } } }
23
+ - kind: call
24
+ all:
25
+ - { has: { field: function, regex: '^(hashlib\.)?pbkdf2_hmac$' } }
26
+ - any:
27
+ - has:
28
+ field: arguments
29
+ has:
30
+ kind: keyword_argument
31
+ all:
32
+ - { has: { field: name, regex: '^iterations$' } }
33
+ - { has: { field: value, kind: integer, regex: '^([0-9]{1,5}|[1-5][0-9]{5})$' } }
34
+ - has:
35
+ field: arguments
36
+ has: { nthChild: 4, kind: integer, regex: '^([0-9]{1,5}|[1-5][0-9]{5})$' }
37
+ - kind: call
38
+ all:
39
+ - { has: { field: function, regex: '^(bcrypt\.)?gensalt$' } }
40
+ - has:
41
+ field: arguments
42
+ has:
43
+ kind: keyword_argument
44
+ all:
45
+ - { has: { field: name, regex: '^rounds$' } }
46
+ - { has: { field: value, kind: integer, regex: '^[0-9]$' } }
47
+ message: 'This password hash is too fast; use Argon2 or bcrypt, or more iterations'
48
+ ```
49
+
50
+ ## How to fix
51
+
52
+ 1. Use `argon2-cffi` (`PasswordHasher().hash(password)`) or `bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))`.
53
+ 2. If you must use PBKDF2, use SHA-256 with at least 600 000 iterations (OWASP).
54
+ 3. Rehash old entries when a user logs in.
55
+
56
+ ## Examples
57
+
58
+ ### Python
59
+
60
+ #### Don't
61
+
62
+ ```py
63
+ import hashlib
64
+
65
+ stored = hashlib.sha256(password.encode()).hexdigest()
66
+ ```
67
+
68
+ #### Do
69
+
70
+ ```py
71
+ from argon2 import PasswordHasher
72
+
73
+ stored = PasswordHasher().hash(password)
74
+ ```
@@ -0,0 +1,41 @@
1
+ # sec-py/requests-timeout — cases
2
+
3
+ ### case: no timeout
4
+
5
+ ```py
6
+ requests.get(url)
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/requests-timeout
11
+ ```
12
+
13
+ ### case: with timeout
14
+
15
+ ```py
16
+ requests.get(url, timeout=3)
17
+ ```
18
+
19
+ ```expect
20
+ (none)
21
+ ```
22
+
23
+ ### case: a session is not matched
24
+
25
+ ```py
26
+ session.get(url)
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
32
+
33
+ ### case: post with data
34
+
35
+ ```py
36
+ requests.post(url, json=body)
37
+ ```
38
+
39
+ ```expect
40
+ 1:1 sec-py/requests-timeout
41
+ ```
@@ -0,0 +1,63 @@
1
+ ---
2
+ id: sec-py/requests-timeout
3
+ title: 'HTTP requests have a timeout'
4
+ languages: [python]
5
+ exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
6
+ fix: suggest
7
+ fix_reason: "the right timeout depends on the service; the suggested value is a starting point"
8
+ stability: preview
9
+ ported_from: [bandit/B113, ruff/S113]
10
+ security: { cwe: [400], owasp: ["A06:2025"], severity: medium }
11
+ since: 0.2.0
12
+ ---
13
+
14
+ `requests` and `httpx` calls pass `timeout=`.
15
+
16
+ **Why:** `requests` waits forever by default, so one stuck server holds a worker for good and a flood of slow responses exhausts them (CWE-400).
17
+
18
+ ```codelaw
19
+ match:
20
+ all:
21
+ - { pattern: '$F($$$A)' }
22
+ - { kind: call }
23
+ - not:
24
+ has:
25
+ field: arguments
26
+ has: { kind: keyword_argument, has: { field: name, regex: '^timeout$' } }
27
+ constraints: { F: { regex: '^requests\.(get|post|put|patch|delete|head|options|request)$' } }
28
+ message: 'This request has no timeout; pass `timeout=`'
29
+ fix: '$F($$$A, timeout=10)'
30
+ ```
31
+
32
+ ## How to fix
33
+
34
+ 1. Pass `timeout=` in seconds, or a `(connect, read)` tuple.
35
+ 2. Keep the value in one setting if several calls share it.
36
+
37
+ ## Examples
38
+
39
+ ### Python
40
+
41
+ #### Don't
42
+
43
+ ```py
44
+ import requests
45
+
46
+ resp = requests.get(url)
47
+ ```
48
+
49
+ #### Do
50
+
51
+ ```py
52
+ import requests
53
+
54
+ resp = requests.get(url, timeout=10)
55
+ ```
56
+
57
+ #### Fixed
58
+
59
+ ```py
60
+ import requests
61
+
62
+ resp = requests.get(url, timeout=10)
63
+ ```
@@ -0,0 +1,51 @@
1
+ # sec-py/tarfile-filter — cases
2
+
3
+ ### case: no filter
4
+
5
+ ```py
6
+ tar.extractall(dest)
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/tarfile-filter
11
+ ```
12
+
13
+ ### case: a filter
14
+
15
+ ```py
16
+ tar.extractall(dest, filter="data")
17
+ ```
18
+
19
+ ```expect
20
+ (none)
21
+ ```
22
+
23
+ ### case: a zip file
24
+
25
+ ```py
26
+ zf.extract(name)
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
32
+
33
+ ### case: unpack_archive
34
+
35
+ ```py
36
+ shutil.unpack_archive(path, dest)
37
+ ```
38
+
39
+ ```expect
40
+ 1:1 sec-py/tarfile-filter
41
+ ```
42
+
43
+ ### case: unpack_archive with a filter
44
+
45
+ ```py
46
+ shutil.unpack_archive(path, dest, filter="data")
47
+ ```
48
+
49
+ ```expect
50
+ (none)
51
+ ```
@@ -0,0 +1,71 @@
1
+ ---
2
+ id: sec-py/tarfile-filter
3
+ title: 'Archive extraction uses `filter="data"`'
4
+ languages: [python]
5
+ fix: suggest
6
+ fix_reason: "`filter=\"data\"` refuses members that carry device files or special permissions some archives need"
7
+ stability: preview
8
+ ported_from: [bandit/B202, ruff/S202]
9
+ security: { cwe: [22], owasp: ["A01:2025"], severity: high }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ `TarFile.extractall` and `shutil.unpack_archive` pass `filter="data"`.
14
+
15
+ **Why:** A member named `../../etc/cron.d/x`, an absolute path or a symlink writes outside the target folder (CWE-22).
16
+
17
+ ```codelaw
18
+ match:
19
+ any:
20
+ - all:
21
+ - { pattern: '$T.extractall($$$A)' }
22
+ - not:
23
+ has:
24
+ field: arguments
25
+ has: { kind: keyword_argument, has: { field: name, regex: '^filter$' } }
26
+ - all:
27
+ - { pattern: 'shutil.unpack_archive($$$B)' }
28
+ - not:
29
+ has:
30
+ field: arguments
31
+ has: { kind: keyword_argument, has: { field: name, regex: '^filter$' } }
32
+ message: '`extractall` follows member paths out of the folder; pass `filter="data"`'
33
+ fix: '$T.extractall($$$A, filter="data")'
34
+ ```
35
+
36
+ ## How to fix
37
+
38
+ 1. Pass `filter="data"` (Python 3.12, and the 3.8–3.11 security releases).
39
+ 2. Pass it to `shutil.unpack_archive(…, filter="data")` too.
40
+ 3. Extract into a fresh folder, and never run as root.
41
+
42
+ ## Examples
43
+
44
+ ### Python
45
+
46
+ #### Don't
47
+
48
+ ```py
49
+ import tarfile
50
+
51
+ with tarfile.open(path) as tar:
52
+ tar.extractall(dest)
53
+ ```
54
+
55
+ #### Do
56
+
57
+ ```py
58
+ import tarfile
59
+
60
+ with tarfile.open(path) as tar:
61
+ tar.extractall(dest, filter="data")
62
+ ```
63
+
64
+ #### Fixed
65
+
66
+ ```py
67
+ import tarfile
68
+
69
+ with tarfile.open(path) as tar:
70
+ tar.extractall(dest, filter="data")
71
+ ```
@@ -0,0 +1,41 @@
1
+ # sec-py/torch-load-weights-only — cases
2
+
3
+ ### case: a bare load
4
+
5
+ ```py
6
+ torch.load(path)
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/torch-load-weights-only
11
+ ```
12
+
13
+ ### case: map_location only
14
+
15
+ ```py
16
+ torch.load(path, map_location="cpu")
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/torch-load-weights-only
21
+ ```
22
+
23
+ ### case: weights_only=False
24
+
25
+ ```py
26
+ torch.load(path, weights_only=False)
27
+ ```
28
+
29
+ ```expect
30
+ 1:1 sec-py/torch-load-weights-only
31
+ ```
32
+
33
+ ### case: weights_only=True
34
+
35
+ ```py
36
+ torch.load(path, weights_only=True)
37
+ ```
38
+
39
+ ```expect
40
+ (none)
41
+ ```
@@ -0,0 +1,73 @@
1
+ ---
2
+ id: sec-py/torch-load-weights-only
3
+ title: "`torch.load` with `weights_only=True`"
4
+ languages: [python]
5
+ fix: suggest
6
+ fix_reason: "`weights_only=True` refuses checkpoints that hold arbitrary objects; those need a conversion to `safetensors`"
7
+ stability: preview
8
+ ported_from: [bandit/B614]
9
+ security: { cwe: [502], owasp: ["A08:2025"], severity: high }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ `torch.load` is called with `weights_only=True`.
14
+
15
+ **Why:** Without it the file is unpickled, so a model file from anywhere else runs code on load (CWE-502).
16
+
17
+ ```codelaw
18
+ match:
19
+ any:
20
+ - all:
21
+ - { pattern: 'torch.load($$$A)' }
22
+ - not:
23
+ has:
24
+ field: arguments
25
+ has:
26
+ kind: keyword_argument
27
+ all: [{ has: { field: name, regex: '^weights_only$' } }]
28
+ - kind: call
29
+ all:
30
+ - { has: { field: function, regex: '^torch\.load$' } }
31
+ - has:
32
+ field: arguments
33
+ has:
34
+ kind: keyword_argument
35
+ all:
36
+ - { has: { field: name, regex: '^weights_only$' } }
37
+ - { has: { field: value, kind: 'false' } }
38
+ message: '`torch.load` unpickles the file; pass `weights_only=True`'
39
+ fix: 'torch.load($$$A, weights_only=True)'
40
+ ```
41
+
42
+ ## How to fix
43
+
44
+ 1. Add `weights_only=True`.
45
+ 2. If the checkpoint holds more than tensors, convert it once with a trusted environment and keep it as `safetensors`.
46
+
47
+ ## Examples
48
+
49
+ ### Python
50
+
51
+ #### Don't
52
+
53
+ ```py
54
+ import torch
55
+
56
+ model = torch.load(path)
57
+ ```
58
+
59
+ #### Do
60
+
61
+ ```py
62
+ import torch
63
+
64
+ model = torch.load(path, weights_only=True)
65
+ ```
66
+
67
+ #### Fixed
68
+
69
+ ```py
70
+ import torch
71
+
72
+ model = torch.load(path, weights_only=True)
73
+ ```
@@ -0,0 +1,41 @@
1
+ # sec-py/upload-secure-filename — cases
2
+
3
+ ### case: the raw name
4
+
5
+ ```py
6
+ f.save(f.filename)
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/upload-secure-filename
11
+ ```
12
+
13
+ ### case: joined
14
+
15
+ ```py
16
+ file.save(os.path.join(UP, file.filename))
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/upload-secure-filename
21
+ ```
22
+
23
+ ### case: secured
24
+
25
+ ```py
26
+ f.save(os.path.join(UP, secure_filename(f.filename)))
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
32
+
33
+ ### case: a generated name
34
+
35
+ ```py
36
+ f.save(uuid4().hex)
37
+ ```
38
+
39
+ ```expect
40
+ (none)
41
+ ```
@@ -0,0 +1,65 @@
1
+ ---
2
+ id: sec-py/upload-secure-filename
3
+ title: 'Uploaded files are saved under a safe name'
4
+ languages: [python]
5
+ fix: suggest
6
+ fix_reason: "`secure_filename` removes characters a name may legitimately have, and returns an empty string for some names, which needs handling"
7
+ stability: preview
8
+ security: { cwe: [434, 22], owasp: ["A01:2025"], severity: high }
9
+ since: 0.2.0
10
+ ---
11
+
12
+ A file from a request is not saved under the name the client gave it.
13
+
14
+ **Why:** The name can be `../../app.py` or an absolute path, so the upload overwrites a file outside the upload folder (CWE-434, CWE-22).
15
+
16
+ ```codelaw
17
+ match:
18
+ all:
19
+ - { kind: call }
20
+ - { has: { field: function, regex: '\.save$' } }
21
+ - has:
22
+ field: arguments
23
+ has:
24
+ any:
25
+ - { kind: attribute, regex: '\.filename$', pattern: $NAME }
26
+ - kind: call
27
+ has: { field: arguments, has: { kind: attribute, regex: '\.filename$', pattern: $NAME } }
28
+ message: 'The upload is saved under the client''s file name; wrap it in `secure_filename`'
29
+ fix:
30
+ - { replace: { target: $NAME, with: 'secure_filename($NAME)' } }
31
+ ```
32
+
33
+ ## How to fix
34
+
35
+ 1. Import `from werkzeug.utils import secure_filename`.
36
+ 2. Use `secure_filename(file.filename)`, and refuse an empty result.
37
+ 3. Better: store the file under a name you generate (a UUID) and keep the client's name as data.
38
+
39
+ ## Examples
40
+
41
+ ### Python
42
+
43
+ #### Don't
44
+
45
+ ```py
46
+ from werkzeug.utils import secure_filename
47
+
48
+ file.save(os.path.join(UPLOADS, file.filename))
49
+ ```
50
+
51
+ #### Do
52
+
53
+ ```py
54
+ from werkzeug.utils import secure_filename
55
+
56
+ file.save(os.path.join(UPLOADS, secure_filename(file.filename)))
57
+ ```
58
+
59
+ #### Fixed
60
+
61
+ ```py
62
+ from werkzeug.utils import secure_filename
63
+
64
+ file.save(os.path.join(UPLOADS, secure_filename(file.filename)))
65
+ ```
@@ -0,0 +1,41 @@
1
+ # sec-py/urlopen-scheme — cases
2
+
3
+ ### case: a name
4
+
5
+ ```py
6
+ urlopen(url)
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/urlopen-scheme
11
+ ```
12
+
13
+ ### case: an https literal
14
+
15
+ ```py
16
+ urlopen("https://example.com")
17
+ ```
18
+
19
+ ```expect
20
+ (none)
21
+ ```
22
+
23
+ ### case: qualified
24
+
25
+ ```py
26
+ urllib.request.urlopen(u, timeout=5)
27
+ ```
28
+
29
+ ```expect
30
+ 1:1 sec-py/urlopen-scheme
31
+ ```
32
+
33
+ ### case: an f-string with a fixed prefix
34
+
35
+ ```py
36
+ urlopen(f"https://example.com/{x}")
37
+ ```
38
+
39
+ ```expect
40
+ (none)
41
+ ```
@@ -0,0 +1,54 @@
1
+ ---
2
+ id: sec-py/urlopen-scheme
3
+ title: "`urlopen` of a value that may be `file://`"
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "the allowed schemes and hosts are policy"
7
+ stability: preview
8
+ ported_from: [bandit/B310, ruff/S310]
9
+ security: { cwe: [22, 918], owasp: ["A01:2025"], severity: medium }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ `urllib.request.urlopen` and `urlretrieve` are not given a value that is not an `http(s)` literal.
14
+
15
+ **Why:** `urlopen` also opens `file://` and `ftp://` URLs, so a value that is not checked can read local files (CWE-22, CWE-918).
16
+
17
+ ```codelaw
18
+ match:
19
+ any:
20
+ - { pattern: 'urlopen($X)' }
21
+ - { pattern: 'urlopen($X, $$$R)' }
22
+ - { pattern: 'urllib.request.urlopen($X)' }
23
+ - { pattern: 'urllib.request.urlopen($X, $$$R)' }
24
+ - { pattern: 'urllib.request.urlretrieve($X)' }
25
+ - { pattern: 'urllib.request.urlretrieve($X, $$$R)' }
26
+ - { pattern: 'urlretrieve($X, $$$R)' }
27
+ constraints: { X: { not: { kind: string, regex: '^[rbf]?[''\"]https?://' } } }
28
+ message: '`$X` may be a `file://` URL; check the scheme first'
29
+ ```
30
+
31
+ ## How to fix
32
+
33
+ 1. Parse the URL and check `scheme in ("http", "https")` before opening it.
34
+ 2. Or use `requests`, which supports only HTTP(S), with a `timeout`.
35
+
36
+ ## Examples
37
+
38
+ ### Python
39
+
40
+ #### Don't
41
+
42
+ ```py
43
+ from urllib.request import urlopen
44
+
45
+ data = urlopen(url).read()
46
+ ```
47
+
48
+ #### Do
49
+
50
+ ```py
51
+ from urllib.request import urlopen
52
+
53
+ data = urlopen("https://example.com/data").read()
54
+ ```