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,61 @@
|
|
|
1
|
+
# sec-py/django-settings — cases
|
|
2
|
+
|
|
3
|
+
### case: DEBUG
|
|
4
|
+
|
|
5
|
+
```py path=settings.py
|
|
6
|
+
DEBUG = True
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/django-settings
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: wildcard hosts
|
|
14
|
+
|
|
15
|
+
```py path=settings.py
|
|
16
|
+
ALLOWED_HOSTS = ["*"]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/django-settings
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: a literal key
|
|
24
|
+
|
|
25
|
+
```py path=settings.py
|
|
26
|
+
SECRET_KEY = "django-insecure-abc123"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:1 sec-py/django-settings
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: a key from the environment
|
|
34
|
+
|
|
35
|
+
```py path=settings.py
|
|
36
|
+
SECRET_KEY = os.environ["KEY"]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### case: an insecure cookie
|
|
44
|
+
|
|
45
|
+
```py path=settings.py
|
|
46
|
+
SESSION_COOKIE_SECURE = False
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
1:1 sec-py/django-settings
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### case: a secure cookie
|
|
54
|
+
|
|
55
|
+
```py path=settings.py
|
|
56
|
+
CSRF_COOKIE_SECURE = True
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```expect
|
|
60
|
+
(none)
|
|
61
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/django-settings
|
|
3
|
+
title: 'Django settings are safe'
|
|
4
|
+
languages: [python]
|
|
5
|
+
files: ["**/settings.py", "**/settings/**/*.py", "**/*_settings.py", "**/settings_*.py"]
|
|
6
|
+
exclude: ["**/*dev*.py", "**/*local*.py", "**/*test*.py"]
|
|
7
|
+
fix: manual
|
|
8
|
+
fix_reason: "the production value comes from the deployment (environment variables, a secret manager)"
|
|
9
|
+
stability: preview
|
|
10
|
+
ported_from: [ruff/S105]
|
|
11
|
+
security: { cwe: [489, 798, 614], owasp: ["A02:2025"], severity: high }
|
|
12
|
+
since: 0.2.0
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
In a settings module, `DEBUG`, `ALLOWED_HOSTS`, `SECRET_KEY` and the cookie flags are not left in their development values.
|
|
16
|
+
|
|
17
|
+
**Why:** Debug pages show settings and source (CWE-489); `ALLOWED_HOSTS = ["*"]` accepts forged Host headers; a literal `SECRET_KEY` is in the repository (CWE-798); cookies without `Secure` travel over plain HTTP (CWE-614).
|
|
18
|
+
|
|
19
|
+
```codelaw
|
|
20
|
+
match:
|
|
21
|
+
all:
|
|
22
|
+
- { kind: assignment }
|
|
23
|
+
- any:
|
|
24
|
+
- all: [{ has: { field: left, regex: '^DEBUG$' } }, { has: { field: right, kind: 'true' } }]
|
|
25
|
+
- all:
|
|
26
|
+
- { has: { field: left, regex: '^ALLOWED_HOSTS$' } }
|
|
27
|
+
- { has: { field: right, regex: '^\[\s*[''"]\*[''"]\s*\]$' } }
|
|
28
|
+
- all:
|
|
29
|
+
- { has: { field: left, regex: '^SECRET_KEY$' } }
|
|
30
|
+
- has:
|
|
31
|
+
field: right
|
|
32
|
+
kind: string
|
|
33
|
+
regex: '^[rb]?("[^"]{4,}"|''[^'']{4,}'')$'
|
|
34
|
+
not: { has: { kind: interpolation } }
|
|
35
|
+
- all:
|
|
36
|
+
- has:
|
|
37
|
+
field: left
|
|
38
|
+
regex: '^(SESSION_COOKIE_SECURE|CSRF_COOKIE_SECURE|SECURE_SSL_REDIRECT|SESSION_COOKIE_HTTPONLY)$'
|
|
39
|
+
- { has: { field: right, kind: 'false' } }
|
|
40
|
+
message: 'This Django setting is unsafe outside development'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## How to fix
|
|
44
|
+
|
|
45
|
+
1. `DEBUG = os.environ.get("DJANGO_DEBUG") == "1"`.
|
|
46
|
+
2. `ALLOWED_HOSTS = ["app.example.com"]`: list the host names you serve.
|
|
47
|
+
3. `SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]`, and rotate the one that was committed.
|
|
48
|
+
4. Set `SESSION_COOKIE_SECURE`, `CSRF_COOKIE_SECURE` and `SECURE_SSL_REDIRECT` to `True`; run `python manage.py check --deploy`.
|
|
49
|
+
|
|
50
|
+
## Examples
|
|
51
|
+
|
|
52
|
+
### Python
|
|
53
|
+
|
|
54
|
+
#### Don't
|
|
55
|
+
|
|
56
|
+
```py path=settings.py
|
|
57
|
+
DEBUG = True
|
|
58
|
+
ALLOWED_HOSTS = ["*"]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### Do
|
|
62
|
+
|
|
63
|
+
```py path=settings.py
|
|
64
|
+
import os
|
|
65
|
+
|
|
66
|
+
DEBUG = os.environ.get("DJANGO_DEBUG") == "1"
|
|
67
|
+
ALLOWED_HOSTS = ["app.example.com"]
|
|
68
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# sec-py/fastapi-cors — cases
|
|
2
|
+
|
|
3
|
+
### case: wildcard with credentials
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/fastapi-cors
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: wildcard without credentials
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
app.add_middleware(CORSMiddleware, allow_origins=["*"])
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
(none)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: listed origins
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
app.add_middleware(CORSMiddleware, allow_origins=["https://a.example"], allow_credentials=True)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/fastapi-cors
|
|
3
|
+
title: 'CORS does not allow every origin with credentials'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the list of trusted origins is the application's own"
|
|
7
|
+
stability: preview
|
|
8
|
+
security: { cwe: [942], owasp: ["A02:2025"], severity: high }
|
|
9
|
+
since: 0.2.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
`CORSMiddleware` is not given `allow_origins=["*"]` together with `allow_credentials=True`.
|
|
13
|
+
|
|
14
|
+
**Why:** Any site can then make authenticated requests as the user and read the answers (CWE-942).
|
|
15
|
+
|
|
16
|
+
```codelaw
|
|
17
|
+
match:
|
|
18
|
+
any:
|
|
19
|
+
- all:
|
|
20
|
+
- { kind: call }
|
|
21
|
+
- { has: { field: function, regex: '^(CORSMiddleware|.*\.add_middleware)$' } }
|
|
22
|
+
- { has: { field: arguments, regex: 'allow_origins\s*=\s*\[\s*[''"]\*[''"]\s*\]' } }
|
|
23
|
+
- { has: { field: arguments, regex: 'allow_credentials\s*=\s*True' } }
|
|
24
|
+
message: 'Every origin is allowed with credentials; list the trusted origins'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## How to fix
|
|
28
|
+
|
|
29
|
+
1. List the origins: `allow_origins=["https://app.example.com"]`.
|
|
30
|
+
2. If the API needs no cookies, set `allow_credentials=False` and keep `*`.
|
|
31
|
+
|
|
32
|
+
## Examples
|
|
33
|
+
|
|
34
|
+
### Python
|
|
35
|
+
|
|
36
|
+
#### Don't
|
|
37
|
+
|
|
38
|
+
```py
|
|
39
|
+
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
#### Do
|
|
43
|
+
|
|
44
|
+
```py
|
|
45
|
+
app.add_middleware(CORSMiddleware, allow_origins=["https://app.example.com"], allow_credentials=True)
|
|
46
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# sec-py/flask-debug — cases
|
|
2
|
+
|
|
3
|
+
### case: debug=True
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
app.run(host="127.0.0.1", debug=True)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/flask-debug
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: debug=False
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
app.run(debug=False)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
(none)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: no debug
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
app.run()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/flask-debug
|
|
3
|
+
title: 'Flask''s debugger is off'
|
|
4
|
+
languages: [python]
|
|
5
|
+
exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
|
|
6
|
+
fix: suggest
|
|
7
|
+
fix_reason: "the setting belongs to the environment, so the debug flag has to come from configuration"
|
|
8
|
+
stability: preview
|
|
9
|
+
ported_from: [bandit/B201, ruff/S201]
|
|
10
|
+
security: { cwe: [489], owasp: ["A02:2025"], severity: high }
|
|
11
|
+
since: 0.2.0
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
`app.run(debug=True)` is not written in code.
|
|
15
|
+
|
|
16
|
+
**Why:** The debugger serves an interactive console that runs code on the server, and a stack trace shows source and settings (CWE-489).
|
|
17
|
+
|
|
18
|
+
```codelaw
|
|
19
|
+
match:
|
|
20
|
+
kind: call
|
|
21
|
+
all:
|
|
22
|
+
- { has: { field: function, regex: '^(app|application|flask_app|self\.app)\.run$' } }
|
|
23
|
+
- has:
|
|
24
|
+
field: arguments
|
|
25
|
+
has:
|
|
26
|
+
kind: keyword_argument
|
|
27
|
+
all: [{ has: { field: name, regex: '^debug$' } }, { has: { field: value, kind: 'true' } }]
|
|
28
|
+
pattern: $KW
|
|
29
|
+
message: 'Flask runs with the debugger on; take `debug` from the environment'
|
|
30
|
+
fix:
|
|
31
|
+
- { remove: $KW }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## How to fix
|
|
35
|
+
|
|
36
|
+
1. Remove `debug=True`; turn the debugger on with `flask --debug run` while developing.
|
|
37
|
+
2. In production run a WSGI server (`gunicorn app:app`), not `app.run`.
|
|
38
|
+
|
|
39
|
+
## Examples
|
|
40
|
+
|
|
41
|
+
### Python
|
|
42
|
+
|
|
43
|
+
#### Don't
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
from flask import Flask
|
|
47
|
+
|
|
48
|
+
app = Flask(__name__)
|
|
49
|
+
app.run(debug=True)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### Do
|
|
53
|
+
|
|
54
|
+
```py
|
|
55
|
+
from flask import Flask
|
|
56
|
+
|
|
57
|
+
app = Flask(__name__)
|
|
58
|
+
app.run()
|
|
59
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# sec-py/flask-settings — cases
|
|
2
|
+
|
|
3
|
+
### case: a literal key
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
app.config["SECRET_KEY"] = "dev-secret-key"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/flask-settings
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: csrf off
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
app.config["WTF_CSRF_ENABLED"] = False
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:1 sec-py/flask-settings
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: update
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
app.config.update(SECRET_KEY="abcdef")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:1 sec-py/flask-settings
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: an environment key
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
app.config["SECRET_KEY"] = os.environ["K"]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
(none)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### case: a secure cookie
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
app.config["SESSION_COOKIE_SECURE"] = True
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
(none)
|
|
51
|
+
```
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/flask-settings
|
|
3
|
+
title: 'Flask configuration is safe'
|
|
4
|
+
languages: [python]
|
|
5
|
+
exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
|
|
6
|
+
fix: manual
|
|
7
|
+
fix_reason: "the production values come from the deployment (environment variables, a secret manager)"
|
|
8
|
+
stability: preview
|
|
9
|
+
security: { cwe: [798, 352, 614], owasp: ["A02:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`SECRET_KEY` is not a literal, and `WTF_CSRF_ENABLED` and `SESSION_COOKIE_SECURE` are not switched off in the app configuration.
|
|
14
|
+
|
|
15
|
+
**Why:** A literal key is in the repository (CWE-798), turning off CSRF protection lets other sites submit forms as the user (CWE-352), and a cookie without `Secure` travels over plain HTTP (CWE-614).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- all:
|
|
21
|
+
- { kind: assignment }
|
|
22
|
+
- { has: { field: left, regex: 'config\[\s*[''"]SECRET_KEY[''"]\s*\]$' } }
|
|
23
|
+
- has:
|
|
24
|
+
field: right
|
|
25
|
+
kind: string
|
|
26
|
+
regex: '^[rb]?("[^"]{4,}"|''[^'']{4,}'')$'
|
|
27
|
+
not: { has: { kind: interpolation } }
|
|
28
|
+
- all:
|
|
29
|
+
- { kind: assignment }
|
|
30
|
+
- has:
|
|
31
|
+
field: left
|
|
32
|
+
regex: 'config\[\s*[''"](WTF_CSRF_ENABLED|WTF_CSRF_CHECK_DEFAULT|SESSION_COOKIE_SECURE|SESSION_COOKIE_HTTPONLY)[''"]\s*\]$'
|
|
33
|
+
- { has: { field: right, kind: 'false' } }
|
|
34
|
+
- kind: call
|
|
35
|
+
all:
|
|
36
|
+
- { has: { field: function, regex: '\.config\.(update|from_mapping)$' } }
|
|
37
|
+
- any:
|
|
38
|
+
- has:
|
|
39
|
+
field: arguments
|
|
40
|
+
has:
|
|
41
|
+
kind: keyword_argument
|
|
42
|
+
all:
|
|
43
|
+
- { has: { field: name, regex: '^SECRET_KEY$' } }
|
|
44
|
+
- { has: { field: value, kind: string, not: { has: { kind: interpolation } } } }
|
|
45
|
+
- has:
|
|
46
|
+
field: arguments
|
|
47
|
+
has:
|
|
48
|
+
kind: keyword_argument
|
|
49
|
+
all:
|
|
50
|
+
- { has: { field: name, regex: '^WTF_CSRF_ENABLED$' } }
|
|
51
|
+
- { has: { field: value, kind: 'false' } }
|
|
52
|
+
- has:
|
|
53
|
+
field: arguments
|
|
54
|
+
has:
|
|
55
|
+
kind: keyword_argument
|
|
56
|
+
all:
|
|
57
|
+
- { has: { field: name, regex: '^SESSION_COOKIE_SECURE$' } }
|
|
58
|
+
- { has: { field: value, kind: 'false' } }
|
|
59
|
+
message: 'This Flask setting is unsafe outside development'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## How to fix
|
|
63
|
+
|
|
64
|
+
1. `app.config["SECRET_KEY"] = os.environ["FLASK_SECRET_KEY"]`, and rotate the key that was committed.
|
|
65
|
+
2. Leave `WTF_CSRF_ENABLED` on; send the token with `{{ csrf_token() }}`.
|
|
66
|
+
3. Set `SESSION_COOKIE_SECURE`, `SESSION_COOKIE_HTTPONLY` and `SESSION_COOKIE_SAMESITE` to safe values.
|
|
67
|
+
|
|
68
|
+
## Examples
|
|
69
|
+
|
|
70
|
+
### Python
|
|
71
|
+
|
|
72
|
+
#### Don't
|
|
73
|
+
|
|
74
|
+
```py
|
|
75
|
+
app.config["SECRET_KEY"] = "dev-secret-key"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Do
|
|
79
|
+
|
|
80
|
+
```py
|
|
81
|
+
import os
|
|
82
|
+
|
|
83
|
+
app.config["SECRET_KEY"] = os.environ["FLASK_SECRET_KEY"]
|
|
84
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# sec-py/hf-pin-revision — cases
|
|
2
|
+
|
|
3
|
+
### case: pinned
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
model = AutoModel.from_pretrained(name, revision=rev)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
(none)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: unpinned
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
tok = AutoTokenizer.from_pretrained(name)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:7 sec-py/hf-pin-revision
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: hub download
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
hf_hub_download(repo_id, filename)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:1 sec-py/hf-pin-revision
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: dataset
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
load_dataset("org/data")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
1:1 sec-py/hf-pin-revision
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### case: a local path is still flagged
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
AutoModel.from_pretrained("./model")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
1:1 sec-py/hf-pin-revision
|
|
51
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/hf-pin-revision
|
|
3
|
+
title: 'Hugging Face downloads pin a revision'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: manual
|
|
6
|
+
fix_reason: "the commit hash to pin is a choice the author makes after reviewing the model"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B615]
|
|
9
|
+
security: { cwe: [494], owasp: ["A03:2025"], severity: medium }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`from_pretrained`, `hf_hub_download`, `snapshot_download` and `load_dataset` are called with `revision=`.
|
|
14
|
+
|
|
15
|
+
**Why:** Without a revision the code runs whatever the repository's `main` branch holds today, so a change there reaches production unreviewed (CWE-494).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
kind: call
|
|
20
|
+
all:
|
|
21
|
+
- has:
|
|
22
|
+
field: function
|
|
23
|
+
regex: '^((.*\.)?(from_pretrained|hf_hub_download|snapshot_download)|(datasets\.)?load_dataset)$'
|
|
24
|
+
- not:
|
|
25
|
+
has:
|
|
26
|
+
field: arguments
|
|
27
|
+
has:
|
|
28
|
+
kind: keyword_argument
|
|
29
|
+
all: [{ has: { field: name, regex: '^revision$' } }]
|
|
30
|
+
message: 'This download follows `main`; pin `revision=` to a commit hash'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## How to fix
|
|
34
|
+
|
|
35
|
+
1. Find the commit hash of the revision you reviewed on the model's page.
|
|
36
|
+
2. Pass it: `from_pretrained(name, revision="<hash>")`.
|
|
37
|
+
3. Keep the hash in one constant or setting so an upgrade is one reviewed change.
|
|
38
|
+
|
|
39
|
+
## Examples
|
|
40
|
+
|
|
41
|
+
### Python
|
|
42
|
+
|
|
43
|
+
#### Don't
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
from transformers import AutoModel
|
|
47
|
+
|
|
48
|
+
model = AutoModel.from_pretrained("org/model")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### Do
|
|
52
|
+
|
|
53
|
+
```py
|
|
54
|
+
from transformers import AutoModel
|
|
55
|
+
|
|
56
|
+
MODEL_REVISION = "5c9e4d1"
|
|
57
|
+
model = AutoModel.from_pretrained("org/model", revision=MODEL_REVISION)
|
|
58
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# sec-py/jinja-autoescape — cases
|
|
2
|
+
|
|
3
|
+
### case: autoescape=False
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
env = Environment(autoescape=False)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:7 sec-py/jinja-autoescape
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: no autoescape given
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
env = Environment(loader=loader)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
1:7 sec-py/jinja-autoescape
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: select_autoescape
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
env = Environment(autoescape=select_autoescape())
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
(none)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: SSTI
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
def v(request):
|
|
37
|
+
return render_template_string(request.args["t"])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```expect
|
|
41
|
+
2:12 sec-py/jinja-autoescape
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### case: from_string
|
|
45
|
+
|
|
46
|
+
```py
|
|
47
|
+
env.from_string(text)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```expect
|
|
51
|
+
1:1 sec-py/jinja-autoescape
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### case: a literal template
|
|
55
|
+
|
|
56
|
+
```py
|
|
57
|
+
render_template_string("<p>{{ x }}</p>", x=x)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```expect
|
|
61
|
+
(none)
|
|
62
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/jinja-autoescape
|
|
3
|
+
title: 'Jinja2 keeps autoescape on'
|
|
4
|
+
languages: [python]
|
|
5
|
+
fix: suggest
|
|
6
|
+
fix_reason: "turning autoescape on changes the output of templates that write HTML on purpose; those need `|safe` or `Markup`"
|
|
7
|
+
stability: preview
|
|
8
|
+
ported_from: [bandit/B701, ruff/S701]
|
|
9
|
+
security: { cwe: [79, 1336], owasp: ["A05:2025"], severity: high }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`Environment` is not created with `autoescape=False` or without `autoescape`, and `render_template_string` / `Template` / `from_string` are not given a template that is not a literal.
|
|
14
|
+
|
|
15
|
+
**Why:** Without autoescape a value written into a page can carry a script (CWE-79); a template built from a non-literal runs template code on the server (CWE-1336).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- kind: call
|
|
21
|
+
all:
|
|
22
|
+
- { has: { field: function, regex: '^(jinja2\.)?Environment$' } }
|
|
23
|
+
- has:
|
|
24
|
+
field: arguments
|
|
25
|
+
has:
|
|
26
|
+
kind: keyword_argument
|
|
27
|
+
all:
|
|
28
|
+
- { has: { field: name, regex: '^autoescape$' } }
|
|
29
|
+
- { has: { field: value, kind: 'false' } }
|
|
30
|
+
pattern: $KW
|
|
31
|
+
- all:
|
|
32
|
+
- { kind: call }
|
|
33
|
+
- { has: { field: function, regex: '^(jinja2\.)?Environment$' } }
|
|
34
|
+
- not:
|
|
35
|
+
has:
|
|
36
|
+
field: arguments
|
|
37
|
+
has: { kind: keyword_argument, has: { field: name, regex: '^(autoescape)$' } }
|
|
38
|
+
- { pattern: 'render_template_string($T)' }
|
|
39
|
+
- { pattern: 'jinja2.Template($T)' }
|
|
40
|
+
- { pattern: '$E.from_string($T)' }
|
|
41
|
+
constraints:
|
|
42
|
+
T: { not: { kind: string, not: { has: { kind: interpolation } } } }
|
|
43
|
+
E: { regex: '^(env|environment|jinja_env|jinja2\.Environment\(.*\))$' }
|
|
44
|
+
message: 'Jinja2 is not escaping, or the template comes from the request'
|
|
45
|
+
fix:
|
|
46
|
+
- { replace: { target: $KW, with: 'autoescape=True' } }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## How to fix
|
|
50
|
+
|
|
51
|
+
1. Create the environment with `autoescape=True`, or `select_autoescape()`.
|
|
52
|
+
2. Never use request data as the template: pass it as a value to a template file instead.
|
|
53
|
+
|
|
54
|
+
## Examples
|
|
55
|
+
|
|
56
|
+
### Python
|
|
57
|
+
|
|
58
|
+
#### Don't
|
|
59
|
+
|
|
60
|
+
```py
|
|
61
|
+
from jinja2 import Environment
|
|
62
|
+
|
|
63
|
+
env = Environment(autoescape=False)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Do
|
|
67
|
+
|
|
68
|
+
```py
|
|
69
|
+
from jinja2 import Environment
|
|
70
|
+
|
|
71
|
+
env = Environment(autoescape=True)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Fixed
|
|
75
|
+
|
|
76
|
+
```py
|
|
77
|
+
from jinja2 import Environment
|
|
78
|
+
|
|
79
|
+
env = Environment(autoescape=True)
|
|
80
|
+
```
|