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,41 @@
1
+ # sec-py/no-insecure-random — cases
2
+
3
+ ### case: a token
4
+
5
+ ```py
6
+ session_token = random.randint(0, 10**9)
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-insecure-random
11
+ ```
12
+
13
+ ### case: a password
14
+
15
+ ```py
16
+ password = random.choice(chars)
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/no-insecure-random
21
+ ```
22
+
23
+ ### case: a dice roll
24
+
25
+ ```py
26
+ roll = random.randint(1, 6)
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
32
+
33
+ ### case: secrets
34
+
35
+ ```py
36
+ token = secrets.token_hex(16)
37
+ ```
38
+
39
+ ```expect
40
+ (none)
41
+ ```
@@ -0,0 +1,67 @@
1
+ ---
2
+ id: sec-py/no-insecure-random
3
+ title: "`secrets` for tokens, not `random`"
4
+ languages: [python]
5
+ fix: suggest
6
+ fix_reason: "`secrets` has different functions (`token_hex`, `choice`, `randbelow`), so the call has to be chosen, not renamed"
7
+ stability: preview
8
+ ported_from: [bandit/B311, ruff/S311]
9
+ security: { cwe: [338], owasp: ["A04:2025"], severity: high }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ A password, token, key, nonce or code is not made with the `random` module.
14
+
15
+ **Why:** `random` is a predictable generator, so a few outputs reveal the rest (CWE-338).
16
+
17
+ ```codelaw
18
+ match:
19
+ all:
20
+ - { kind: assignment }
21
+ - has:
22
+ field: left
23
+ regex: '(?i)(pass(word|wd)?|secret|token|api_?key|auth|otp|nonce|salt|session_?id|csrf|reset_?code|verification_?code)'
24
+ - has:
25
+ field: right
26
+ kind: call
27
+ pattern: $R
28
+ has:
29
+ field: function
30
+ regex: '^(random\.)?(random|randint|randrange|choice|choices|getrandbits|sample|randbytes|uniform)$'
31
+ message: 'This secret comes from `random`; use `secrets`'
32
+ fix:
33
+ - { replace: { target: $R, with: 'secrets.token_hex(16)' } }
34
+ ```
35
+
36
+ ## How to fix
37
+
38
+ 1. Use `secrets.token_urlsafe(32)` or `secrets.token_hex(16)` for a token.
39
+ 2. Use `secrets.choice` for a character picked from an alphabet, `secrets.randbelow(n)` for a number.
40
+
41
+ ## Examples
42
+
43
+ ### Python
44
+
45
+ #### Don't
46
+
47
+ ```py
48
+ import random
49
+
50
+ reset_token = random.getrandbits(64)
51
+ ```
52
+
53
+ #### Do
54
+
55
+ ```py
56
+ import secrets
57
+
58
+ reset_token = secrets.token_hex(16)
59
+ ```
60
+
61
+ #### Fixed
62
+
63
+ ```py
64
+ import random
65
+
66
+ reset_token = secrets.token_hex(16)
67
+ ```
@@ -0,0 +1,41 @@
1
+ # sec-py/no-insecure-tempfile — cases
2
+
3
+ ### case: mktemp
4
+
5
+ ```py
6
+ tempfile.mktemp(suffix=".txt")
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-insecure-tempfile
11
+ ```
12
+
13
+ ### case: a fixed /tmp path
14
+
15
+ ```py
16
+ open("/tmp/report.txt", "w")
17
+ ```
18
+
19
+ ```expect
20
+ 1:6 sec-py/no-insecure-tempfile
21
+ ```
22
+
23
+ ### case: mkstemp
24
+
25
+ ```py
26
+ tempfile.mkstemp()
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
32
+
33
+ ### case: a path outside /tmp
34
+
35
+ ```py
36
+ open("/var/data/report.txt", "w")
37
+ ```
38
+
39
+ ```expect
40
+ (none)
41
+ ```
@@ -0,0 +1,62 @@
1
+ ---
2
+ id: sec-py/no-insecure-tempfile
3
+ title: "`mkstemp` instead of `mktemp`"
4
+ languages: [python]
5
+ fix: suggest
6
+ fix_reason: "`mkstemp` returns an open file descriptor and a path, not just a name, so the surrounding code changes"
7
+ stability: preview
8
+ ported_from: [bandit/B306, bandit/B108, bandit/B325, ruff/S306, ruff/S108]
9
+ security: { cwe: [377], owasp: ["A01:2025"], severity: medium }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ Temporary files are made with `tempfile.mkstemp`, `NamedTemporaryFile` or `TemporaryDirectory`, not `mktemp`, and not at a fixed path under `/tmp`.
14
+
15
+ **Why:** `mktemp` returns a name without creating the file, so another user can create it first and the program then writes into a file they control (CWE-377).
16
+
17
+ ```codelaw
18
+ match:
19
+ any:
20
+ - { pattern: 'tempfile.mktemp($$$A)' }
21
+ - { pattern: 'os.tempnam($$$A)' }
22
+ - { pattern: 'os.tmpnam()' }
23
+ - kind: string
24
+ regex: '^[''"]/(var/)?tmp/[^''"{}]*[''"]$'
25
+ inside: { kind: call, stopBy: end }
26
+ message: 'A predictable temporary file name; use `tempfile.mkstemp` or `NamedTemporaryFile`'
27
+ fix: 'tempfile.mkstemp($$$A)[1]'
28
+ ```
29
+
30
+ ## How to fix
31
+
32
+ 1. Use `with tempfile.NamedTemporaryFile(delete=False) as f:` and write through `f`.
33
+ 2. Use `tempfile.mkdtemp()` or `TemporaryDirectory()` for a directory.
34
+ 3. Never build a path under `/tmp` by hand.
35
+
36
+ ## Examples
37
+
38
+ ### Python
39
+
40
+ #### Don't
41
+
42
+ ```py
43
+ import tempfile
44
+
45
+ path = tempfile.mktemp()
46
+ ```
47
+
48
+ #### Do
49
+
50
+ ```py
51
+ import tempfile
52
+
53
+ path = tempfile.mkstemp()[1]
54
+ ```
55
+
56
+ #### Fixed
57
+
58
+ ```py
59
+ import tempfile
60
+
61
+ path = tempfile.mkstemp()[1]
62
+ ```
@@ -0,0 +1,41 @@
1
+ # sec-py/no-ldap-injection — cases
2
+
3
+ ### case: an f-string filter
4
+
5
+ ```py
6
+ conn.search_s(base, 2, f"(uid={name})")
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-ldap-injection
11
+ ```
12
+
13
+ ### case: ldap3
14
+
15
+ ```py
16
+ conn.search(base, "(cn=" + name + ")")
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/no-ldap-injection
21
+ ```
22
+
23
+ ### case: a fixed filter
24
+
25
+ ```py
26
+ conn.search(base, "(objectClass=*)")
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
32
+
33
+ ### case: a re.search is not matched
34
+
35
+ ```py
36
+ re.search("a", text)
37
+ ```
38
+
39
+ ```expect
40
+ (none)
41
+ ```
@@ -0,0 +1,51 @@
1
+ ---
2
+ id: sec-py/no-ldap-injection
3
+ title: 'LDAP filters are not built from strings'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "the escaping function belongs to the LDAP library in use"
7
+ stability: preview
8
+ security: { cwe: [90], owasp: ["A05:2025"], severity: high }
9
+ since: 0.2.0
10
+ ---
11
+
12
+ An LDAP search filter is not made with an f-string, `%`, `+` or `.format`.
13
+
14
+ **Why:** A value with `*`, `)` or `(` in it changes which entries the filter matches, which can turn a login check into a match-all (CWE-90).
15
+
16
+ ```codelaw
17
+ match:
18
+ kind: call
19
+ all:
20
+ - has: { field: function, regex: '\.(search|search_s|search_st|search_ext|search_ext_s)$' }
21
+ - has:
22
+ field: arguments
23
+ has:
24
+ regex: '\(.*='
25
+ any:
26
+ - { kind: string, has: { kind: interpolation } }
27
+ - { kind: binary_operator, has: { field: operator, regex: '^(%|\+)$' } }
28
+ - { kind: call, has: { field: function, regex: '\.format$' } }
29
+ message: 'This LDAP filter is built from a string; escape the values'
30
+ ```
31
+
32
+ ## How to fix
33
+
34
+ 1. Escape each value: `ldap3.utils.conv.escape_filter_chars(value)` or `ldap.filter.escape_filter_chars(value)`.
35
+ 2. Or build the filter with `ldap.filter.filter_format("(uid=%s)", [value])`.
36
+
37
+ ## Examples
38
+
39
+ ### Python
40
+
41
+ #### Don't
42
+
43
+ ```py
44
+ conn.search_s(base, ldap.SCOPE_SUBTREE, f"(uid={name})")
45
+ ```
46
+
47
+ #### Do
48
+
49
+ ```py
50
+ conn.search_s(base, ldap.SCOPE_SUBTREE, filter_format("(uid=%s)", [name]))
51
+ ```
@@ -0,0 +1,31 @@
1
+ # sec-py/no-llm-output-to-sink — cases
2
+
3
+ ### case: exec of a completion
4
+
5
+ ```py
6
+ exec(completion.choices[0].message.content)
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-llm-output-to-sink
11
+ ```
12
+
13
+ ### case: a subprocess
14
+
15
+ ```py
16
+ subprocess.run(llm_output, shell=True)
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/no-llm-output-to-sink
21
+ ```
22
+
23
+ ### case: a lookup
24
+
25
+ ```py
26
+ ACTIONS[reply]()
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
@@ -0,0 +1,55 @@
1
+ ---
2
+ id: sec-py/no-llm-output-to-sink
3
+ title: 'Model output is not run or rendered'
4
+ languages: [python]
5
+ severity: warn
6
+ fix: manual
7
+ fix_reason: "what to do with the output instead (parse it against a schema, pick from a fixed list of actions) is a design choice"
8
+ stability: preview
9
+ security: { cwe: [94], owasp: ["A05:2025"], severity: high }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ The reply of an OpenAI, Anthropic or LangChain call does not reach `exec`, `eval`, a subprocess, SQL or `mark_safe`.
14
+
15
+ **Why:** A model's output is shaped by its input, so a prompt written by an attacker becomes code, a command, a query or a script (CWE-94).
16
+
17
+ ```codelaw
18
+ match:
19
+ all:
20
+ - { kind: call }
21
+ - has:
22
+ field: function
23
+ regex: '^(exec|eval|os\.system|os\.popen|subprocess\.(run|call|check_call|check_output|Popen)|mark_safe|Markup|(.*\.)?(execute|executescript))$'
24
+ - has:
25
+ field: arguments
26
+ has:
27
+ regex: '(?i)(\.choices\[0\]\.message\.content|\.content\[0\]\.text|\.content\b|\.text\b|llm_output|completion|model_output|llm_response|ai_response)'
28
+ message: 'The output of a language model reaches a code, command, query or HTML sink'
29
+ ```
30
+
31
+ ## How to fix
32
+
33
+ 1. Ask the model for structured output and validate it against a schema.
34
+ 2. Map the answer to a fixed list of actions the program already has; never pass it to a sink as text.
35
+ 3. Escape it before it goes into HTML; use parameters for SQL.
36
+
37
+ ## Examples
38
+
39
+ ### Python
40
+
41
+ #### Don't
42
+
43
+ ```py
44
+ reply = client.chat.completions.create(model=m, messages=msgs)
45
+ exec(reply.choices[0].message.content)
46
+ ```
47
+
48
+ #### Do
49
+
50
+ ```py
51
+ reply = client.chat.completions.create(model=m, messages=msgs)
52
+ action = reply.choices[0].message.content.strip()
53
+ if action in ACTIONS:
54
+ ACTIONS[action]()
55
+ ```
@@ -0,0 +1,45 @@
1
+ # sec-py/no-log-injection — cases
2
+
3
+ ### case: a request value
4
+
5
+ ```py
6
+ def v(request):
7
+ logger.info("login %s", request.POST["u"])
8
+ ```
9
+
10
+ ```expect
11
+ 2:5 sec-py/no-log-injection
12
+ ```
13
+
14
+ ### case: a message only
15
+
16
+ ```py
17
+ def v(request):
18
+ logger.warning(request.GET["u"])
19
+ ```
20
+
21
+ ```expect
22
+ 2:5 sec-py/no-log-injection
23
+ ```
24
+
25
+ ### case: a constant
26
+
27
+ ```py
28
+ def v(request):
29
+ logger.info("login %s", "x")
30
+ ```
31
+
32
+ ```expect
33
+ (none)
34
+ ```
35
+
36
+ ### case: not a logger
37
+
38
+ ```py
39
+ def v(request):
40
+ other.info("login %s", request.POST["u"])
41
+ ```
42
+
43
+ ```expect
44
+ (none)
45
+ ```
@@ -0,0 +1,52 @@
1
+ ---
2
+ id: sec-py/no-log-injection
3
+ title: 'A request value is not logged raw'
4
+ languages: [python]
5
+ severity: warn
6
+ fix: manual
7
+ fix_reason: "whether to strip, escape or reject the value depends on how the log is read"
8
+ stability: preview
9
+ security: { cwe: [117], owasp: ["A09:2025"], severity: low }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ A value from the request is not passed to a logger without its line breaks removed.
14
+
15
+ **Why:** A CR or LF in the value writes a line that looks like another log entry, so an attacker can forge audit records (CWE-117). 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: { any: [{ pattern: '$L($V)' }, { pattern: '$L($A, $V)' }] }
19
+ constraints:
20
+ L:
21
+ regex: '^(logger|logging|log|self\.logger|self\.log|app\.logger|_logger|_log)\.(debug|info|warning|warn|error|exception|critical|log)$'
22
+ A: { regex: '^[rbf]?[''\"]' }
23
+ where: { $V: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
24
+ message: '`$V` comes from the request and is logged as it is'
25
+ ```
26
+
27
+ ## How to fix
28
+
29
+ 1. Pass the value through a helper that strips the line breaks, such as `clean(value)` with `text.replace("\r", "").replace("\n", " ")`; a method called on the request value itself is still seen as the request value.
30
+ 2. Or log it as structured data (`extra={"name": value}`) and use a JSON formatter that escapes it.
31
+
32
+ ## Examples
33
+
34
+ ### Python
35
+
36
+ #### Don't
37
+
38
+ ```py
39
+ def view(request):
40
+ logger.info("login %s", request.POST["user"])
41
+ ```
42
+
43
+ #### Do
44
+
45
+ ```py
46
+ def clean(text):
47
+ return text.replace("\r", "").replace("\n", " ")
48
+
49
+
50
+ def view(request):
51
+ logger.info("login %s", clean(request.POST["user"]))
52
+ ```
@@ -0,0 +1,37 @@
1
+ # sec-py/no-mako — cases
2
+
3
+ ### case: Template
4
+
5
+ ```py
6
+ from mako.template import Template
7
+
8
+ Template(src)
9
+ ```
10
+
11
+ ```expect
12
+ 3:1 sec-py/no-mako
13
+ ```
14
+
15
+ ### case: qualified
16
+
17
+ ```py
18
+ import mako.template
19
+
20
+ mako.template.Template(src)
21
+ ```
22
+
23
+ ```expect
24
+ 3:1 sec-py/no-mako
25
+ ```
26
+
27
+ ### case: jinja Template
28
+
29
+ ```py
30
+ from jinja2 import Template
31
+
32
+ Template(src)
33
+ ```
34
+
35
+ ```expect
36
+ (none)
37
+ ```
@@ -0,0 +1,52 @@
1
+ ---
2
+ id: sec-py/no-mako
3
+ title: 'No Mako templates'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "moving to another engine (or turning on `default_filters`) changes every template"
7
+ stability: preview
8
+ ported_from: [bandit/B702, ruff/S702]
9
+ security: { cwe: [79], owasp: ["A05:2025"], severity: medium }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ `mako.template.Template` is not used.
14
+
15
+ **Why:** Mako does not escape output unless told to, so every `${value}` is a script waiting for a hostile value (CWE-79).
16
+
17
+ ```codelaw
18
+ match: { any: [{ pattern: '$T($$$A)' }] }
19
+ constraints: { T: { regex: '^(Template|[A-Za-z_]+\.Template|mako\.template\.Template)$' } }
20
+ where:
21
+ $T:
22
+ any:
23
+ - { imported-from: { module: mako.template, name: Template } }
24
+ - { imported-from: mako.template }
25
+ - { imported-from: { module: mako, name: template } }
26
+ message: 'Mako does not escape by default; use Jinja2 with autoescape, or set `default_filters=[''h'']`'
27
+ ```
28
+
29
+ ## How to fix
30
+
31
+ 1. Use Jinja2 with `autoescape=True`.
32
+ 2. If you keep Mako, pass `default_filters=["h"]` to `Template` / `TemplateLookup`, and review each `|n`.
33
+
34
+ ## Examples
35
+
36
+ ### Python
37
+
38
+ #### Don't
39
+
40
+ ```py
41
+ from mako.template import Template
42
+
43
+ t = Template(source)
44
+ ```
45
+
46
+ #### Do
47
+
48
+ ```py
49
+ from jinja2 import Environment
50
+
51
+ env = Environment(autoescape=True)
52
+ ```
@@ -0,0 +1,51 @@
1
+ # sec-py/no-mark-safe — cases
2
+
3
+ ### case: a concatenation
4
+
5
+ ```py
6
+ mark_safe("<b>" + name + "</b>")
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-mark-safe
11
+ ```
12
+
13
+ ### case: an f-string
14
+
15
+ ```py
16
+ mark_safe(f"<b>{name}</b>")
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/no-mark-safe
21
+ ```
22
+
23
+ ### case: a literal
24
+
25
+ ```py
26
+ mark_safe("<br>")
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
32
+
33
+ ### case: Markup
34
+
35
+ ```py
36
+ Markup(user_text)
37
+ ```
38
+
39
+ ```expect
40
+ 1:1 sec-py/no-mark-safe
41
+ ```
42
+
43
+ ### case: format_html
44
+
45
+ ```py
46
+ format_html("<b>{}</b>", name)
47
+ ```
48
+
49
+ ```expect
50
+ (none)
51
+ ```
@@ -0,0 +1,54 @@
1
+ ---
2
+ id: sec-py/no-mark-safe
3
+ title: 'No `mark_safe` of dynamic text'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "the right fix is escaping the parts (`format_html`, `escape`) or rendering through a template, not removing the call"
7
+ stability: preview
8
+ ported_from: [bandit/B308, bandit/B703, bandit/B704, ruff/S308, ruff/S703, ruff/S704]
9
+ security: { cwe: [79], owasp: ["A05:2025"], severity: high }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ `mark_safe`, `SafeString`, `Markup` and `format_html` are not given a value that is not a literal.
14
+
15
+ **Why:** They switch HTML escaping off for that value, so any part of it that came from a user is a script (CWE-79).
16
+
17
+ ```codelaw
18
+ match:
19
+ all:
20
+ - { kind: call }
21
+ - has:
22
+ field: function
23
+ regex: '^((.*\.)?(mark_safe|SafeString|SafeText|SafeData)|(markupsafe\.)?Markup)$'
24
+ - has:
25
+ field: arguments
26
+ has: { nthChild: 1, not: { kind: string, not: { has: { kind: interpolation } } }, pattern: $X }
27
+ message: '`$X` is marked safe, so it is not escaped; escape the parts with `format_html` or `escape`'
28
+ ```
29
+
30
+ ## How to fix
31
+
32
+ 1. Build HTML with `format_html("<b>{}</b>", value)`: it escapes `value` and trusts only the literal.
33
+ 2. Or render a template with autoescape on.
34
+ 3. Use `mark_safe` only on a literal, or on text you produced by escaping.
35
+
36
+ ## Examples
37
+
38
+ ### Python
39
+
40
+ #### Don't
41
+
42
+ ```py
43
+ from django.utils.safestring import mark_safe
44
+
45
+ html = mark_safe("<b>" + name + "</b>")
46
+ ```
47
+
48
+ #### Do
49
+
50
+ ```py
51
+ from django.utils.html import format_html
52
+
53
+ html = format_html("<b>{}</b>", name)
54
+ ```