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