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.
- codelaw_rules_security_python/README.md +12 -0
- codelaw_rules_security_python/codelaw-pack.toml +1 -1
- codelaw_rules_security_python/corpus.toml +5 -0
- codelaw_rules_security_python/rules/sec-py/django-settings.cases.md +14 -0
- codelaw_rules_security_python/rules/sec-py/django-settings.md +7 -9
- codelaw_rules_security_python/rules/sec-py/fastapi-cors.md +1 -0
- codelaw_rules_security_python/rules/sec-py/flask-settings.cases.md +28 -0
- codelaw_rules_security_python/rules/sec-py/flask-settings.md +9 -9
- codelaw_rules_security_python/rules/sec-py/hf-pin-revision.md +1 -0
- codelaw_rules_security_python/rules/sec-py/logging-listen.md +1 -0
- codelaw_rules_security_python/rules/sec-py/ml-unsafe-load.cases.md +14 -0
- codelaw_rules_security_python/rules/sec-py/ml-unsafe-load.md +5 -2
- codelaw_rules_security_python/rules/sec-py/no-assert-for-auth.cases.md +16 -0
- codelaw_rules_security_python/rules/sec-py/no-assert-for-auth.md +21 -2
- codelaw_rules_security_python/rules/sec-py/no-bind-all-interfaces.cases.md +38 -0
- codelaw_rules_security_python/rules/sec-py/no-bind-all-interfaces.md +25 -4
- codelaw_rules_security_python/rules/sec-py/no-cleartext-http.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-csrf-exempt.cases.md +17 -0
- codelaw_rules_security_python/rules/sec-py/no-csrf-exempt.md +13 -2
- codelaw_rules_security_python/rules/sec-py/no-dynamic-import.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-exec-eval.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-format-string-injection.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-hardcoded-crypto-key.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-hardcoded-secret.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-header-injection.cases.md +16 -0
- codelaw_rules_security_python/rules/sec-py/no-header-injection.md +29 -2
- codelaw_rules_security_python/rules/sec-py/no-insecure-protocol.md +2 -1
- codelaw_rules_security_python/rules/sec-py/no-ldap-injection.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-llm-output-to-sink.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-log-injection.cases.md +16 -0
- codelaw_rules_security_python/rules/sec-py/no-log-injection.md +11 -2
- codelaw_rules_security_python/rules/sec-py/no-mako.cases.md +30 -0
- codelaw_rules_security_python/rules/sec-py/no-mako.md +18 -3
- codelaw_rules_security_python/rules/sec-py/no-mark-safe.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-mass-assignment.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-nosql-injection.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-open-redirect.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-partial-path.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-path-traversal.md +2 -1
- codelaw_rules_security_python/rules/sec-py/no-prompt-injection.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-pycrypto.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-reflected-xss.md +2 -1
- codelaw_rules_security_python/rules/sec-py/no-secret-in-log.cases.md +27 -0
- codelaw_rules_security_python/rules/sec-py/no-secret-in-log.md +14 -6
- codelaw_rules_security_python/rules/sec-py/no-silent-except.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-sql-string.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-ssrf.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-stack-trace-response.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-trust-remote-code.cases.md +14 -0
- codelaw_rules_security_python/rules/sec-py/no-trust-remote-code.md +11 -2
- codelaw_rules_security_python/rules/sec-py/no-unsafe-deserialize.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-weak-cipher.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-wildcard-injection.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-xmlrpc.md +1 -0
- codelaw_rules_security_python/rules/sec-py/no-xpath-injection.md +1 -0
- codelaw_rules_security_python/rules/sec-py/paramiko-call.md +51 -0
- codelaw_rules_security_python/rules/sec-py/paramiko-host-key.md +10 -8
- codelaw_rules_security_python/rules/sec-py/password-hash-effort.md +1 -0
- codelaw_rules_security_python/rules/sec-py/start-process-with-no-shell.cases.md +25 -0
- codelaw_rules_security_python/rules/sec-py/start-process-with-no-shell.md +50 -0
- codelaw_rules_security_python/rules/sec-py/suspicious-httpoxy-import.md +50 -0
- codelaw_rules_security_python/rules/sec-py/suspicious-pyghmi-import.md +50 -0
- codelaw_rules_security_python/rules/sec-py/suspicious-subprocess-import.cases.md +24 -0
- codelaw_rules_security_python/rules/sec-py/suspicious-subprocess-import.md +51 -0
- codelaw_rules_security_python/rules/sec-py/urlopen-scheme.md +1 -0
- {codelaw_rules_security_python-0.4.2.dist-info → codelaw_rules_security_python-0.5.1.dist-info}/METADATA +13 -1
- {codelaw_rules_security_python-0.4.2.dist-info → codelaw_rules_security_python-0.5.1.dist-info}/RECORD +69 -62
- {codelaw_rules_security_python-0.4.2.dist-info → codelaw_rules_security_python-0.5.1.dist-info}/WHEEL +0 -0
- {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:
|
|
7
|
-
fix_reason: "
|
|
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:
|
|
6
|
-
fix_reason: "
|
|
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:
|
|
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
|
|
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:
|
|
6
|
-
fix_reason: "
|
|
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:
|
|
6
|
-
fix_reason: "
|
|
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:
|
|
6
|
-
fix_reason: "where the known
|
|
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.
|