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,45 @@
1
+ # sec-py/no-reflected-xss — cases
2
+
3
+ ### case: a direct value
4
+
5
+ ```py
6
+ def v(request):
7
+ return HttpResponse(request.GET["n"])
8
+ ```
9
+
10
+ ```expect
11
+ 2:12 sec-py/no-reflected-xss
12
+ ```
13
+
14
+ ### case: a returned string
15
+
16
+ ```py
17
+ def v():
18
+ return "<p>" + request.args["n"] + "</p>"
19
+ ```
20
+
21
+ ```expect
22
+ 2:5 sec-py/no-reflected-xss
23
+ ```
24
+
25
+ ### case: escaped
26
+
27
+ ```py
28
+ def v(request):
29
+ return HttpResponse(escape(request.GET["n"]))
30
+ ```
31
+
32
+ ```expect
33
+ (none)
34
+ ```
35
+
36
+ ### case: a constant
37
+
38
+ ```py
39
+ def v(request):
40
+ return HttpResponse("ok")
41
+ ```
42
+
43
+ ```expect
44
+ (none)
45
+ ```
@@ -0,0 +1,56 @@
1
+ ---
2
+ id: sec-py/no-reflected-xss
3
+ title: 'A request value is not written into the response as it is'
4
+ languages: [python]
5
+ exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
6
+ fix: manual
7
+ fix_reason: "escaping the value or rendering a template are both fixes; which fits depends on the response"
8
+ stability: preview
9
+ security: { cwe: [79], owasp: ["A05:2025"], severity: high }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ A value from the request is not passed to `HttpResponse`, `make_response` or `HTMLResponse`, or built into a returned string, without escaping.
14
+
15
+ **Why:** The page then contains whatever the client sent, including a script (CWE-79). 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.
16
+
17
+ ```codelaw
18
+ match:
19
+ any:
20
+ - { pattern: 'HttpResponse($V)' }
21
+ - { pattern: 'make_response($V)' }
22
+ - { pattern: 'HTMLResponse($V)' }
23
+ - all:
24
+ - { pattern: 'return $V' }
25
+ - has:
26
+ any:
27
+ - { kind: string, has: { kind: interpolation } }
28
+ - { kind: binary_operator, has: { field: operator, regex: '^(%|\+)$' } }
29
+ - { kind: call, has: { field: function, regex: '\.format$' } }
30
+ where: { $V: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
31
+ message: '`$V` comes from the request and is written into the page without escaping'
32
+ ```
33
+
34
+ ## How to fix
35
+
36
+ 1. Escape it: `markupsafe.escape(value)` or Django's `django.utils.html.escape`.
37
+ 2. Or render a template with autoescape on and pass the value in.
38
+ 3. If the response is not HTML, set `Content-Type: text/plain` or `application/json`.
39
+
40
+ ## Examples
41
+
42
+ ### Python
43
+
44
+ #### Don't
45
+
46
+ ```py
47
+ def hello(request):
48
+ return HttpResponse(request.GET["name"])
49
+ ```
50
+
51
+ #### Do
52
+
53
+ ```py
54
+ def hello(request):
55
+ return HttpResponse(escape(request.GET["name"]))
56
+ ```
@@ -0,0 +1,33 @@
1
+ # sec-py/no-regex-injection — cases
2
+
3
+ ### case: a request pattern
4
+
5
+ ```py
6
+ def f(request, t):
7
+ return re.search(request.GET["q"], t)
8
+ ```
9
+
10
+ ```expect
11
+ 2:12 sec-py/no-regex-injection
12
+ ```
13
+
14
+ ### case: escaped
15
+
16
+ ```py
17
+ def f(request, t):
18
+ return re.search(re.escape(request.GET["q"]), t)
19
+ ```
20
+
21
+ ```expect
22
+ (none)
23
+ ```
24
+
25
+ ### case: a literal
26
+
27
+ ```py
28
+ re.compile("a+")
29
+ ```
30
+
31
+ ```expect
32
+ (none)
33
+ ```
@@ -0,0 +1,49 @@
1
+ ---
2
+ id: sec-py/no-regex-injection
3
+ title: 'A request value is not a regular expression'
4
+ languages: [python]
5
+ fix: suggest
6
+ fix_reason: "`re.escape` turns the value into a literal search; a client that is meant to send a pattern needs a length limit and a timeout instead"
7
+ stability: preview
8
+ security: { cwe: [730, 1333], owasp: ["A06:2025"], severity: medium }
9
+ since: 0.2.0
10
+ ---
11
+
12
+ A value from the request is not used as the pattern of `re.compile`, `re.search`, `re.match` or the other `re` functions.
13
+
14
+ **Why:** The client can write a pattern that takes exponential time (a ReDoS) or changes what the search matches (CWE-730, CWE-1333). 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: { any: [{ pattern: '$F($P, $$$R)' }, { pattern: '$F($P)' }] }
18
+ constraints: { F: { regex: '^re\.(compile|search|match|fullmatch|findall|finditer|sub|subn|split)$' } }
19
+ where: { $P: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
20
+ message: '`$P` comes from the request and is used as a regular expression; escape it'
21
+ fix: '$F(re.escape($P), $$$R)'
22
+ ```
23
+
24
+ ## How to fix
25
+
26
+ 1. If the value is text to look for, wrap it in `re.escape(value)`.
27
+ 2. If the client must send a pattern, limit its length and run it with a timeout (the `regex` package's `timeout=`).
28
+
29
+ ## Examples
30
+
31
+ ### Python
32
+
33
+ #### Don't
34
+
35
+ ```py
36
+ import re
37
+
38
+ def find(request, text):
39
+ return re.search(request.GET["q"], text)
40
+ ```
41
+
42
+ #### Do
43
+
44
+ ```py
45
+ import re
46
+
47
+ def find(request, text):
48
+ return re.search(re.escape(request.GET["q"]), text)
49
+ ```
@@ -0,0 +1,41 @@
1
+ # sec-py/no-secret-in-log — cases
2
+
3
+ ### case: a password
4
+
5
+ ```py
6
+ logger.info("pw=%s", password)
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-secret-in-log
11
+ ```
12
+
13
+ ### case: an f-string
14
+
15
+ ```py
16
+ log.debug(f"token {token}")
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/no-secret-in-log
21
+ ```
22
+
23
+ ### case: a user name
24
+
25
+ ```py
26
+ logger.info("user %s", username)
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
32
+
33
+ ### case: print
34
+
35
+ ```py
36
+ print(api_key)
37
+ ```
38
+
39
+ ```expect
40
+ 1:1 sec-py/no-secret-in-log
41
+ ```
@@ -0,0 +1,55 @@
1
+ ---
2
+ id: sec-py/no-secret-in-log
3
+ title: 'Secrets are not logged'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "whether to drop the value or mask it depends on what the log is for"
7
+ stability: preview
8
+ security: { cwe: [532], owasp: ["A09:2025"], severity: medium }
9
+ since: 0.2.0
10
+ ---
11
+
12
+ A value named `password`, `token` or `secret` is not written to a log.
13
+
14
+ **Why:** Logs are kept longer and read by more people than the secret was meant for (CWE-532).
15
+
16
+ ```codelaw
17
+ match:
18
+ all:
19
+ - { kind: call }
20
+ - has:
21
+ field: function
22
+ regex: '^((logger|logging|log|self\.logger|self\.log|app\.logger)\.(debug|info|warning|warn|error|exception|critical|log)|print)$'
23
+ - has:
24
+ field: arguments
25
+ has:
26
+ regex: '(?i)(pass(word|wd)?|secret|token|api_?key|auth|otp|nonce|salt|session_?id|csrf|reset_?code|verification_?code)'
27
+ any:
28
+ - { kind: identifier }
29
+ - { kind: attribute }
30
+ - { kind: subscript }
31
+ - { kind: string, has: { kind: interpolation } }
32
+ - { kind: keyword_argument }
33
+ message: 'A secret may be written to the log; log that it was set, not its value'
34
+ ```
35
+
36
+ ## How to fix
37
+
38
+ 1. Remove the value from the message.
39
+ 2. Log an id or `"set"/"missing"`, or a masked form such as the last four characters.
40
+
41
+ ## Examples
42
+
43
+ ### Python
44
+
45
+ #### Don't
46
+
47
+ ```py
48
+ logger.info("login with password %s", password)
49
+ ```
50
+
51
+ #### Do
52
+
53
+ ```py
54
+ logger.info("login for user %s", username)
55
+ ```
@@ -0,0 +1,55 @@
1
+ # sec-py/no-shell — cases
2
+
3
+ ### case: shell=True with a string
4
+
5
+ ```py
6
+ import subprocess
7
+
8
+ subprocess.check_output(cmd, shell=True)
9
+ ```
10
+
11
+ ```expect
12
+ 3:1 sec-py/no-shell
13
+ ```
14
+
15
+ ### case: shell=False
16
+
17
+ ```py
18
+ import subprocess
19
+
20
+ subprocess.run(["ls"], shell=False)
21
+ ```
22
+
23
+ ```expect
24
+ (none)
25
+ ```
26
+
27
+ ### case: os.system of a literal
28
+
29
+ ```py
30
+ os.system("clear")
31
+ ```
32
+
33
+ ```expect
34
+ (none)
35
+ ```
36
+
37
+ ### case: os.system of an f-string
38
+
39
+ ```py
40
+ os.system(f"rm {path}")
41
+ ```
42
+
43
+ ```expect
44
+ 1:1 sec-py/no-shell
45
+ ```
46
+
47
+ ### case: os.popen of a name
48
+
49
+ ```py
50
+ os.popen(cmd)
51
+ ```
52
+
53
+ ```expect
54
+ 1:1 sec-py/no-shell
55
+ ```
@@ -0,0 +1,63 @@
1
+ ---
2
+ id: sec-py/no-shell
3
+ title: 'A shell command built from data'
4
+ languages: [python]
5
+ fix: suggest
6
+ fix_reason: "dropping `shell=True` only works when the command is already a list of arguments; a string command must be split"
7
+ stability: preview
8
+ ported_from: [bandit/B602, bandit/B603, bandit/B604, bandit/B605, bandit/B609]
9
+ security: { cwe: [78], owasp: ["A05:2025"], severity: high }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ A subprocess is not started with `shell=True`, and `os.system` / `os.popen` are not given a value that is not a literal.
14
+
15
+ **Why:** The command line goes through the shell, so a quote, `;` or `$(…)` in any part of it runs another command (CWE-78).
16
+
17
+ ```codelaw
18
+ match:
19
+ any:
20
+ - kind: call
21
+ all:
22
+ - has:
23
+ field: function
24
+ regex: '^subprocess\.(run|call|check_call|check_output|Popen|getoutput|getstatusoutput)$'
25
+ - has:
26
+ field: arguments
27
+ has:
28
+ kind: keyword_argument
29
+ all: [{ has: { field: name, regex: '^shell$' } }, { has: { field: value, kind: 'true' } }]
30
+ pattern: $KW
31
+ - { pattern: 'os.system($X)' }
32
+ - { pattern: 'os.popen($X)' }
33
+ constraints: { X: { not: { kind: string, not: { has: { kind: interpolation } } } } }
34
+ message: 'This command runs through a shell; pass a list of arguments and leave `shell` off'
35
+ fix:
36
+ - { remove: $KW }
37
+ ```
38
+
39
+ ## How to fix
40
+
41
+ 1. Pass the command as a list: `subprocess.run(["ls", "-l", path])`.
42
+ 2. Remove `shell=True`.
43
+ 3. Replace `os.system(cmd)` with `subprocess.run([...], check=True)`; use `shlex.split` only for a trusted fixed string.
44
+
45
+ ## Examples
46
+
47
+ ### Python
48
+
49
+ #### Don't
50
+
51
+ ```py
52
+ import subprocess
53
+
54
+ subprocess.run(["ls", path], shell=True)
55
+ ```
56
+
57
+ #### Do
58
+
59
+ ```py
60
+ import subprocess
61
+
62
+ subprocess.run(["ls", path])
63
+ ```
@@ -0,0 +1,53 @@
1
+ # sec-py/no-silent-except — cases
2
+
3
+ ### case: bare except
4
+
5
+ ```py
6
+ try:
7
+ f()
8
+ except:
9
+ pass
10
+ ```
11
+
12
+ ```expect
13
+ 3:1 sec-py/no-silent-except
14
+ ```
15
+
16
+ ### case: Exception
17
+
18
+ ```py
19
+ try:
20
+ f()
21
+ except Exception:
22
+ pass
23
+ ```
24
+
25
+ ```expect
26
+ 3:1 sec-py/no-silent-except
27
+ ```
28
+
29
+ ### case: a specific exception
30
+
31
+ ```py
32
+ try:
33
+ f()
34
+ except KeyError:
35
+ pass
36
+ ```
37
+
38
+ ```expect
39
+ (none)
40
+ ```
41
+
42
+ ### case: logged
43
+
44
+ ```py
45
+ try:
46
+ f()
47
+ except Exception:
48
+ log.exception("x")
49
+ ```
50
+
51
+ ```expect
52
+ (none)
53
+ ```
@@ -0,0 +1,54 @@
1
+ ---
2
+ id: sec-py/no-silent-except
3
+ title: 'Errors are not swallowed'
4
+ languages: [python]
5
+ exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
6
+ fix: manual
7
+ fix_reason: "what to do with the error (log, retry, stop) is a decision"
8
+ stability: preview
9
+ ported_from: [bandit/B110, bandit/B112, ruff/S110, ruff/S112]
10
+ security: { cwe: [703], owasp: ["A10:2025"], severity: low }
11
+ since: 0.2.0
12
+ ---
13
+
14
+ An `except` that catches everything (bare, `Exception` or `BaseException`) does not just `pass` or `continue`.
15
+
16
+ **Why:** A failure that is dropped hides a broken check or a failed login step, and the code goes on as though it had worked (CWE-703).
17
+
18
+ ```codelaw
19
+ match:
20
+ all:
21
+ - { kind: except_clause }
22
+ - any:
23
+ - { regex: '^except\s*:' }
24
+ - { has: { kind: identifier, regex: '^(Exception|BaseException)$' } }
25
+ - { has: { kind: block, regex: '^(pass|continue)$' } }
26
+ message: 'This `except` drops the error; handle it or log it'
27
+ ```
28
+
29
+ ## How to fix
30
+
31
+ 1. Catch the specific exceptions you expect.
32
+ 2. Log the others with `logger.exception("…")` and let them go on or re-raise.
33
+
34
+ ## Examples
35
+
36
+ ### Python
37
+
38
+ #### Don't
39
+
40
+ ```py
41
+ try:
42
+ check(token)
43
+ except Exception:
44
+ pass
45
+ ```
46
+
47
+ #### Do
48
+
49
+ ```py
50
+ try:
51
+ check(token)
52
+ except InvalidToken:
53
+ logger.warning("bad token")
54
+ ```
@@ -0,0 +1,91 @@
1
+ # sec-py/no-sql-string — cases
2
+
3
+ ### case: a literal statement
4
+
5
+ ```py
6
+ cursor.execute("SELECT 1")
7
+ ```
8
+
9
+ ```expect
10
+ (none)
11
+ ```
12
+
13
+ ### case: a statement with parameters
14
+
15
+ ```py
16
+ cursor.execute("SELECT * FROM t WHERE a = %s", (a,))
17
+ ```
18
+
19
+ ```expect
20
+ (none)
21
+ ```
22
+
23
+ ### case: percent formatting
24
+
25
+ ```py
26
+ cursor.execute("SELECT * FROM t WHERE a = %s" % a)
27
+ ```
28
+
29
+ ```expect
30
+ 1:1 sec-py/no-sql-string
31
+ ```
32
+
33
+ ### case: concatenation
34
+
35
+ ```py
36
+ cursor.execute("SELECT * FROM t WHERE a = " + a)
37
+ ```
38
+
39
+ ```expect
40
+ 1:1 sec-py/no-sql-string
41
+ ```
42
+
43
+ ### case: format
44
+
45
+ ```py
46
+ db.execute("SELECT {}".format(col))
47
+ ```
48
+
49
+ ```expect
50
+ 1:1 sec-py/no-sql-string
51
+ ```
52
+
53
+ ### case: Django raw with an f-string
54
+
55
+ ```py
56
+ User.objects.raw(f"SELECT * FROM auth_user WHERE id = {uid}")
57
+ ```
58
+
59
+ ```expect
60
+ 1:1 sec-py/no-sql-string
61
+ ```
62
+
63
+ ### case: a name
64
+
65
+ ```py
66
+ cursor.execute(sql)
67
+ ```
68
+
69
+ ```expect
70
+ (none)
71
+ ```
72
+
73
+ ### case: extra with an interpolated where
74
+
75
+ ```py
76
+ Entry.objects.extra(where=[f"id = {i}"])
77
+ ```
78
+
79
+ ```expect
80
+ 1:1 sec-py/no-sql-string
81
+ ```
82
+
83
+ ### case: extra with parameters
84
+
85
+ ```py
86
+ Entry.objects.extra(where=["id = %s"], params=[i])
87
+ ```
88
+
89
+ ```expect
90
+ (none)
91
+ ```
@@ -0,0 +1,78 @@
1
+ ---
2
+ id: sec-py/no-sql-string
3
+ title: 'SQL built from a string'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "which placeholder style the driver takes, and where each value goes, depends on the driver"
7
+ stability: preview
8
+ ported_from: [bandit/B608, bandit/B610, bandit/B611]
9
+ security: { cwe: [89], owasp: ["A05:2025"], severity: high }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ A SQL statement is not built with an f-string, `%`, `+` or `.format`; the values go in as parameters.
14
+
15
+ **Why:** A value that is part of the statement text can change the statement (CWE-89).
16
+
17
+ ```codelaw
18
+ match:
19
+ any:
20
+ - kind: call
21
+ all:
22
+ - has:
23
+ field: function
24
+ regex: '^((.*\.)?(execute|executemany|executescript|raw|read_sql|read_sql_query)|RawSQL|text|sa\.text|sqlalchemy\.text)$'
25
+ - has:
26
+ field: arguments
27
+ has:
28
+ nthChild: 1
29
+ any:
30
+ - { kind: string, has: { kind: interpolation } }
31
+ - { kind: binary_operator, has: { field: operator, regex: '^(%|\+)$' } }
32
+ - { kind: call, has: { field: function, regex: '\.format$' } }
33
+ - kind: call
34
+ all:
35
+ - { has: { field: function, regex: '\.extra$' } }
36
+ - has:
37
+ field: arguments
38
+ has:
39
+ kind: keyword_argument
40
+ has:
41
+ field: value
42
+ any:
43
+ - { kind: string, has: { kind: interpolation } }
44
+ - { kind: binary_operator, has: { field: operator, regex: '^(%|\+)$' } }
45
+ - { kind: call, has: { field: function, regex: '\.format$' } }
46
+ - kind: list
47
+ has:
48
+ any:
49
+ - { kind: string, has: { kind: interpolation } }
50
+ - { kind: binary_operator, has: { field: operator, regex: '^(%|\+)$' } }
51
+ - { kind: call, has: { field: function, regex: '\.format$' } }
52
+ message: 'This SQL is built from a string; pass the values as parameters'
53
+ ```
54
+
55
+ ## How to fix
56
+
57
+ 1. Write the statement with placeholders: `cursor.execute("SELECT * FROM t WHERE id = %s", (item_id,))`.
58
+ 2. Pass the values as the second argument; the driver quotes them.
59
+ 3. A table or column name cannot be a parameter: choose it from a fixed allow-list.
60
+ 4. In Django use the ORM, `.raw(sql, params)` or `RawSQL(sql, params)`; give `.extra` parameters through `params=`.
61
+
62
+ ## Examples
63
+
64
+ ### Python
65
+
66
+ #### Don't
67
+
68
+ ```py
69
+ def find(cursor, name):
70
+ cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
71
+ ```
72
+
73
+ #### Do
74
+
75
+ ```py
76
+ def find(cursor, name):
77
+ cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
78
+ ```