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,51 @@
1
+ # sec-py/no-exec-eval — cases
2
+
3
+ ### case: a literal string
4
+
5
+ ```py
6
+ eval("1 + 1")
7
+ ```
8
+
9
+ ```expect
10
+ (none)
11
+ ```
12
+
13
+ ### case: an f-string
14
+
15
+ ```py
16
+ exec(f"import {name}")
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/no-exec-eval
21
+ ```
22
+
23
+ ### case: exec with a namespace
24
+
25
+ ```py
26
+ exec(code, {})
27
+ ```
28
+
29
+ ```expect
30
+ 1:1 sec-py/no-exec-eval
31
+ ```
32
+
33
+ ### case: a method named eval
34
+
35
+ ```py
36
+ model.eval()
37
+ ```
38
+
39
+ ```expect
40
+ (none)
41
+ ```
42
+
43
+ ### case: a name
44
+
45
+ ```py
46
+ compile(source, 'f.py', 'exec')
47
+ ```
48
+
49
+ ```expect
50
+ 1:1 sec-py/no-exec-eval
51
+ ```
@@ -0,0 +1,56 @@
1
+ ---
2
+ id: sec-py/no-exec-eval
3
+ title: "`exec`, `eval` and `compile` of a non-literal"
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "the safe replacement (`ast.literal_eval`, a lookup table, a parser) depends on what the string is for"
7
+ stability: preview
8
+ ported_from: [bandit/B102, bandit/B307]
9
+ security: { cwe: [94], owasp: ["A05:2025"], severity: high }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ `exec`, `eval` and `compile` are not given a value that is not a string literal.
14
+
15
+ **Why:** The string is run as code, so whoever controls it controls the program (CWE-94).
16
+
17
+ ```codelaw
18
+ match:
19
+ any:
20
+ - { pattern: 'exec($X)' }
21
+ - { pattern: 'exec($X, $$$R)' }
22
+ - { pattern: 'eval($X)' }
23
+ - { pattern: 'eval($X, $$$R)' }
24
+ - all:
25
+ - { pattern: 'compile($X, $$$R)' }
26
+ - not:
27
+ inside:
28
+ kind: argument_list
29
+ inside: { kind: call, has: { field: function, regex: '^(exec|eval)$' } }
30
+ constraints: { X: { not: { kind: string, not: { has: { kind: interpolation } } } } }
31
+ message: '`$X` is run as code; use `ast.literal_eval` or a lookup table'
32
+ ```
33
+
34
+ ## How to fix
35
+
36
+ 1. For a literal (a number, a list, a dict), call `ast.literal_eval`.
37
+ 2. For a choice among behaviours, map the input to a function through a dict.
38
+ 3. For arithmetic or expressions, use a parser built for it, not the interpreter.
39
+
40
+ ## Examples
41
+
42
+ ### Python
43
+
44
+ #### Don't
45
+
46
+ ```py
47
+ result = eval(user_text)
48
+ ```
49
+
50
+ #### Do
51
+
52
+ ```py
53
+ import ast
54
+
55
+ result = ast.literal_eval(user_text)
56
+ ```
@@ -0,0 +1,35 @@
1
+ # sec-py/no-format-string-injection — cases
2
+
3
+ ### case: a request template
4
+
5
+ ```py
6
+ def g(request):
7
+ return request.GET["t"].format(user)
8
+ ```
9
+
10
+ ```expect
11
+ 2:12 sec-py/no-format-string-injection
12
+ ```
13
+
14
+ ### case: a literal template
15
+
16
+ ```py
17
+ def g(request):
18
+ return "Hello {}".format(request.GET["n"])
19
+ ```
20
+
21
+ ```expect
22
+ (none)
23
+ ```
24
+
25
+ ### case: format_map
26
+
27
+ ```py
28
+ def g(request):
29
+ t = request.args["t"]
30
+ return t.format_map(d)
31
+ ```
32
+
33
+ ```expect
34
+ 3:12 sec-py/no-format-string-injection
35
+ ```
@@ -0,0 +1,44 @@
1
+ ---
2
+ id: sec-py/no-format-string-injection
3
+ title: 'A request value is not a format string'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "the template has to be a literal, which needs the format string moved out of the request"
7
+ stability: preview
8
+ security: { cwe: [134], owasp: ["A05:2025"], severity: high }
9
+ since: 0.2.0
10
+ ---
11
+
12
+ A value from the request is not the receiver of `.format`.
13
+
14
+ **Why:** A field such as `{obj.__class__.__init__.__globals__[SECRET]}` reads attributes of the arguments, so the client reads secrets (CWE-134). This rule uses `from_source:`, which is not taint analysis: it sees a request value only in the function that reads it, never through a call or a parameter.
15
+
16
+ ```codelaw
17
+ match:
18
+ any: [{ pattern: '$V.format($$$A)' }, { pattern: '$V.format_map($$$A)' }]
19
+ where: { $V: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
20
+ message: '`$V` comes from the request and is used as a format string'
21
+ ```
22
+
23
+ ## How to fix
24
+
25
+ 1. Make the template a literal and pass the request value as an argument: `"Hello {}".format(name)`.
26
+ 2. For client-written templates use `string.Template(...).safe_substitute(...)`, which has no attribute access.
27
+
28
+ ## Examples
29
+
30
+ ### Python
31
+
32
+ #### Don't
33
+
34
+ ```py
35
+ def greet(request):
36
+ return request.GET["tpl"].format(user)
37
+ ```
38
+
39
+ #### Do
40
+
41
+ ```py
42
+ def greet(request):
43
+ return "Hello {}".format(request.GET["name"])
44
+ ```
@@ -0,0 +1,51 @@
1
+ # sec-py/no-hardcoded-crypto-key — cases
2
+
3
+ ### case: Fernet
4
+
5
+ ```py
6
+ Fernet(b"abc")
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-hardcoded-crypto-key
11
+ ```
12
+
13
+ ### case: AES
14
+
15
+ ```py
16
+ AES.new(b"0123456789abcdef", AES.MODE_GCM)
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/no-hardcoded-crypto-key
21
+ ```
22
+
23
+ ### case: a key from the environment
24
+
25
+ ```py
26
+ Fernet(os.environ["K"])
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
32
+
33
+ ### case: a fixed IV
34
+
35
+ ```py
36
+ Cipher(alg, modes.CBC(b"0000000000000000"))
37
+ ```
38
+
39
+ ```expect
40
+ 1:13 sec-py/no-hardcoded-crypto-key
41
+ ```
42
+
43
+ ### case: a keyword key
44
+
45
+ ```py
46
+ sign(key="secret-key")
47
+ ```
48
+
49
+ ```expect
50
+ 1:1 sec-py/no-hardcoded-crypto-key
51
+ ```
@@ -0,0 +1,91 @@
1
+ ---
2
+ id: sec-py/no-hardcoded-crypto-key
3
+ title: 'No keys or IVs in the source'
4
+ languages: [python]
5
+ exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
6
+ fix: manual
7
+ fix_reason: "where the key comes from (a key store, an environment variable) is a deployment decision"
8
+ stability: preview
9
+ security: { cwe: [321, 329], owasp: ["A04:2025"], severity: high }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ A cipher key, HMAC key, IV or nonce is not a literal.
14
+
15
+ **Why:** A key in the source is known to everyone who can read the source, and a fixed IV or nonce breaks the cipher's guarantees (CWE-321, CWE-329).
16
+
17
+ ```codelaw
18
+ match:
19
+ any:
20
+ - kind: call
21
+ all:
22
+ - has:
23
+ field: function
24
+ regex: '^((Crypto|Cryptodome)\.Cipher\.)?(AES|DES|DES3|ChaCha20|Blowfish|ARC4|Salsa20)\.new$|^(Fernet|hmac\.new|ChaCha20Poly1305|AESGCM|AESCCM)$'
25
+ - has:
26
+ field: arguments
27
+ has: { nthChild: 1, kind: string, not: { has: { kind: interpolation } } }
28
+ - kind: call
29
+ all:
30
+ - { has: { field: function, regex: '^(modes\.)?(CBC|CTR|GCM|CFB|OFB|CCM)$' } }
31
+ - has:
32
+ field: arguments
33
+ has: { nthChild: 1, kind: string, not: { has: { kind: interpolation } } }
34
+ - all:
35
+ - { kind: call }
36
+ - has:
37
+ field: arguments
38
+ has:
39
+ kind: keyword_argument
40
+ all:
41
+ - { has: { field: name, regex: '^iv$' } }
42
+ - { has: { field: value, kind: string, not: { has: { kind: interpolation } } } }
43
+ - all:
44
+ - { kind: call }
45
+ - has:
46
+ field: arguments
47
+ has:
48
+ kind: keyword_argument
49
+ all:
50
+ - { has: { field: name, regex: '^nonce$' } }
51
+ - { has: { field: value, kind: string, not: { has: { kind: interpolation } } } }
52
+ - all:
53
+ - { kind: call }
54
+ - has:
55
+ field: function
56
+ regex: '(?i)(cipher|crypt|aes|fernet|hmac|jwt|sign|cmac|pbkdf|scrypt)'
57
+ - has:
58
+ field: arguments
59
+ has:
60
+ kind: keyword_argument
61
+ all:
62
+ - { has: { field: name, regex: '^key$' } }
63
+ - { has: { field: value, kind: string, not: { has: { kind: interpolation } } } }
64
+ message: 'A key, IV or nonce is a literal; load the key from a key store and make the IV random'
65
+ ```
66
+
67
+ ## How to fix
68
+
69
+ 1. Generate keys with `os.urandom` or the library (`Fernet.generate_key()`) once, and store them in a secret manager or an environment variable.
70
+ 2. Make a fresh random IV or nonce for every message (`os.urandom(12)`) and store it beside the ciphertext.
71
+
72
+ ## Examples
73
+
74
+ ### Python
75
+
76
+ #### Don't
77
+
78
+ ```py
79
+ from cryptography.fernet import Fernet
80
+
81
+ f = Fernet(b"ZmFrZWtleWZha2VrZXlmYWtla2V5ZmFrZWtleTA9")
82
+ ```
83
+
84
+ #### Do
85
+
86
+ ```py
87
+ import os
88
+ from cryptography.fernet import Fernet
89
+
90
+ f = Fernet(os.environ["FERNET_KEY"])
91
+ ```
@@ -0,0 +1,61 @@
1
+ # sec-py/no-hardcoded-secret — cases
2
+
3
+ ### case: a password assignment
4
+
5
+ ```py
6
+ password = "s3cr3tvalue"
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-hardcoded-secret
11
+ ```
12
+
13
+ ### case: a keyword argument
14
+
15
+ ```py
16
+ connect(host, password="s3cr3tvalue")
17
+ ```
18
+
19
+ ```expect
20
+ 1:15 sec-py/no-hardcoded-secret
21
+ ```
22
+
23
+ ### case: a placeholder is short
24
+
25
+ ```py
26
+ password = ""
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
32
+
33
+ ### case: an environment read
34
+
35
+ ```py
36
+ password = os.environ["PASSWORD"]
37
+ ```
38
+
39
+ ```expect
40
+ (none)
41
+ ```
42
+
43
+ ### case: an AWS key
44
+
45
+ ```py
46
+ key = "AKIAIOSFODNN7EXAMPLE"
47
+ ```
48
+
49
+ ```expect
50
+ 1:7 sec-py/no-hardcoded-secret
51
+ ```
52
+
53
+ ### case: a name that is not a secret
54
+
55
+ ```py
56
+ title = "A long title string"
57
+ ```
58
+
59
+ ```expect
60
+ (none)
61
+ ```
@@ -0,0 +1,82 @@
1
+ ---
2
+ id: sec-py/no-hardcoded-secret
3
+ title: 'No secrets in the source'
4
+ languages: [python]
5
+ exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
6
+ fix: manual
7
+ fix_reason: "where the secret lives instead (environment, secret manager) is a decision, and the leaked one must be rotated"
8
+ stability: preview
9
+ ported_from: [bandit/B105, bandit/B106, bandit/B107, ruff/S105, ruff/S106, ruff/S107]
10
+ security: { cwe: [798], owasp: ["A07:2025"], severity: high }
11
+ since: 0.2.0
12
+ ---
13
+
14
+ A password, key or token is not a string literal in the code, and a cloud access key is not pasted into it.
15
+
16
+ **Why:** The source goes to every clone, build log and backup, so the secret does too, and revoking it means a release (CWE-798).
17
+
18
+ ```codelaw
19
+ match:
20
+ any:
21
+ - all:
22
+ - { kind: assignment }
23
+ - has:
24
+ field: left
25
+ regex: '(?i)^_*[a-z0-9_]*(pass(word|wd)?|secret|api_?key|private_?key|token|credentials?|auth_?key|access_?key)[a-z0-9_]*$'
26
+ not:
27
+ regex: '(?i)(_type|_url|_uri|_path|_file|_header|_name|_field|_class|_backend|_func|_label|_prefix)$'
28
+ - has:
29
+ field: right
30
+ kind: string
31
+ regex: '^[rb]?("[^"]{4,}"|''[^'']{4,}'')$'
32
+ not:
33
+ any:
34
+ - { has: { kind: interpolation } }
35
+ - { regex: '^[rb]?[''"][*x#.\-]+[''"]$' }
36
+ - { regex: '^[rb]?[''"][A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+[''"]$' }
37
+ - all:
38
+ - { kind: keyword_argument }
39
+ - has:
40
+ field: name
41
+ regex: '(?i)^_*[a-z0-9_]*(pass(word|wd)?|secret|api_?key|private_?key|token|credentials?|auth_?key|access_?key)[a-z0-9_]*$'
42
+ not:
43
+ regex: '(?i)(_type|_url|_uri|_path|_file|_header|_name|_field|_class|_backend|_func|_label|_prefix)$'
44
+ - has:
45
+ field: value
46
+ kind: string
47
+ regex: '^[rb]?("[^"]{4,}"|''[^'']{4,}'')$'
48
+ not:
49
+ any:
50
+ - { has: { kind: interpolation } }
51
+ - { regex: '^[rb]?[''"][*x#.\-]+[''"]$' }
52
+ - { regex: '^[rb]?[''"][A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+[''"]$' }
53
+ - { kind: string, regex: '^[''"](AKIA|ASIA)[0-9A-Z]{16}[''"]$' }
54
+ - { kind: string, regex: '^[''"](ghp|gho|ghs|ghu)_[A-Za-z0-9]{36,}[''"]$' }
55
+ - { kind: string, regex: '^[''"]xox[abprs]-[A-Za-z0-9-]{10,}[''"]$' }
56
+ - { kind: string, regex: '^[''"]sk_live_[A-Za-z0-9]{16,}[''"]$' }
57
+ message: 'A secret is written in the source; read it from the environment or a secret manager'
58
+ ```
59
+
60
+ ## How to fix
61
+
62
+ 1. Rotate the secret: it is in version control now.
63
+ 2. Read it at start-up: `os.environ["DB_PASSWORD"]`, or from your secret manager.
64
+ 3. Keep local values in an untracked `.env` file.
65
+
66
+ ## Examples
67
+
68
+ ### Python
69
+
70
+ #### Don't
71
+
72
+ ```py
73
+ DB_PASSWORD = "hunter2-prod"
74
+ ```
75
+
76
+ #### Do
77
+
78
+ ```py
79
+ import os
80
+
81
+ DB_PASSWORD = os.environ["DB_PASSWORD"]
82
+ ```
@@ -0,0 +1,45 @@
1
+ # sec-py/no-header-injection — cases
2
+
3
+ ### case: a request value
4
+
5
+ ```py
6
+ def v(request):
7
+ resp.headers["X-A"] = request.args["a"]
8
+ ```
9
+
10
+ ```expect
11
+ 2:5 sec-py/no-header-injection
12
+ ```
13
+
14
+ ### case: a constant
15
+
16
+ ```py
17
+ def v(request):
18
+ resp.headers["X-A"] = "1"
19
+ ```
20
+
21
+ ```expect
22
+ (none)
23
+ ```
24
+
25
+ ### case: Django
26
+
27
+ ```py
28
+ def v(request):
29
+ response["X-A"] = request.GET["a"]
30
+ ```
31
+
32
+ ```expect
33
+ 2:5 sec-py/no-header-injection
34
+ ```
35
+
36
+ ### case: a dict is not matched
37
+
38
+ ```py
39
+ def v(request):
40
+ d["x"] = request.GET["a"]
41
+ ```
42
+
43
+ ```expect
44
+ (none)
45
+ ```
@@ -0,0 +1,55 @@
1
+ ---
2
+ id: sec-py/no-header-injection
3
+ title: 'A request value is not a response header'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "whether to reject, strip or encode the value depends on what the header is for"
7
+ stability: preview
8
+ security: { cwe: [113], owasp: ["A05:2025"], severity: medium }
9
+ since: 0.2.0
10
+ ---
11
+
12
+ A value from the request is not written into a response header or a cookie value without its line breaks removed.
13
+
14
+ **Why:** A CR or LF in the value ends the header and starts a new one, so an attacker can add headers or a second response (CWE-113). This rule uses `from_source:`, which is not taint analysis: it sees a request value only in the function that reads it, never through a call or a parameter.
15
+
16
+ ```codelaw
17
+ match:
18
+ any:
19
+ - { pattern: '$R.headers[$K] = $V' }
20
+ - { pattern: '$R.headers.add($K, $V)' }
21
+ - { pattern: '$R.headers.set($K, $V)' }
22
+ - { pattern: '$R[$K] = $V' }
23
+ - { pattern: '$R.set_cookie($K, $V)' }
24
+ constraints: { R: { regex: '^(resp|response|res|r|rv|rsp|self\.response|make_response\(.*\))$' } }
25
+ where: { $V: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
26
+ message: '`$V` comes from the request and is written into a header'
27
+ ```
28
+
29
+ ## How to fix
30
+
31
+ 1. Reject a value that contains `\r` or `\n`: `if "\n" in value or "\r" in value: abort(400)`.
32
+ 2. Or percent-encode it: `urllib.parse.quote(value)`.
33
+ 3. Modern servers and frameworks refuse newlines in headers; do not rely on that.
34
+
35
+ ## Examples
36
+
37
+ ### Python
38
+
39
+ #### Don't
40
+
41
+ ```py
42
+ def view(request):
43
+ resp = make_response("ok")
44
+ resp.headers["X-Name"] = request.args["name"]
45
+ return resp
46
+ ```
47
+
48
+ #### Do
49
+
50
+ ```py
51
+ def view(request):
52
+ resp = make_response("ok")
53
+ resp.headers["X-Name"] = quote(request.args["name"])
54
+ return resp
55
+ ```
@@ -0,0 +1,41 @@
1
+ # sec-py/no-insecure-protocol — cases
2
+
3
+ ### case: an import
4
+
5
+ ```py
6
+ import telnetlib
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-insecure-protocol
11
+ ```
12
+
13
+ ### case: a from import
14
+
15
+ ```py
16
+ from ftplib import FTP
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/no-insecure-protocol
21
+ ```
22
+
23
+ ### case: SNMP
24
+
25
+ ```py
26
+ CommunityData("public")
27
+ ```
28
+
29
+ ```expect
30
+ 1:1 sec-py/no-insecure-protocol
31
+ ```
32
+
33
+ ### case: ssh
34
+
35
+ ```py
36
+ import paramiko
37
+ ```
38
+
39
+ ```expect
40
+ (none)
41
+ ```
@@ -0,0 +1,52 @@
1
+ ---
2
+ id: sec-py/no-insecure-protocol
3
+ title: 'No Telnet, FTP or SNMP v1/v2'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "the replacement (SSH or SFTP, FTPS, SNMPv3) needs the server to support it"
7
+ stability: preview
8
+ ported_from: [bandit/B312, bandit/B321, bandit/B401, bandit/B402, bandit/B508, bandit/B509, ruff/S401, ruff/S402, ruff/S312, ruff/S321, ruff/S508, ruff/S509]
9
+ security: { cwe: [319], owasp: ["A04:2025"], severity: high }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ `telnetlib`, `ftplib` and SNMP community strings (v1/v2c) are not used.
14
+
15
+ **Why:** These send credentials and data in clear text, so anyone on the path reads and changes them (CWE-319).
16
+
17
+ ```codelaw
18
+ match:
19
+ any:
20
+ - all: [{ kind: import_statement }, { has: { field: name, regex: '^(telnetlib|ftplib)$' } }]
21
+ - all:
22
+ - { kind: import_from_statement }
23
+ - { has: { field: module_name, regex: '^(telnetlib|ftplib)$' } }
24
+ - kind: call
25
+ all: [{ has: { field: function, regex: '^(pysnmp\.hlapi\.)?CommunityData$' } }]
26
+ message: 'This protocol sends credentials in clear text'
27
+ ```
28
+
29
+ ## How to fix
30
+
31
+ 1. Use SSH or SFTP (`paramiko`, `asyncssh`) instead of Telnet and FTP, or `ftplib.FTP_TLS` for FTPS.
32
+ 2. Use SNMPv3 (`UsmUserData` with authentication and privacy) instead of a community string.
33
+
34
+ ## Examples
35
+
36
+ ### Python
37
+
38
+ #### Don't
39
+
40
+ ```py
41
+ import ftplib
42
+
43
+ ftp = ftplib.FTP(host)
44
+ ```
45
+
46
+ #### Do
47
+
48
+ ```py
49
+ import paramiko
50
+
51
+ client = paramiko.SSHClient()
52
+ ```