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,31 @@
|
|
|
1
|
+
# sec-py/logging-listen — cases
|
|
2
|
+
|
|
3
|
+
### case: listen
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
logging.config.listen(9999)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/logging-listen
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: dictConfig
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
logging.config.dictConfig(c)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
(none)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: another listen
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
server.listen(5)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/logging-listen
|
|
3
|
+
title: "`logging.config.listen` is not used"
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "reconfiguring a running process needs another route (a signal handler, a config reload) the author has to choose"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B612, ruff/S612]
|
|
9
|
+
security: { cwe: [94], owasp: ["A08:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`logging.config.listen()` is not called.
|
|
14
|
+
|
|
15
|
+
**Why:** It opens a socket that accepts a logging configuration, which can name any callable to run, so anyone who can connect runs code (CWE-94).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
kind: call
|
|
20
|
+
all: [{ has: { field: function, regex: '^(logging\.)?config\.listen$' } }]
|
|
21
|
+
message: 'This opens a socket that accepts logging configuration, which can run code'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## How to fix
|
|
25
|
+
|
|
26
|
+
1. Remove the call; load the configuration from a file at start-up with `logging.config.dictConfig`.
|
|
27
|
+
2. To change levels at run time use a signal handler that re-reads the file.
|
|
28
|
+
|
|
29
|
+
## Examples
|
|
30
|
+
|
|
31
|
+
### Python
|
|
32
|
+
|
|
33
|
+
#### Don't
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
import logging.config
|
|
37
|
+
|
|
38
|
+
t = logging.config.listen(9999)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
#### Do
|
|
42
|
+
|
|
43
|
+
```py
|
|
44
|
+
import logging.config
|
|
45
|
+
|
|
46
|
+
logging.config.dictConfig(CONFIG)
|
|
47
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# sec-py/ml-unsafe-load — cases
|
|
2
|
+
|
|
3
|
+
### case: allow_pickle
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
np.load(path, allow_pickle=True)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/ml-unsafe-load
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: no pickle
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
np.load(path)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
(none)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: joblib
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
joblib.load(path)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:1 sec-py/ml-unsafe-load
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: pandas
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
pd.read_pickle(path)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
1:1 sec-py/ml-unsafe-load
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### case: keras
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
keras.models.load_model(p, safe_mode=False)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
1:1 sec-py/ml-unsafe-load
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### case: keras safe
|
|
54
|
+
|
|
55
|
+
```py
|
|
56
|
+
keras.models.load_model(p)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```expect
|
|
60
|
+
(none)
|
|
61
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/ml-unsafe-load
|
|
3
|
+
title: 'Model and array files are not unpickled'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the replacement format (`safetensors`, `.npy` without pickle, Parquet) means converting the stored files"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [502], owasp: ["A08:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
`numpy.load(allow_pickle=True)`, `joblib.load`, `pandas.read_pickle` and Keras `load_model(safe_mode=False)` are not used.
|
|
13
|
+
|
|
14
|
+
**Why:** Each one unpickles the file, so a model or dataset from anywhere else runs code on load (CWE-502).
|
|
15
|
+
|
|
16
|
+
```codelaw
|
|
17
|
+
match:
|
|
18
|
+
any:
|
|
19
|
+
- kind: call
|
|
20
|
+
all:
|
|
21
|
+
- { has: { field: function, regex: '^(np|numpy)\.load$' } }
|
|
22
|
+
- has:
|
|
23
|
+
field: arguments
|
|
24
|
+
has:
|
|
25
|
+
kind: keyword_argument
|
|
26
|
+
all:
|
|
27
|
+
- { has: { field: name, regex: '^allow_pickle$' } }
|
|
28
|
+
- { has: { field: value, kind: 'true' } }
|
|
29
|
+
- kind: call
|
|
30
|
+
all: [{ has: { field: function, regex: '^joblib\.load$' } }]
|
|
31
|
+
- kind: call
|
|
32
|
+
all: [{ has: { field: function, regex: '^(pd|pandas)\.read_pickle$' } }]
|
|
33
|
+
- kind: call
|
|
34
|
+
all:
|
|
35
|
+
- { has: { field: function, regex: '(^|\.)load_model$' } }
|
|
36
|
+
- has:
|
|
37
|
+
field: arguments
|
|
38
|
+
has:
|
|
39
|
+
kind: keyword_argument
|
|
40
|
+
all: [{ has: { field: name, regex: '^safe_mode$' } }, { has: { field: value, kind: 'false' } }]
|
|
41
|
+
message: 'This loads a pickle, which can run code; use `safetensors` or a data-only format'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## How to fix
|
|
45
|
+
|
|
46
|
+
1. Save and load arrays with `np.save` / `np.load` without `allow_pickle`, and tensors with `safetensors`.
|
|
47
|
+
2. Store tables as Parquet or CSV, not pickle.
|
|
48
|
+
3. If the file must be a pickle, load only files your own pipeline wrote and check a signature first.
|
|
49
|
+
|
|
50
|
+
## Examples
|
|
51
|
+
|
|
52
|
+
### Python
|
|
53
|
+
|
|
54
|
+
#### Don't
|
|
55
|
+
|
|
56
|
+
```py
|
|
57
|
+
import numpy as np
|
|
58
|
+
|
|
59
|
+
arr = np.load(path, allow_pickle=True)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Do
|
|
63
|
+
|
|
64
|
+
```py
|
|
65
|
+
import numpy as np
|
|
66
|
+
|
|
67
|
+
arr = np.load(path)
|
|
68
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# sec-py/no-assert-for-auth — cases
|
|
2
|
+
|
|
3
|
+
### case: a permission
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
assert user.has_perm("x")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-assert-for-auth
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: an invariant
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
assert len(items) > 0
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
(none)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: current_user
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
assert current_user.id == owner_id
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:1 sec-py/no-assert-for-auth
|
|
31
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-assert-for-auth
|
|
3
|
+
title: "`assert` is not an access check"
|
|
4
|
+
languages: [python]
|
|
5
|
+
exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
|
|
6
|
+
fix: manual
|
|
7
|
+
fix_reason: "the replacement raises the right exception for the framework, which only the author knows"
|
|
8
|
+
stability: preview
|
|
9
|
+
ported_from: [bandit/B101, ruff/S101]
|
|
10
|
+
security: { cwe: [703], owasp: ["A10:2025"], severity: medium }
|
|
11
|
+
since: 0.2.0
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
A permission, role or authentication test is not written as an `assert`.
|
|
15
|
+
|
|
16
|
+
**Why:** `python -O` removes every `assert`, so the check disappears in an optimised run (CWE-703).
|
|
17
|
+
|
|
18
|
+
```codelaw
|
|
19
|
+
match:
|
|
20
|
+
all:
|
|
21
|
+
- { kind: assert_statement }
|
|
22
|
+
- regex: '(?i)(is_authenticated|is_admin|is_staff|is_superuser|has_perm|has_permission|permission|authorized|authenticated|is_owner|\.role\b|\.roles\b|current_user|user\.id\s*==)'
|
|
23
|
+
message: 'An `assert` is removed by `python -O`; raise an exception for an access check'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## How to fix
|
|
27
|
+
|
|
28
|
+
1. Replace it with `if not user.is_authenticated: raise PermissionDenied`.
|
|
29
|
+
2. Use your framework's decorator (`@login_required`, `Depends(...)`) where one fits.
|
|
30
|
+
|
|
31
|
+
## Examples
|
|
32
|
+
|
|
33
|
+
### Python
|
|
34
|
+
|
|
35
|
+
#### Don't
|
|
36
|
+
|
|
37
|
+
```py
|
|
38
|
+
def delete(request, item):
|
|
39
|
+
assert request.user.is_staff
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
#### Do
|
|
43
|
+
|
|
44
|
+
```py
|
|
45
|
+
from django.core.exceptions import PermissionDenied
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def delete(request, item):
|
|
49
|
+
if not request.user.is_staff:
|
|
50
|
+
raise PermissionDenied
|
|
51
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sec-py/no-bind-all-interfaces — cases
|
|
2
|
+
|
|
3
|
+
### case: uvicorn
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
uvicorn.run(app, host="0.0.0.0")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-bind-all-interfaces
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: socket
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
s.bind(("0.0.0.0", 80))
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-bind-all-interfaces
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: loopback
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
s.bind(("127.0.0.1", 80))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: from the environment
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
app.run(host=HOST)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-bind-all-interfaces
|
|
3
|
+
title: 'A server does not bind to every interface in code'
|
|
4
|
+
languages: [python]
|
|
5
|
+
severity: warn
|
|
6
|
+
fix: manual
|
|
7
|
+
fix_reason: "the address belongs in configuration, with a loopback default"
|
|
8
|
+
stability: preview
|
|
9
|
+
ported_from: [bandit/B104, ruff/S104]
|
|
10
|
+
security: { cwe: [605], owasp: ["A02:2025"], severity: low }
|
|
11
|
+
since: 0.2.0
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
A socket or server is not bound to `0.0.0.0` in the source.
|
|
15
|
+
|
|
16
|
+
**Why:** The service is then reachable from every network the machine is on, including ones that were not meant to reach it (CWE-605).
|
|
17
|
+
|
|
18
|
+
```codelaw
|
|
19
|
+
match:
|
|
20
|
+
any:
|
|
21
|
+
- all:
|
|
22
|
+
- { kind: call }
|
|
23
|
+
- { has: { field: function, regex: '\.(bind|connect_ex)$' } }
|
|
24
|
+
- has:
|
|
25
|
+
field: arguments
|
|
26
|
+
has:
|
|
27
|
+
nthChild: 1
|
|
28
|
+
kind: tuple
|
|
29
|
+
has: { nthChild: 1, kind: string, regex: '^[''"](0\.0\.0\.0|::)[''"]$' }
|
|
30
|
+
- all:
|
|
31
|
+
- { kind: call }
|
|
32
|
+
- has:
|
|
33
|
+
field: arguments
|
|
34
|
+
has:
|
|
35
|
+
kind: keyword_argument
|
|
36
|
+
all:
|
|
37
|
+
- { has: { field: name, regex: '^host$' } }
|
|
38
|
+
- { has: { field: value, kind: string, regex: '^[''"](0\.0\.0\.0|::)[''"]$' } }
|
|
39
|
+
- all:
|
|
40
|
+
- { kind: call }
|
|
41
|
+
- has:
|
|
42
|
+
field: arguments
|
|
43
|
+
has:
|
|
44
|
+
kind: keyword_argument
|
|
45
|
+
all:
|
|
46
|
+
- { has: { field: name, regex: '^bind$' } }
|
|
47
|
+
- { has: { field: value, kind: string, regex: '^[''"](0\.0\.0\.0|\[::\]):[0-9]+[''"]$' } }
|
|
48
|
+
message: 'This binds to every network interface; read the address from configuration'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## How to fix
|
|
52
|
+
|
|
53
|
+
1. Bind to `127.0.0.1` by default and take the address from an environment variable.
|
|
54
|
+
2. A container that must listen on every interface sets it in its deployment settings, not in the code.
|
|
55
|
+
|
|
56
|
+
## Examples
|
|
57
|
+
|
|
58
|
+
### Python
|
|
59
|
+
|
|
60
|
+
#### Don't
|
|
61
|
+
|
|
62
|
+
```py
|
|
63
|
+
import uvicorn
|
|
64
|
+
|
|
65
|
+
uvicorn.run(app, host="0.0.0.0")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### Do
|
|
69
|
+
|
|
70
|
+
```py
|
|
71
|
+
import os
|
|
72
|
+
import uvicorn
|
|
73
|
+
|
|
74
|
+
uvicorn.run(app, host=os.environ.get("HOST", "127.0.0.1"))
|
|
75
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sec-py/no-cleartext-http — cases
|
|
2
|
+
|
|
3
|
+
### case: plain http
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
requests.get("http://example.com", timeout=5)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-cleartext-http
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: https
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
requests.get("https://example.com")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
(none)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: localhost
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
requests.get("http://localhost:8000/x")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: an f-string
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
httpx.post(f"http://example.com/{p}/x")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
1:1 sec-py/no-cleartext-http
|
|
41
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-cleartext-http
|
|
3
|
+
title: 'Requests go over HTTPS'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the server must serve HTTPS first, and the URL change has to follow it"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [319], owasp: ["A04:2025"], severity: medium }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
`requests` and `httpx` are not called with an `http://` URL, except for localhost.
|
|
13
|
+
|
|
14
|
+
**Why:** The data, and any token in it, crosses the network readable and changeable by anyone on the path (CWE-319).
|
|
15
|
+
|
|
16
|
+
```codelaw
|
|
17
|
+
match:
|
|
18
|
+
kind: call
|
|
19
|
+
all:
|
|
20
|
+
- has:
|
|
21
|
+
field: function
|
|
22
|
+
regex: '^(requests|httpx)\.(get|post|put|patch|delete|head|options|request|stream)$'
|
|
23
|
+
- has:
|
|
24
|
+
field: arguments
|
|
25
|
+
has:
|
|
26
|
+
nthChild: 1
|
|
27
|
+
kind: string
|
|
28
|
+
regex: '^[rbf]?[''\"]http://'
|
|
29
|
+
not: { regex: '^[rbf]?[''\"]http://(localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0)' }
|
|
30
|
+
message: 'This URL is plain HTTP; use `https://`'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## How to fix
|
|
34
|
+
|
|
35
|
+
1. Change the URL to `https://`.
|
|
36
|
+
2. If the server cannot do HTTPS, put it behind a proxy that can, or reach it over a private tunnel.
|
|
37
|
+
|
|
38
|
+
## Examples
|
|
39
|
+
|
|
40
|
+
### Python
|
|
41
|
+
|
|
42
|
+
#### Don't
|
|
43
|
+
|
|
44
|
+
```py
|
|
45
|
+
import requests
|
|
46
|
+
|
|
47
|
+
resp = requests.get("http://api.example.com/data", timeout=5)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### Do
|
|
51
|
+
|
|
52
|
+
```py
|
|
53
|
+
import requests
|
|
54
|
+
|
|
55
|
+
resp = requests.get("https://api.example.com/data", timeout=5)
|
|
56
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# sec-py/no-csrf-exempt — cases
|
|
2
|
+
|
|
3
|
+
### case: exempt
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
@csrf_exempt
|
|
7
|
+
def v(request):
|
|
8
|
+
pass
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```expect
|
|
12
|
+
1:1 sec-py/no-csrf-exempt
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### case: qualified
|
|
16
|
+
|
|
17
|
+
```py
|
|
18
|
+
@csrf.csrf_exempt
|
|
19
|
+
def v(request):
|
|
20
|
+
pass
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```expect
|
|
24
|
+
1:1 sec-py/no-csrf-exempt
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### case: another decorator
|
|
28
|
+
|
|
29
|
+
```py
|
|
30
|
+
@login_required
|
|
31
|
+
def v(request):
|
|
32
|
+
pass
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```expect
|
|
36
|
+
(none)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### case: csrf_protect
|
|
40
|
+
|
|
41
|
+
```py
|
|
42
|
+
@csrf_protect
|
|
43
|
+
def v(request):
|
|
44
|
+
pass
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```expect
|
|
48
|
+
(none)
|
|
49
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-csrf-exempt
|
|
3
|
+
title: 'No `@csrf_exempt`'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "an endpoint that needs to be exempt (a webhook) needs its own authentication, which only the author can choose"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [352], owasp: ["A01:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
A view is not decorated with `@csrf_exempt`.
|
|
13
|
+
|
|
14
|
+
**Why:** Without the token check any page the user visits can submit the form as the user (CWE-352).
|
|
15
|
+
|
|
16
|
+
```codelaw
|
|
17
|
+
match:
|
|
18
|
+
kind: decorator
|
|
19
|
+
regex: '^@\s*((django\.views\.decorators\.csrf\.)|(csrf\.))?csrf_exempt\b'
|
|
20
|
+
message: 'This view skips the CSRF check'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How to fix
|
|
24
|
+
|
|
25
|
+
1. Remove `@csrf_exempt` and send the token (`{% csrf_token %}`, or the `X-CSRFToken` header).
|
|
26
|
+
2. For a webhook, authenticate the sender (a signed header) and keep the exemption on that one view with a comment saying why.
|
|
27
|
+
|
|
28
|
+
## Examples
|
|
29
|
+
|
|
30
|
+
### Python
|
|
31
|
+
|
|
32
|
+
#### Don't
|
|
33
|
+
|
|
34
|
+
```py
|
|
35
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
36
|
+
|
|
37
|
+
@csrf_exempt
|
|
38
|
+
def update(request):
|
|
39
|
+
...
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
#### Do
|
|
43
|
+
|
|
44
|
+
```py
|
|
45
|
+
def update(request):
|
|
46
|
+
...
|
|
47
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# sec-py/no-dynamic-import — cases
|
|
2
|
+
|
|
3
|
+
### case: getattr
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
def v(request):
|
|
7
|
+
n = request.GET["a"]
|
|
8
|
+
return getattr(handlers, n)()
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```expect
|
|
12
|
+
3:12 sec-py/no-dynamic-import
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### case: import_module
|
|
16
|
+
|
|
17
|
+
```py
|
|
18
|
+
def v(request):
|
|
19
|
+
return importlib.import_module(request.args["m"])
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```expect
|
|
23
|
+
2:12 sec-py/no-dynamic-import
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### case: a constant
|
|
27
|
+
|
|
28
|
+
```py
|
|
29
|
+
def v(request):
|
|
30
|
+
return getattr(handlers, "run")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```expect
|
|
34
|
+
(none)
|
|
35
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-dynamic-import
|
|
3
|
+
title: 'A request value does not choose a module or attribute'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the allow-list of names is the author's"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [470], owasp: ["A05:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
A value from the request is not passed to `__import__`, `importlib.import_module` or `getattr`.
|
|
13
|
+
|
|
14
|
+
**Why:** The client then picks which module is loaded or which attribute is read or called, including ones that run commands (CWE-470). 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: '__import__($X)' }
|
|
20
|
+
- { pattern: 'importlib.import_module($X)' }
|
|
21
|
+
- { pattern: 'import_module($X)' }
|
|
22
|
+
- { pattern: 'getattr($O, $X)' }
|
|
23
|
+
- { pattern: 'getattr($O, $X, $D)' }
|
|
24
|
+
- { pattern: 'importlib.import_module($X, $P)' }
|
|
25
|
+
where: { $X: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
|
|
26
|
+
message: '`$X` comes from the request and chooses a module or attribute'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## How to fix
|
|
30
|
+
|
|
31
|
+
1. Map the client's value to a fixed dict of allowed handlers: `HANDLERS = {"csv": export_csv}`; `HANDLERS[name]`.
|
|
32
|
+
2. Reject a name that is not a key.
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
### Python
|
|
37
|
+
|
|
38
|
+
#### Don't
|
|
39
|
+
|
|
40
|
+
```py
|
|
41
|
+
def run(request):
|
|
42
|
+
action = request.GET["action"]
|
|
43
|
+
return getattr(handlers, action)()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Do
|
|
47
|
+
|
|
48
|
+
```py
|
|
49
|
+
def run(request):
|
|
50
|
+
return HANDLERS[request.GET["action"]]()
|
|
51
|
+
```
|