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,55 @@
|
|
|
1
|
+
# sec-py/no-mass-assignment — cases
|
|
2
|
+
|
|
3
|
+
### case: a splat
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
def v(request):
|
|
7
|
+
return Post.objects.create(**request.POST)
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```expect
|
|
11
|
+
2:12 sec-py/no-mass-assignment
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### case: a constructor
|
|
15
|
+
|
|
16
|
+
```py
|
|
17
|
+
def v(request):
|
|
18
|
+
return Post(**request.data)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```expect
|
|
22
|
+
2:12 sec-py/no-mass-assignment
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### case: __all__
|
|
26
|
+
|
|
27
|
+
```py
|
|
28
|
+
class Meta:
|
|
29
|
+
fields = "__all__"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```expect
|
|
33
|
+
2:5 sec-py/no-mass-assignment
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### case: a list
|
|
37
|
+
|
|
38
|
+
```py
|
|
39
|
+
class Meta:
|
|
40
|
+
fields = ["title"]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```expect
|
|
44
|
+
(none)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### case: a plain splat
|
|
48
|
+
|
|
49
|
+
```py
|
|
50
|
+
Post.objects.create(**defaults)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```expect
|
|
54
|
+
(none)
|
|
55
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-mass-assignment
|
|
3
|
+
title: 'A request is not splatted into a model'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "which fields a client may set is the author's decision"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [915], owasp: ["A01:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
`Model(**request.POST)`, `Model.objects.create(**request.data)` and `update(**request.json)` are not used, and a `ModelForm` or serializer does not set `fields = "__all__"`.
|
|
13
|
+
|
|
14
|
+
**Why:** The client chooses which columns are set, so it can set `is_staff`, `owner_id` or a price (CWE-915).
|
|
15
|
+
|
|
16
|
+
```codelaw
|
|
17
|
+
match:
|
|
18
|
+
any:
|
|
19
|
+
- all:
|
|
20
|
+
- { kind: call }
|
|
21
|
+
- has:
|
|
22
|
+
field: function
|
|
23
|
+
regex: '\.(create|update|get_or_create|update_or_create|bulk_create)$|^[A-Z][A-Za-z0-9_]*$'
|
|
24
|
+
- has:
|
|
25
|
+
field: arguments
|
|
26
|
+
has:
|
|
27
|
+
kind: dictionary_splat
|
|
28
|
+
regex: '^\*\*\s*(self\.)?request\.(POST|GET|data|json|form|args|values|query_params)\b'
|
|
29
|
+
- all:
|
|
30
|
+
- { kind: assignment }
|
|
31
|
+
- { has: { field: left, regex: '^fields$' } }
|
|
32
|
+
- { has: { field: right, regex: '^[''"]__all__[''"]$' } }
|
|
33
|
+
message: 'The client chooses which fields are set; list the fields'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## How to fix
|
|
37
|
+
|
|
38
|
+
1. List the allowed fields: `fields = ["title", "body"]`.
|
|
39
|
+
2. Build the model from named values: `Post.objects.create(title=data["title"], owner=request.user)`.
|
|
40
|
+
3. Mark server-only fields `read_only_fields`.
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
### Python
|
|
45
|
+
|
|
46
|
+
#### Don't
|
|
47
|
+
|
|
48
|
+
```py
|
|
49
|
+
def create(request):
|
|
50
|
+
return Post.objects.create(**request.POST)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### Do
|
|
54
|
+
|
|
55
|
+
```py
|
|
56
|
+
def create(request):
|
|
57
|
+
return Post.objects.create(title=request.POST["title"], owner=request.user)
|
|
58
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# sec-py/no-nosql-injection — cases
|
|
2
|
+
|
|
3
|
+
### case: a body used whole
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
def f(request):
|
|
7
|
+
return db.users.find(request.json)
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```expect
|
|
11
|
+
2:12 sec-py/no-nosql-injection
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### case: a built filter
|
|
15
|
+
|
|
16
|
+
```py
|
|
17
|
+
def f(request):
|
|
18
|
+
return db.users.find({"name": str(request.json["name"])})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```expect
|
|
22
|
+
(none)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### case: $where
|
|
26
|
+
|
|
27
|
+
```py
|
|
28
|
+
def f(request):
|
|
29
|
+
q = {"$where": f"this.a == {request.args['a']}"}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```expect
|
|
33
|
+
2:10 sec-py/no-nosql-injection
|
|
34
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-nosql-injection
|
|
3
|
+
title: 'A request is not a MongoDB query'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "which fields the client may filter on, and their types, are the author's decision"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [943], owasp: ["A05:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
A value from the request is not used whole as a `pymongo` filter, and a `$where` expression is not built from a request value.
|
|
13
|
+
|
|
14
|
+
**Why:** A JSON body such as `{"$ne": null}` or `{"$gt": ""}` in a query field matches every document, and `$where` runs JavaScript (CWE-943). 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
|
+
- all:
|
|
20
|
+
- { kind: call }
|
|
21
|
+
- has:
|
|
22
|
+
field: function
|
|
23
|
+
regex: '\.(find|find_one|find_one_and_update|find_one_and_delete|find_one_and_replace|update_one|update_many|delete_one|delete_many|count_documents|replace_one)$'
|
|
24
|
+
- { has: { field: arguments, has: { nthChild: 1, pattern: $Q } } }
|
|
25
|
+
- all:
|
|
26
|
+
- { kind: pair }
|
|
27
|
+
- { has: { field: key, regex: '^[''"]\$where[''"]$' } }
|
|
28
|
+
- { has: { field: value, pattern: $Q } }
|
|
29
|
+
where: { $Q: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
|
|
30
|
+
message: '`$Q` comes from the request and is used as a MongoDB query'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## How to fix
|
|
34
|
+
|
|
35
|
+
1. Build the filter yourself from named fields: `{"name": str(request.json["name"])}`; `str()` stops an operator object from getting through.
|
|
36
|
+
2. Validate the body against a schema (pydantic, marshmallow) before it reaches the query.
|
|
37
|
+
3. Do not use `$where`; express the condition with query operators.
|
|
38
|
+
|
|
39
|
+
## Examples
|
|
40
|
+
|
|
41
|
+
### Python
|
|
42
|
+
|
|
43
|
+
#### Don't
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
def find(request):
|
|
47
|
+
return db.users.find_one(request.json)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### Do
|
|
51
|
+
|
|
52
|
+
```py
|
|
53
|
+
def find(request):
|
|
54
|
+
name = str(request.json["name"])
|
|
55
|
+
return db.users.find_one({"name": name})
|
|
56
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# sec-py/no-open-redirect — cases
|
|
2
|
+
|
|
3
|
+
### case: a bound request value
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
def v(request):
|
|
7
|
+
n = request.args.get("next")
|
|
8
|
+
return redirect(n)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```expect
|
|
12
|
+
3:12 sec-py/no-open-redirect
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### case: Django
|
|
16
|
+
|
|
17
|
+
```py
|
|
18
|
+
def v(request):
|
|
19
|
+
return HttpResponseRedirect(request.GET["next"])
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```expect
|
|
23
|
+
2:12 sec-py/no-open-redirect
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### case: a route
|
|
27
|
+
|
|
28
|
+
```py
|
|
29
|
+
def v(request):
|
|
30
|
+
return redirect("/home")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```expect
|
|
34
|
+
(none)
|
|
35
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-open-redirect
|
|
3
|
+
title: 'A request value is not a redirect target'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the allowed destinations depend on the application"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [601], owasp: ["A01:2025"], severity: medium }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
A value from the request is not passed to `redirect`, `HttpResponseRedirect` or `RedirectResponse`.
|
|
13
|
+
|
|
14
|
+
**Why:** A link on another site can send your users to a look-alike page that starts from yours (CWE-601). 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
|
+
all:
|
|
19
|
+
- { kind: call }
|
|
20
|
+
- has:
|
|
21
|
+
field: function
|
|
22
|
+
regex: '^(redirect|(.*\.)?(HttpResponseRedirect|HttpResponsePermanentRedirect|RedirectResponse))$'
|
|
23
|
+
- { has: { field: arguments, has: { nthChild: 1, pattern: $U } } }
|
|
24
|
+
where: { $U: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
|
|
25
|
+
message: '`$U` comes from the request and is a redirect target'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## How to fix
|
|
29
|
+
|
|
30
|
+
1. Redirect to a named route (`redirect(url_for("home"))`) and take only an id from the client.
|
|
31
|
+
2. Or check the target: Django `url_has_allowed_host_and_scheme(url, allowed_hosts={request.get_host()})`; elsewhere require a path that starts with a single `/`.
|
|
32
|
+
|
|
33
|
+
## Examples
|
|
34
|
+
|
|
35
|
+
### Python
|
|
36
|
+
|
|
37
|
+
#### Don't
|
|
38
|
+
|
|
39
|
+
```py
|
|
40
|
+
def login(request):
|
|
41
|
+
nxt = request.args.get("next")
|
|
42
|
+
return redirect(nxt)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
#### Do
|
|
46
|
+
|
|
47
|
+
```py
|
|
48
|
+
def login(request):
|
|
49
|
+
target = SAFE_TARGETS.get(request.args.get("next"), "/")
|
|
50
|
+
return redirect(target)
|
|
51
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sec-py/no-partial-path — cases
|
|
2
|
+
|
|
3
|
+
### case: a bare list
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
subprocess.run(["git", "status"])
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-partial-path
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: a bare string
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
subprocess.run("git status", shell=True)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-partial-path
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: a full path
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
subprocess.run(["/usr/bin/git", "status"])
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: a variable
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
subprocess.run([GIT, "status"])
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-partial-path
|
|
3
|
+
title: 'A program is started by its full path'
|
|
4
|
+
languages: [python]
|
|
5
|
+
severity: warn
|
|
6
|
+
fix: manual
|
|
7
|
+
fix_reason: "the absolute path differs by system, so it has to come from configuration or `shutil.which` at start-up"
|
|
8
|
+
stability: preview
|
|
9
|
+
ported_from: [bandit/B607, ruff/S607]
|
|
10
|
+
security: { cwe: [426], owasp: ["A08:2025"], severity: low }
|
|
11
|
+
since: 0.2.0
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
A subprocess is not started with a bare program name such as `"git"`.
|
|
15
|
+
|
|
16
|
+
**Why:** The shell looks the name up in `PATH`, so a program of the same name in an earlier folder runs instead (CWE-426).
|
|
17
|
+
|
|
18
|
+
```codelaw
|
|
19
|
+
match:
|
|
20
|
+
kind: call
|
|
21
|
+
all:
|
|
22
|
+
- has:
|
|
23
|
+
field: function
|
|
24
|
+
regex: '^(os\.system|os\.popen|subprocess\.(run|call|check_call|check_output|Popen))$'
|
|
25
|
+
- any:
|
|
26
|
+
- has:
|
|
27
|
+
field: arguments
|
|
28
|
+
has: { nthChild: 1, kind: string, regex: '^[rb]?[''\"][^/''\"\s]+(\s[^''\"]*)?[''\"]$' }
|
|
29
|
+
- has:
|
|
30
|
+
field: arguments
|
|
31
|
+
has:
|
|
32
|
+
nthChild: 1
|
|
33
|
+
kind: list
|
|
34
|
+
has: { nthChild: 1, kind: string, regex: '^[rb]?[''\"][^/''\"\s]+(\s[^''\"]*)?[''\"]$' }
|
|
35
|
+
message: 'This starts a program found through `PATH`; use its full path'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## How to fix
|
|
39
|
+
|
|
40
|
+
1. Resolve it once at start-up: `GIT = shutil.which("git")`, and fail if it is `None`.
|
|
41
|
+
2. Or put the absolute path in configuration.
|
|
42
|
+
|
|
43
|
+
## Examples
|
|
44
|
+
|
|
45
|
+
### Python
|
|
46
|
+
|
|
47
|
+
#### Don't
|
|
48
|
+
|
|
49
|
+
```py
|
|
50
|
+
import subprocess
|
|
51
|
+
|
|
52
|
+
subprocess.run(["git", "status"])
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Do
|
|
56
|
+
|
|
57
|
+
```py
|
|
58
|
+
import subprocess
|
|
59
|
+
|
|
60
|
+
subprocess.run(["/usr/bin/git", "status"])
|
|
61
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# sec-py/no-path-traversal — cases
|
|
2
|
+
|
|
3
|
+
### case: a request value
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
def v(request):
|
|
7
|
+
return send_file(request.args["f"])
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```expect
|
|
11
|
+
2:12 sec-py/no-path-traversal
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### case: a bound name
|
|
15
|
+
|
|
16
|
+
```py
|
|
17
|
+
def v(request):
|
|
18
|
+
f = request.GET.get("f")
|
|
19
|
+
return open(f)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```expect
|
|
23
|
+
3:12 sec-py/no-path-traversal
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### case: a constant
|
|
27
|
+
|
|
28
|
+
```py
|
|
29
|
+
def v(request):
|
|
30
|
+
return send_file("report.pdf")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```expect
|
|
34
|
+
(none)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### case: a parameter is not followed
|
|
38
|
+
|
|
39
|
+
```py
|
|
40
|
+
def v(name):
|
|
41
|
+
return open(name)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```expect
|
|
45
|
+
(none)
|
|
46
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-path-traversal
|
|
3
|
+
title: 'A request value is not a file path'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the safe form (a lookup by id, a resolved path checked against a base folder) depends on the storage layout"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [22], owasp: ["A01:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
A value from the request is not passed to `open`, `send_file`, `send_from_directory` or `FileResponse` as a path.
|
|
13
|
+
|
|
14
|
+
**Why:** `../../etc/passwd` in the value reads any file the process can (CWE-22). 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
|
+
all:
|
|
19
|
+
- { kind: call }
|
|
20
|
+
- has:
|
|
21
|
+
field: function
|
|
22
|
+
regex: '^(open|(.*\.)?(send_file|send_from_directory|FileResponse|FileSystemStorage|read_text|read_bytes)|Path|pathlib\.Path|os\.path\.join)$'
|
|
23
|
+
- { has: { field: arguments, has: { nthChild: 1, pattern: $P } } }
|
|
24
|
+
where: { $P: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
|
|
25
|
+
message: '`$P` comes from the request and is used as a path'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## How to fix
|
|
29
|
+
|
|
30
|
+
1. Look the file up by an id the program issued, not by a name the client chose.
|
|
31
|
+
2. Or resolve the path and check it stays inside the base folder: `p = (BASE / name).resolve(); p.relative_to(BASE.resolve())`.
|
|
32
|
+
3. Flask: use `send_from_directory(BASE, secure_filename(name))`.
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
### Python
|
|
37
|
+
|
|
38
|
+
#### Don't
|
|
39
|
+
|
|
40
|
+
```py
|
|
41
|
+
def download(request):
|
|
42
|
+
name = request.GET["file"]
|
|
43
|
+
return open(name, "rb")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Do
|
|
47
|
+
|
|
48
|
+
```py
|
|
49
|
+
def download(request):
|
|
50
|
+
name = request.GET["file"]
|
|
51
|
+
path = (BASE / name).resolve()
|
|
52
|
+
path.relative_to(BASE.resolve())
|
|
53
|
+
return open(path, "rb")
|
|
54
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# sec-py/no-prompt-injection — cases
|
|
2
|
+
|
|
3
|
+
### case: an anthropic system argument
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
def v(request):
|
|
7
|
+
t = request.GET["t"]
|
|
8
|
+
return client.messages.create(system=f"About {t}")
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```expect
|
|
12
|
+
3:35 sec-py/no-prompt-injection
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### case: an openai system message
|
|
16
|
+
|
|
17
|
+
```py
|
|
18
|
+
def v(request):
|
|
19
|
+
m = {"role": "system", "content": request.GET["p"]}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```expect
|
|
23
|
+
2:9 sec-py/no-prompt-injection
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### case: a constant
|
|
27
|
+
|
|
28
|
+
```py
|
|
29
|
+
def v(request):
|
|
30
|
+
m = {"role": "system", "content": PROMPT}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```expect
|
|
34
|
+
(none)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### case: a user message
|
|
38
|
+
|
|
39
|
+
```py
|
|
40
|
+
def v(request):
|
|
41
|
+
m = {"role": "user", "content": request.GET["p"]}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```expect
|
|
45
|
+
(none)
|
|
46
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-prompt-injection
|
|
3
|
+
title: 'A request value is not written into a system prompt'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "what goes in the system role and what goes in the user role is a design decision"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [1427], owasp: ["A05:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
A value from the request is not part of the system prompt of a language-model call.
|
|
13
|
+
|
|
14
|
+
**Why:** The model cannot tell your instructions from the user's text, so a user can rewrite the rules the system prompt sets (CWE-1427). 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
|
+
- all:
|
|
20
|
+
- { kind: dictionary }
|
|
21
|
+
- has:
|
|
22
|
+
kind: pair
|
|
23
|
+
all:
|
|
24
|
+
- { has: { field: key, regex: '^[''"]role[''"]$' } }
|
|
25
|
+
- { has: { field: value, regex: '^[''"]system[''"]$' } }
|
|
26
|
+
- has:
|
|
27
|
+
kind: pair
|
|
28
|
+
all:
|
|
29
|
+
- { has: { field: key, regex: '^[''"]content[''"]$' } }
|
|
30
|
+
- { has: { field: value, pattern: $V } }
|
|
31
|
+
- { pattern: 'SystemMessage($V)' }
|
|
32
|
+
- { pattern: 'SystemMessage(content=$V)' }
|
|
33
|
+
- all:
|
|
34
|
+
- { kind: keyword_argument }
|
|
35
|
+
- { has: { field: name, regex: '^(system|system_prompt|instructions)$' } }
|
|
36
|
+
- { has: { field: value, pattern: $V } }
|
|
37
|
+
where: { $V: { from_source: { tables: [sources/django, sources/flask, sources/fastapi] } } }
|
|
38
|
+
message: '`$V` comes from the request and is part of the system prompt'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## How to fix
|
|
42
|
+
|
|
43
|
+
1. Keep the system prompt a constant.
|
|
44
|
+
2. Send the user's text as a separate `user` message, never concatenated into the system one.
|
|
45
|
+
3. Treat the model's answer as untrusted too (see `sec-py/no-llm-output-to-sink`).
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
### Python
|
|
50
|
+
|
|
51
|
+
#### Don't
|
|
52
|
+
|
|
53
|
+
```py
|
|
54
|
+
def ask(request):
|
|
55
|
+
topic = request.GET["topic"]
|
|
56
|
+
return client.messages.create(system=f"You help with {topic}.", messages=msgs)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### Do
|
|
60
|
+
|
|
61
|
+
```py
|
|
62
|
+
def ask(request):
|
|
63
|
+
msgs = [{"role": "user", "content": request.GET["topic"]}]
|
|
64
|
+
return client.messages.create(system=SYSTEM_PROMPT, messages=msgs)
|
|
65
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sec-py/no-pycrypto — cases
|
|
2
|
+
|
|
3
|
+
### case: pycrypto
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
from Crypto.Cipher import AES
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/no-pycrypto
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: import
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
import Crypto
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/no-pycrypto
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: Cryptodome
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
from Cryptodome.Cipher import AES
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: cryptography
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
from cryptography.fernet import Fernet
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/no-pycrypto
|
|
3
|
+
title: 'No `pycrypto`'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the replacement package changes the import path and some call signatures"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B413, ruff/S413]
|
|
9
|
+
security: { cwe: [1104], owasp: ["A03:2025"], severity: medium }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`Crypto` is not imported from the unmaintained `pycrypto` package.
|
|
14
|
+
|
|
15
|
+
**Why:** `pycrypto` has had no release since 2013 and carries known flaws, including a weak random generator and a buffer overflow (CWE-1104).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- all: [{ kind: import_statement }, { has: { field: name, regex: '^Crypto(\..*)?$' } }]
|
|
21
|
+
- all:
|
|
22
|
+
- { kind: import_from_statement }
|
|
23
|
+
- { has: { field: module_name, regex: '^Crypto(\..*)?$' } }
|
|
24
|
+
message: '`Crypto` is the unmaintained `pycrypto`; use `cryptography` or `pycryptodomex`'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## How to fix
|
|
28
|
+
|
|
29
|
+
1. Prefer `cryptography` (`pip install cryptography`).
|
|
30
|
+
2. `pycryptodomex` is a maintained drop-in with the package name `Cryptodome`: change `Crypto` to `Cryptodome`.
|
|
31
|
+
3. `pycryptodome` (not `x`) also installs a package named `Crypto`; if that is what you use, switch to `Cryptodome` so this rule can tell them apart.
|
|
32
|
+
|
|
33
|
+
## Examples
|
|
34
|
+
|
|
35
|
+
### Python
|
|
36
|
+
|
|
37
|
+
#### Don't
|
|
38
|
+
|
|
39
|
+
```py
|
|
40
|
+
from Crypto.Cipher import AES
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### Do
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
from Cryptodome.Cipher import AES
|
|
47
|
+
```
|