codelaw-rules-security-python 0.4.2__py3-none-any.whl → 0.5.1__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 (69) hide show
  1. codelaw_rules_security_python/README.md +12 -0
  2. codelaw_rules_security_python/codelaw-pack.toml +1 -1
  3. codelaw_rules_security_python/corpus.toml +5 -0
  4. codelaw_rules_security_python/rules/sec-py/django-settings.cases.md +14 -0
  5. codelaw_rules_security_python/rules/sec-py/django-settings.md +7 -9
  6. codelaw_rules_security_python/rules/sec-py/fastapi-cors.md +1 -0
  7. codelaw_rules_security_python/rules/sec-py/flask-settings.cases.md +28 -0
  8. codelaw_rules_security_python/rules/sec-py/flask-settings.md +9 -9
  9. codelaw_rules_security_python/rules/sec-py/hf-pin-revision.md +1 -0
  10. codelaw_rules_security_python/rules/sec-py/logging-listen.md +1 -0
  11. codelaw_rules_security_python/rules/sec-py/ml-unsafe-load.cases.md +14 -0
  12. codelaw_rules_security_python/rules/sec-py/ml-unsafe-load.md +5 -2
  13. codelaw_rules_security_python/rules/sec-py/no-assert-for-auth.cases.md +16 -0
  14. codelaw_rules_security_python/rules/sec-py/no-assert-for-auth.md +21 -2
  15. codelaw_rules_security_python/rules/sec-py/no-bind-all-interfaces.cases.md +38 -0
  16. codelaw_rules_security_python/rules/sec-py/no-bind-all-interfaces.md +25 -4
  17. codelaw_rules_security_python/rules/sec-py/no-cleartext-http.md +1 -0
  18. codelaw_rules_security_python/rules/sec-py/no-csrf-exempt.cases.md +17 -0
  19. codelaw_rules_security_python/rules/sec-py/no-csrf-exempt.md +13 -2
  20. codelaw_rules_security_python/rules/sec-py/no-dynamic-import.md +1 -0
  21. codelaw_rules_security_python/rules/sec-py/no-exec-eval.md +1 -0
  22. codelaw_rules_security_python/rules/sec-py/no-format-string-injection.md +1 -0
  23. codelaw_rules_security_python/rules/sec-py/no-hardcoded-crypto-key.md +1 -0
  24. codelaw_rules_security_python/rules/sec-py/no-hardcoded-secret.md +1 -0
  25. codelaw_rules_security_python/rules/sec-py/no-header-injection.cases.md +16 -0
  26. codelaw_rules_security_python/rules/sec-py/no-header-injection.md +29 -2
  27. codelaw_rules_security_python/rules/sec-py/no-insecure-protocol.md +2 -1
  28. codelaw_rules_security_python/rules/sec-py/no-ldap-injection.md +1 -0
  29. codelaw_rules_security_python/rules/sec-py/no-llm-output-to-sink.md +1 -0
  30. codelaw_rules_security_python/rules/sec-py/no-log-injection.cases.md +16 -0
  31. codelaw_rules_security_python/rules/sec-py/no-log-injection.md +11 -2
  32. codelaw_rules_security_python/rules/sec-py/no-mako.cases.md +30 -0
  33. codelaw_rules_security_python/rules/sec-py/no-mako.md +18 -3
  34. codelaw_rules_security_python/rules/sec-py/no-mark-safe.md +1 -0
  35. codelaw_rules_security_python/rules/sec-py/no-mass-assignment.md +1 -0
  36. codelaw_rules_security_python/rules/sec-py/no-nosql-injection.md +1 -0
  37. codelaw_rules_security_python/rules/sec-py/no-open-redirect.md +1 -0
  38. codelaw_rules_security_python/rules/sec-py/no-partial-path.md +1 -0
  39. codelaw_rules_security_python/rules/sec-py/no-path-traversal.md +2 -1
  40. codelaw_rules_security_python/rules/sec-py/no-prompt-injection.md +1 -0
  41. codelaw_rules_security_python/rules/sec-py/no-pycrypto.md +1 -0
  42. codelaw_rules_security_python/rules/sec-py/no-reflected-xss.md +2 -1
  43. codelaw_rules_security_python/rules/sec-py/no-secret-in-log.cases.md +27 -0
  44. codelaw_rules_security_python/rules/sec-py/no-secret-in-log.md +14 -6
  45. codelaw_rules_security_python/rules/sec-py/no-silent-except.md +1 -0
  46. codelaw_rules_security_python/rules/sec-py/no-sql-string.md +1 -0
  47. codelaw_rules_security_python/rules/sec-py/no-ssrf.md +1 -0
  48. codelaw_rules_security_python/rules/sec-py/no-stack-trace-response.md +1 -0
  49. codelaw_rules_security_python/rules/sec-py/no-trust-remote-code.cases.md +14 -0
  50. codelaw_rules_security_python/rules/sec-py/no-trust-remote-code.md +11 -2
  51. codelaw_rules_security_python/rules/sec-py/no-unsafe-deserialize.md +1 -0
  52. codelaw_rules_security_python/rules/sec-py/no-weak-cipher.md +1 -0
  53. codelaw_rules_security_python/rules/sec-py/no-wildcard-injection.md +1 -0
  54. codelaw_rules_security_python/rules/sec-py/no-xmlrpc.md +1 -0
  55. codelaw_rules_security_python/rules/sec-py/no-xpath-injection.md +1 -0
  56. codelaw_rules_security_python/rules/sec-py/paramiko-call.md +51 -0
  57. codelaw_rules_security_python/rules/sec-py/paramiko-host-key.md +10 -8
  58. codelaw_rules_security_python/rules/sec-py/password-hash-effort.md +1 -0
  59. codelaw_rules_security_python/rules/sec-py/start-process-with-no-shell.cases.md +25 -0
  60. codelaw_rules_security_python/rules/sec-py/start-process-with-no-shell.md +50 -0
  61. codelaw_rules_security_python/rules/sec-py/suspicious-httpoxy-import.md +50 -0
  62. codelaw_rules_security_python/rules/sec-py/suspicious-pyghmi-import.md +50 -0
  63. codelaw_rules_security_python/rules/sec-py/suspicious-subprocess-import.cases.md +24 -0
  64. codelaw_rules_security_python/rules/sec-py/suspicious-subprocess-import.md +51 -0
  65. codelaw_rules_security_python/rules/sec-py/urlopen-scheme.md +1 -0
  66. {codelaw_rules_security_python-0.4.2.dist-info → codelaw_rules_security_python-0.5.1.dist-info}/METADATA +13 -1
  67. {codelaw_rules_security_python-0.4.2.dist-info → codelaw_rules_security_python-0.5.1.dist-info}/RECORD +69 -62
  68. {codelaw_rules_security_python-0.4.2.dist-info → codelaw_rules_security_python-0.5.1.dist-info}/WHEEL +0 -0
  69. {codelaw_rules_security_python-0.4.2.dist-info → codelaw_rules_security_python-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -43,3 +43,19 @@ def v(request):
43
43
  ```expect
44
44
  (none)
45
45
  ```
46
+
47
+ ### case: the message form is wrapped
48
+
49
+ ```py
50
+ def v(request):
51
+ logger.warning(request.GET["u"])
52
+ ```
53
+
54
+ ```expect
55
+ 2:5 sec-py/no-log-injection
56
+ ```
57
+
58
+ ```fixed
59
+ def v(request):
60
+ logger.warning(str(request.GET["u"]).replace("\r", "").replace("\n", " "))
61
+ ```
@@ -3,8 +3,8 @@ id: sec-py/no-log-injection
3
3
  title: 'A request value is not logged raw'
4
4
  languages: [python]
5
5
  severity: warn
6
- fix: manual
7
- fix_reason: "whether to strip, escape or reject the value depends on how the log is read"
6
+ fix: suggest
7
+ fix_reason: "the reviewer confirms line breaks in the value are only noise for this log, since stripping them changes the logged text"
8
8
  stability: preview
9
9
  security: { cwe: [117], owasp: ["A09:2025"], severity: low }
10
10
  since: 0.2.0
@@ -22,6 +22,8 @@ constraints:
22
22
  A: { regex: '^[rbf]?[''\"]' }
23
23
  where: { $V: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
24
24
  message: '`$V` comes from the request and is logged as it is'
25
+ fix:
26
+ - replace: { target: $V, with: 'str($V).replace("\r", "").replace("\n", " ")' }
25
27
  ```
26
28
 
27
29
  ## How to fix
@@ -40,6 +42,13 @@ def view(request):
40
42
  logger.info("login %s", request.POST["user"])
41
43
  ```
42
44
 
45
+ #### Fixed
46
+
47
+ ```py
48
+ def view(request):
49
+ logger.info("login %s", str(request.POST["user"]).replace("\r", "").replace("\n", " "))
50
+ ```
51
+
43
52
  #### Do
44
53
 
45
54
  ```py
@@ -35,3 +35,33 @@ Template(src)
35
35
  ```expect
36
36
  (none)
37
37
  ```
38
+
39
+ ### case: escaping already on
40
+
41
+ ```py
42
+ from mako.template import Template
43
+
44
+ Template(src, default_filters=["h"])
45
+ ```
46
+
47
+ ```expect
48
+ (none)
49
+ ```
50
+
51
+ ### case: escaping is turned on
52
+
53
+ ```py
54
+ from mako.template import Template
55
+
56
+ Template(src, input_encoding="utf-8")
57
+ ```
58
+
59
+ ```expect
60
+ 3:1 sec-py/no-mako
61
+ ```
62
+
63
+ ```fixed
64
+ from mako.template import Template
65
+
66
+ Template(src, input_encoding="utf-8", default_filters=["str", "h"])
67
+ ```
@@ -2,8 +2,8 @@
2
2
  id: sec-py/no-mako
3
3
  title: 'No Mako templates'
4
4
  languages: [python]
5
- fix: manual
6
- fix_reason: "moving to another engine (or turning on `default_filters`) changes every template"
5
+ fix: suggest
6
+ fix_reason: "turning escaping on changes every template's output, so the reviewer confirms each one, and adds `|n` where markup is meant"
7
7
  stability: preview
8
8
  ported_from: [bandit/B702, ruff/S702]
9
9
  security: { cwe: [79], owasp: ["A05:2025"], severity: medium }
@@ -15,7 +15,13 @@ since: 0.2.0
15
15
  **Why:** Mako does not escape output unless told to, so every `${value}` is a script waiting for a hostile value (CWE-79).
16
16
 
17
17
  ```codelaw
18
- match: { any: [{ pattern: '$T($$$A)' }] }
18
+ match:
19
+ all:
20
+ - pattern: '$T($$$A)'
21
+ - not:
22
+ has:
23
+ field: arguments
24
+ has: { kind: keyword_argument, has: { field: name, regex: '^default_filters$' } }
19
25
  constraints: { T: { regex: '^(Template|[A-Za-z_]+\.Template|mako\.template\.Template)$' } }
20
26
  where:
21
27
  $T:
@@ -24,6 +30,7 @@ where:
24
30
  - { imported-from: mako.template }
25
31
  - { imported-from: { module: mako, name: template } }
26
32
  message: 'Mako does not escape by default; use Jinja2 with autoescape, or set `default_filters=[''h'']`'
33
+ fix: '$T($$$A, default_filters=["str", "h"])'
27
34
  ```
28
35
 
29
36
  ## How to fix
@@ -43,6 +50,14 @@ from mako.template import Template
43
50
  t = Template(source)
44
51
  ```
45
52
 
53
+ #### Fixed
54
+
55
+ ```py
56
+ from mako.template import Template
57
+
58
+ t = Template(source, default_filters=["str", "h"])
59
+ ```
60
+
46
61
  #### Do
47
62
 
48
63
  ```py
@@ -4,6 +4,7 @@ title: 'No `mark_safe` of dynamic text'
4
4
  languages: [python]
5
5
  fix: manual
6
6
  fix_reason: "the right fix is escaping the parts (`format_html`, `escape`) or rendering through a template, not removing the call"
7
+ fix_exception: security
7
8
  stability: preview
8
9
  ported_from: [bandit/B308, bandit/B703, bandit/B704, ruff/S308, ruff/S703, ruff/S704]
9
10
  security: { cwe: [79], owasp: ["A05:2025"], severity: high }
@@ -4,6 +4,7 @@ title: 'A request is not splatted into a model'
4
4
  languages: [python]
5
5
  fix: manual
6
6
  fix_reason: "which fields a client may set is the author's decision"
7
+ fix_exception: value
7
8
  stability: preview
8
9
  security: { cwe: [915], owasp: ["A01:2025"], severity: high }
9
10
  since: 0.2.0
@@ -4,6 +4,7 @@ title: 'A request is not a MongoDB query'
4
4
  languages: [python]
5
5
  fix: manual
6
6
  fix_reason: "which fields the client may filter on, and their types, are the author's decision"
7
+ fix_exception: value
7
8
  stability: preview
8
9
  security: { cwe: [943], owasp: ["A05:2025"], severity: high }
9
10
  since: 0.2.0
@@ -4,6 +4,7 @@ title: 'A request value is not a redirect target'
4
4
  languages: [python]
5
5
  fix: manual
6
6
  fix_reason: "the allowed destinations depend on the application"
7
+ fix_exception: security
7
8
  stability: preview
8
9
  security: { cwe: [601], owasp: ["A01:2025"], severity: medium }
9
10
  since: 0.2.0
@@ -5,6 +5,7 @@ languages: [python]
5
5
  severity: warn
6
6
  fix: manual
7
7
  fix_reason: "the absolute path differs by system, so it has to come from configuration or `shutil.which` at start-up"
8
+ fix_exception: value
8
9
  stability: preview
9
10
  ported_from: [bandit/B607, ruff/S607]
10
11
  security: { cwe: [426], owasp: ["A08:2025"], severity: low }
@@ -3,7 +3,8 @@ id: sec-py/no-path-traversal
3
3
  title: 'A request value is not a file path'
4
4
  languages: [python]
5
5
  fix: manual
6
- fix_reason: "the safe form (a lookup by id, a resolved path checked against a base folder) depends on the storage layout"
6
+ fix_reason: "the base folder the path must stay inside, or the id to look the file up by, is not in the code"
7
+ fix_exception: value
7
8
  stability: preview
8
9
  security: { cwe: [22], owasp: ["A01:2025"], severity: high }
9
10
  since: 0.2.0
@@ -4,6 +4,7 @@ title: 'A request value is not written into a system prompt'
4
4
  languages: [python]
5
5
  fix: manual
6
6
  fix_reason: "what goes in the system role and what goes in the user role is a design decision"
7
+ fix_exception: restructure
7
8
  stability: preview
8
9
  security: { cwe: [1427], owasp: ["A05:2025"], severity: high }
9
10
  since: 0.2.0
@@ -4,6 +4,7 @@ title: 'No `pycrypto`'
4
4
  languages: [python]
5
5
  fix: manual
6
6
  fix_reason: "the replacement package changes the import path and some call signatures"
7
+ fix_exception: restructure
7
8
  stability: preview
8
9
  ported_from: [bandit/B413, ruff/S413]
9
10
  security: { cwe: [1104], owasp: ["A03:2025"], severity: medium }
@@ -4,7 +4,8 @@ title: 'A request value is not written into the response as it is'
4
4
  languages: [python]
5
5
  exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
6
6
  fix: manual
7
- fix_reason: "escaping the value or rendering a template are both fixes; which fits depends on the response"
7
+ fix_reason: "escaping the value or rendering a template are both fixes, and a suggested edit would read as made safe; which fits depends on the response"
8
+ fix_exception: security
8
9
  stability: preview
9
10
  security: { cwe: [79], owasp: ["A05:2025"], severity: high }
10
11
  since: 0.2.0
@@ -39,3 +39,30 @@ print(api_key)
39
39
  ```expect
40
40
  1:1 sec-py/no-secret-in-log
41
41
  ```
42
+
43
+ ### case: the secret argument is redacted
44
+
45
+ ```py
46
+ log.debug(f"token {token}")
47
+ print(api_key)
48
+ ```
49
+
50
+ ```expect
51
+ 1:1 sec-py/no-secret-in-log
52
+ 2:1 sec-py/no-secret-in-log
53
+ ```
54
+
55
+ ```fixed
56
+ log.debug("[REDACTED]")
57
+ print("[REDACTED]")
58
+ ```
59
+
60
+ ### case: a keyword argument has no edit
61
+
62
+ ```py
63
+ logger.info("login", extra=dict(password=p))
64
+ ```
65
+
66
+ ```expect
67
+ 1:1 sec-py/no-secret-in-log
68
+ ```
@@ -2,8 +2,8 @@
2
2
  id: sec-py/no-secret-in-log
3
3
  title: 'Secrets are not logged'
4
4
  languages: [python]
5
- fix: manual
6
- fix_reason: "whether to drop the value or mask it depends on what the log is for"
5
+ fix: suggest
6
+ fix_reason: "the reviewer confirms a redacted argument loses nothing the log needs; a keyword argument (`password=...`) gets no edit"
7
7
  stability: preview
8
8
  security: { cwe: [532], owasp: ["A09:2025"], severity: medium }
9
9
  since: 0.2.0
@@ -25,12 +25,14 @@ match:
25
25
  has:
26
26
  regex: '(?i)(pass(word|wd)?|secret|token|api_?key|auth|otp|nonce|salt|session_?id|csrf|reset_?code|verification_?code)'
27
27
  any:
28
- - { kind: identifier }
29
- - { kind: attribute }
30
- - { kind: subscript }
31
- - { kind: string, has: { kind: interpolation } }
28
+ - { kind: identifier, pattern: $S }
29
+ - { kind: attribute, pattern: $S }
30
+ - { kind: subscript, pattern: $S }
31
+ - { kind: string, has: { kind: interpolation }, pattern: $S }
32
32
  - { kind: keyword_argument }
33
33
  message: 'A secret may be written to the log; log that it was set, not its value'
34
+ fix:
35
+ - replace: { target: $S, with: '"[REDACTED]"' }
34
36
  ```
35
37
 
36
38
  ## How to fix
@@ -48,6 +50,12 @@ message: 'A secret may be written to the log; log that it was set, not its value
48
50
  logger.info("login with password %s", password)
49
51
  ```
50
52
 
53
+ #### Fixed
54
+
55
+ ```py
56
+ logger.info("login with password %s", "[REDACTED]")
57
+ ```
58
+
51
59
  #### Do
52
60
 
53
61
  ```py
@@ -5,6 +5,7 @@ languages: [python]
5
5
  exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
6
6
  fix: manual
7
7
  fix_reason: "what to do with the error (log, retry, stop) is a decision"
8
+ fix_exception: security
8
9
  stability: preview
9
10
  ported_from: [bandit/B110, bandit/B112, ruff/S110, ruff/S112]
10
11
  security: { cwe: [703], owasp: ["A10:2025"], severity: low }
@@ -4,6 +4,7 @@ title: 'SQL built from a string'
4
4
  languages: [python]
5
5
  fix: manual
6
6
  fix_reason: "which placeholder style the driver takes, and where each value goes, depends on the driver"
7
+ fix_exception: security
7
8
  stability: preview
8
9
  ported_from: [bandit/B608, bandit/B610, bandit/B611]
9
10
  security: { cwe: [89], owasp: ["A05:2025"], severity: high }
@@ -4,6 +4,7 @@ title: 'A request value is not a URL to fetch'
4
4
  languages: [python]
5
5
  fix: manual
6
6
  fix_reason: "which hosts are allowed is a policy only the author knows"
7
+ fix_exception: value
7
8
  stability: preview
8
9
  security: { cwe: [918], owasp: ["A01:2025"], severity: high }
9
10
  since: 0.2.0
@@ -5,6 +5,7 @@ languages: [python]
5
5
  exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
6
6
  fix: manual
7
7
  fix_reason: "what the client sees instead (a generic message and an error id) is a product decision"
8
+ fix_exception: security
8
9
  stability: preview
9
10
  security: { cwe: [209], owasp: ["A10:2025"], severity: medium }
10
11
  since: 0.2.0
@@ -29,3 +29,17 @@ load_dataset("x", trust_remote_code=True)
29
29
  ```expect
30
30
  1:1 sec-py/no-trust-remote-code
31
31
  ```
32
+
33
+ ### case: the keyword is removed with its comma
34
+
35
+ ```py
36
+ AutoModel.from_pretrained(n, trust_remote_code=True, revision=r)
37
+ ```
38
+
39
+ ```expect
40
+ 1:1 sec-py/no-trust-remote-code
41
+ ```
42
+
43
+ ```fixed
44
+ AutoModel.from_pretrained(n, revision=r)
45
+ ```
@@ -2,8 +2,8 @@
2
2
  id: sec-py/no-trust-remote-code
3
3
  title: 'No `trust_remote_code=True`'
4
4
  languages: [python]
5
- fix: manual
6
- fix_reason: "using a model that needs custom code means reviewing and vendoring that code, which is the author's call"
5
+ fix: suggest
6
+ fix_reason: "removing the keyword fails closed, but a model that needs its custom code stops loading, so the reviewer confirms it does not, or vendors and pins the code"
7
7
  stability: preview
8
8
  security: { cwe: [94], owasp: ["A03:2025"], severity: high }
9
9
  since: 0.2.0
@@ -21,10 +21,13 @@ match:
21
21
  field: arguments
22
22
  has:
23
23
  kind: keyword_argument
24
+ pattern: $K
24
25
  all:
25
26
  - { has: { field: name, regex: '^trust_remote_code$' } }
26
27
  - { has: { field: value, kind: 'true' } }
27
28
  message: 'This runs code from the model repository; review it and vendor it, or use a model without custom code'
29
+ fix:
30
+ - remove: $K
28
31
  ```
29
32
 
30
33
  ## How to fix
@@ -43,6 +46,12 @@ message: 'This runs code from the model repository; review it and vendor it, or
43
46
  model = AutoModel.from_pretrained(name, trust_remote_code=True)
44
47
  ```
45
48
 
49
+ #### Fixed
50
+
51
+ ```py
52
+ model = AutoModel.from_pretrained(name)
53
+ ```
54
+
46
55
  #### Do
47
56
 
48
57
  ```py
@@ -4,6 +4,7 @@ title: 'Deserialising with `pickle` and its relatives'
4
4
  languages: [python]
5
5
  fix: manual
6
6
  fix_reason: "the replacement format (JSON, a schema, a signed payload) is a design choice"
7
+ fix_exception: restructure
7
8
  stability: preview
8
9
  ported_from: [bandit/B301, bandit/B302, bandit/B403]
9
10
  security: { cwe: [502], owasp: ["A08:2025"], severity: high }
@@ -4,6 +4,7 @@ title: 'No DES, RC4, Blowfish or ECB'
4
4
  languages: [python]
5
5
  fix: manual
6
6
  fix_reason: "the new cipher changes key size, nonce handling and the stored format"
7
+ fix_exception: restructure
7
8
  stability: preview
8
9
  ported_from: [bandit/B304, bandit/B305, ruff/S304, ruff/S305]
9
10
  security: { cwe: [327], owasp: ["A04:2025"], severity: high }
@@ -4,6 +4,7 @@ title: 'A shell wildcard after `tar`, `chown`, `chmod` or `rsync`'
4
4
  languages: [python]
5
5
  fix: manual
6
6
  fix_reason: "the files have to be listed (`glob`) and passed after `--`, which changes the call"
7
+ fix_exception: security
7
8
  stability: preview
8
9
  ported_from: [bandit/B609, ruff/S609]
9
10
  security: { cwe: [155], owasp: ["A05:2025"], severity: medium }
@@ -4,6 +4,7 @@ title: 'No `xmlrpc` without `defusedxml`'
4
4
  languages: [python]
5
5
  fix: manual
6
6
  fix_reason: "the patch has to run once before any xmlrpc object exists, and where that is depends on the program's start-up"
7
+ fix_exception: restructure
7
8
  stability: preview
8
9
  ported_from: [bandit/B411, ruff/S411]
9
10
  security: { cwe: [776], owasp: ["A05:2025"], severity: medium }
@@ -4,6 +4,7 @@ title: 'XPath expressions are not built from strings'
4
4
  languages: [python]
5
5
  fix: manual
6
6
  fix_reason: "the expression has to be rewritten around `$name` variables, and which parts are values is the author's call"
7
+ fix_exception: security
7
8
  stability: preview
8
9
  security: { cwe: [643], owasp: ["A05:2025"], severity: high }
9
10
  since: 0.2.0
@@ -0,0 +1,51 @@
1
+ ---
2
+ id: sec-py/paramiko-call
3
+ title: "`paramiko.exec_command`"
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "what makes the command safe is the program's, not an edit"
7
+ fix_exception: security
8
+ stability: preview
9
+ ported_from: [ruff/S601]
10
+ security: { cwe: [78], owasp: ["A05:2025"], severity: medium }
11
+ since: 0.5.0
12
+ ---
13
+
14
+ A call that resolves to `paramiko.exec_command` is reported.
15
+
16
+ **Why:** A command line sent to a remote shell is parsed there: a quote or `;` in any part of it runs another command (CWE-78).
17
+
18
+ ```codelaw
19
+ native: sec-py/paramiko-call
20
+ ```
21
+
22
+ ## How to fix
23
+
24
+ 1. Build the command from constants and `shlex.quote()` every variable part.
25
+ 2. Or send the arguments over a channel that does not go through a shell.
26
+
27
+
28
+ ## Examples
29
+
30
+ ### Python
31
+
32
+ #### Don't
33
+
34
+ ```python
35
+ import paramiko
36
+
37
+ paramiko.exec_command("ls " + path)
38
+ ```
39
+
40
+ #### Do
41
+
42
+ ```python
43
+ import paramiko
44
+
45
+ client = paramiko.SSHClient()
46
+ client.exec_command("uptime")
47
+ ```
48
+
49
+ ## Differences from the source
50
+
51
+ This is Ruff's check as written: a call whose name resolves to `paramiko.exec_command`. The usual `client.exec_command(…)` on an `SSHClient` instance does not resolve to that name and is not reported; Ruff does not report it either.
@@ -2,8 +2,8 @@
2
2
  id: sec-py/paramiko-host-key
3
3
  title: 'SSH host keys are checked'
4
4
  languages: [python]
5
- fix: manual
6
- fix_reason: "where the known host keys come from is deployment-specific"
5
+ fix: suggest
6
+ fix_reason: "`RejectPolicy` refuses an unknown host, which breaks a connection that relied on accepting it; where the known keys come from (`load_system_host_keys`, a known_hosts file) is deployment-specific"
7
7
  stability: preview
8
8
  ported_from: [bandit/B507, ruff/S507]
9
9
  security: { cwe: [295], owasp: ["A07:2025"], severity: high }
@@ -18,14 +18,10 @@ since: 0.2.0
18
18
  match: { any: [{ pattern: '$C.set_missing_host_key_policy($P)' }] }
19
19
  constraints: { P: { regex: '(AutoAddPolicy|WarningPolicy)' } }
20
20
  message: 'This accepts any SSH host key; load known hosts and reject unknown ones'
21
+ fix:
22
+ - replace: { target: $P, with: "paramiko.RejectPolicy()" }
21
23
  ```
22
24
 
23
- ## How to fix
24
-
25
- 1. Call `client.load_system_host_keys()` or `load_host_keys(path)`.
26
- 2. Use `client.set_missing_host_key_policy(paramiko.RejectPolicy())`.
27
- 3. Put the server's key in `known_hosts` ahead of time.
28
-
29
25
  ## Examples
30
26
 
31
27
  ### Python
@@ -36,6 +32,12 @@ message: 'This accepts any SSH host key; load known hosts and reject unknown one
36
32
  client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
37
33
  ```
38
34
 
35
+ #### Fixed
36
+
37
+ ```py
38
+ client.set_missing_host_key_policy(paramiko.RejectPolicy())
39
+ ```
40
+
39
41
  #### Do
40
42
 
41
43
  ```py
@@ -4,6 +4,7 @@ title: 'Passwords are hashed with a slow function'
4
4
  languages: [python]
5
5
  fix: manual
6
6
  fix_reason: "moving to Argon2 or bcrypt means rehashing at the next login and changing the stored format"
7
+ fix_exception: restructure
7
8
  stability: preview
8
9
  security: { cwe: [916], owasp: ["A04:2025"], severity: high }
9
10
  since: 0.2.0
@@ -0,0 +1,25 @@
1
+ # sec-py/start-process-with-no-shell — cases
2
+
3
+ ### case: imported by name
4
+
5
+ ```python
6
+ from os import execv
7
+
8
+ execv("/bin/ls", ["ls"])
9
+ ```
10
+
11
+ ```expect
12
+ 3:1 sec-py/start-process-with-no-shell
13
+ ```
14
+
15
+ ### case: os.system is another rule's
16
+
17
+ ```python
18
+ import os
19
+
20
+ os.system("ls")
21
+ ```
22
+
23
+ ```expect
24
+ (none)
25
+ ```
@@ -0,0 +1,50 @@
1
+ ---
2
+ id: sec-py/start-process-with-no-shell
3
+ title: "A process is started with `os.exec*`, `os.spawn*` or `os.startfile`"
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "starting the process is the point; whether its arguments are safe is the author's"
7
+ fix_exception: security
8
+ stability: preview
9
+ ported_from: [ruff/S606]
10
+ security: { cwe: [78], owasp: ["A05:2025"], severity: low }
11
+ since: 0.5.0
12
+ ---
13
+
14
+ A call that resolves to `os.execl`, `execle`, `execlp`, `execlpe`, `execv`, `execve`, `execvp`, `execvpe`, `spawnl`, `spawnle`, `spawnlp`, `spawnlpe`, `spawnv`, `spawnve`, `spawnvp`, `spawnvpe` or `startfile` is reported. These start a process without a shell, which is a smaller risk than `os.system` (`sec-py/no-shell`).
15
+
16
+ **Why:** A process started with a value the program did not write runs whatever that value names. The `os` functions have no shell to inject into, so the risk is the program or the arguments an attacker can choose (CWE-78).
17
+
18
+ ```codelaw
19
+ native: sec-py/start-process-with-no-shell
20
+ ```
21
+
22
+ ## How to fix
23
+
24
+ 1. Pass a fixed program and validate every argument.
25
+ 2. Use `subprocess.run([...])` with a list and `check=True`, which reports a failed start.
26
+
27
+
28
+ ## Examples
29
+
30
+ ### Python
31
+
32
+ #### Don't
33
+
34
+ ```python
35
+ import os
36
+
37
+ os.execv(program, [program, name])
38
+ ```
39
+
40
+ #### Do
41
+
42
+ ```python
43
+ import subprocess
44
+
45
+ subprocess.run(["/usr/bin/tool", name], check=True)
46
+ ```
47
+
48
+ ## Differences from the source
49
+
50
+ Names are resolved through the file's imports (`from os import execv` counts); the rule reports the call, where Ruff reports the same range.
@@ -0,0 +1,50 @@
1
+ ---
2
+ id: sec-py/suspicious-httpoxy-import
3
+ title: "`CGIHandler` or `CGIScript` is imported"
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "the replacement is a different server model, not an edit"
7
+ fix_exception: security
8
+ stability: preview
9
+ ported_from: [ruff/S412]
10
+ security: { cwe: [20], owasp: ["A05:2025"], severity: medium }
11
+ since: 0.5.0
12
+ ---
13
+
14
+ `from wsgiref.handlers import CGIHandler` and `from twisted.web.twcgi import CGIScript` are reported at the module name.
15
+
16
+ **Why:** CGI passes the `Proxy` request header to the program as the `HTTP_PROXY` environment variable, which HTTP clients in the same process then obey: a request can route the program's outgoing calls through an attacker's proxy (httpoxy, CVE-2016-5385). Avoiding CGI for web applications avoids the class of attack.
17
+
18
+ ```codelaw
19
+ native: sec-py/suspicious-httpoxy-import
20
+ ```
21
+
22
+ ## How to fix
23
+
24
+ 1. Serve the application through a WSGI or ASGI server (`gunicorn`, `uvicorn`, `waitress`) instead of CGI.
25
+ 2. If CGI cannot be avoided, drop `HTTP_PROXY` from the environment before any outgoing request.
26
+
27
+
28
+ ## Examples
29
+
30
+ ### Python
31
+
32
+ #### Don't
33
+
34
+ ```python
35
+ from wsgiref.handlers import CGIHandler
36
+
37
+ CGIHandler().run(app)
38
+ ```
39
+
40
+ #### Do
41
+
42
+ ```python
43
+ from wsgiref.simple_server import make_server
44
+
45
+ make_server("", 8000, app).serve_forever()
46
+ ```
47
+
48
+ ## Differences from the source
49
+
50
+ Only the two `from … import` forms are reported, as in Ruff; `import wsgiref.handlers` and a later `wsgiref.handlers.CGIHandler` are not. The CWE is codelaw's mapping (20, input validation); Ruff does not give one.