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,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arpit Arya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
SPDX-License-Identifier: MIT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
codelaw-rules-security-python
|
|
2
|
+
|
|
3
|
+
The rules are reimplementations written for codelaw. Rules ported from
|
|
4
|
+
Bandit (https://bandit.readthedocs.io/, Apache-2.0, Copyright (c) PyCQA and
|
|
5
|
+
the Bandit contributors) and from Ruff's `S` rules (https://docs.astral.sh/ruff/,
|
|
6
|
+
MIT License, Copyright (c) 2022 Charles Marsh) name their source check in
|
|
7
|
+
`ported_from:` (`bandit/Bnnn`, `ruff/Snnn`) and reproduce its documented
|
|
8
|
+
behaviour: the matchers, messages, examples and fixes are written fresh, and
|
|
9
|
+
no code or documentation text from either tool is included. The README lists
|
|
10
|
+
every Bandit check with where it went, or why it was not ported.
|
|
11
|
+
|
|
12
|
+
The CWE identifiers are MITRE's (https://cwe.mitre.org/) and the category
|
|
13
|
+
names are the OWASP Top 10:2025's (https://owasp.org/Top10/), used as
|
|
14
|
+
references only.
|
|
15
|
+
|
|
16
|
+
Rule sets under a copyleft or source-available licence — eslint-plugin-no-unsanitized
|
|
17
|
+
(MPL-2.0), njsscan (LGPL-3.0), Semgrep's rules (the Semgrep Rules License) — are
|
|
18
|
+
read for ideas only; no text, pattern or code is taken from them (L2). CodeQL's
|
|
19
|
+
query list (MIT) was read for ideas and nothing was copied.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# `codelaw-rules-security-python`
|
|
2
|
+
|
|
3
|
+
codelaw's Python security rules (Bandit's set, Django, Flask, FastAPI, ML loaders) — one of codelaw's first-party rule packs
|
|
4
|
+
([SR-RULE-PACKS](../../records/0101_rule-packs.md)). It ships on PyPI only, as `codelaw-rules-security-python` (L6 decision 11).
|
|
5
|
+
|
|
6
|
+
```toml
|
|
7
|
+
# codelaw.toml
|
|
8
|
+
extends = ["codelaw:py/recommended", "codelaw:sec-py/recommended"]
|
|
9
|
+
preview = true # every rule is `preview` until a corpus run promotes it
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`codelaw init` does not add it: a security pack is chosen, not detected.
|
|
13
|
+
|
|
14
|
+
## Presets
|
|
15
|
+
|
|
16
|
+
<!-- docs-sync:presets -->
|
|
17
|
+
|
|
18
|
+
| preset | rules | ported from |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `sec-py/recommended` | 57 | high-confidence rules, at `error` |
|
|
21
|
+
| `sec-py/strict` | 61 | `sec-py/recommended` and the heuristic rules, at `warn` |
|
|
22
|
+
|
|
23
|
+
<!-- /docs-sync:presets -->
|
|
24
|
+
|
|
25
|
+
`recommended` holds only high-confidence rules, at `error`; `strict` adds the
|
|
26
|
+
heuristic ones, at `warn`, so a team can start without noise.
|
|
27
|
+
|
|
28
|
+
## Rules
|
|
29
|
+
|
|
30
|
+
61 rules, each a `match:` rule (no Rust). Every rule carries `security: { cwe, owasp, severity }`
|
|
31
|
+
([SR-RULE-FORMAT](../../records/0112_rule-format.md) decision 2a), so
|
|
32
|
+
|
|
33
|
+
- its finding names the CWE (`sec-py/<rule> (CWE-79) …`);
|
|
34
|
+
- `codelaw rules --tag cwe:79` and `--tag owasp:A05` find it, and
|
|
35
|
+
`codelaw rules --security-coverage` puts it in the OWASP Top 10:2025 matrix;
|
|
36
|
+
- SARIF carries its `security` and `external/cwe/cwe-<n>` tags and a
|
|
37
|
+
`security-severity` that GitHub code scanning ranks by
|
|
38
|
+
([SR-CLI-MCP](../../records/0115_cli-and-mcp.md) decision 4).
|
|
39
|
+
|
|
40
|
+
| rule | what it flags | CWE | OWASP 2025 | fix | preset |
|
|
41
|
+
|---|---|---|---|---|---|
|
|
42
|
+
| `sec-py/no-exec-eval` | `exec`, `eval` and `compile` are not given a value that is not a string literal | 94 | A05 | manual | recommended |
|
|
43
|
+
| `sec-py/no-shell` | A subprocess is not started with `shell=True`, and `os.system` / `os.popen` are not given a value that is not a literal | 78 | A05 | suggest | recommended |
|
|
44
|
+
| `sec-py/no-sql-string` | A SQL statement is not built with an f-string, `%`, `+` or `.format`; the values go in as parameters | 89 | A05 | manual | recommended |
|
|
45
|
+
| `sec-py/no-unsafe-deserialize` | Data is not loaded with `pickle`, `cPickle`, `dill`, `marshal`, `shelve` or `jsonpickle` | 502 | A08 | manual | recommended |
|
|
46
|
+
| `sec-py/yaml-safe-load` | YAML is read with `yaml.safe_load`, or with `yaml.load` and `SafeLoader` | 502 | A08 | suggest | recommended |
|
|
47
|
+
| `sec-py/torch-load-weights-only` | `torch.load` is called with `weights_only=True` | 502 | A08 | suggest | recommended |
|
|
48
|
+
| `sec-py/hf-pin-revision` | `from_pretrained`, `hf_hub_download`, `snapshot_download` and `load_dataset` are called with `revision=` | 494 | A03 | manual | recommended |
|
|
49
|
+
| `sec-py/no-xml-unsafe-parser` | XML from outside the program is parsed with `defusedxml`, not with `xml.etree`, `minidom`, `sax` or `lxml` | 611 | A05 | suggest | recommended |
|
|
50
|
+
| `sec-py/no-tls-bypass` | Certificate verification is not switched off (`verify=False`, `_create_unverified_context`), and an SSL context is not pinned to an old protocol | 295 | A04 | suggest | recommended |
|
|
51
|
+
| `sec-py/requests-timeout` | `requests` and `httpx` calls pass `timeout=` | 400 | A06 | suggest | recommended |
|
|
52
|
+
| `sec-py/no-weak-hash` | `hashlib.md5` and `hashlib.sha1` pass `usedforsecurity=False`, or are replaced by SHA-256 | 327 | A04 | suggest | recommended |
|
|
53
|
+
| `sec-py/no-insecure-random` | A password, token, key, nonce or code is not made with the `random` module | 338 | A04 | suggest | recommended |
|
|
54
|
+
| `sec-py/no-hardcoded-secret` | A password, key or token is not a string literal in the code, and a cloud access key is not pasted into it | 798 | A07 | manual | recommended |
|
|
55
|
+
| `sec-py/no-insecure-tempfile` | Temporary files are made with `tempfile.mkstemp`, `NamedTemporaryFile` or `TemporaryDirectory`, not `mktemp`, and not at a fixed path under `/tmp` | 377 | A01 | suggest | recommended |
|
|
56
|
+
| `sec-py/tarfile-filter` | `TarFile.extractall` and `shutil.unpack_archive` pass `filter="data"` | 22 | A01 | suggest | recommended |
|
|
57
|
+
| `sec-py/no-path-traversal` | A value from the request is not passed to `open`, `send_file`, `send_from_directory` or `FileResponse` as a path | 22 | A01 | manual | recommended |
|
|
58
|
+
| `sec-py/no-ssrf` | A value from the request is not the URL given to `requests`, `httpx` or `urllib` | 918 | A01 | manual | recommended |
|
|
59
|
+
| `sec-py/no-open-redirect` | A value from the request is not passed to `redirect`, `HttpResponseRedirect` or `RedirectResponse` | 601 | A01 | manual | recommended |
|
|
60
|
+
| `sec-py/jinja-autoescape` | `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 | 79, 1336 | A05 | suggest | recommended |
|
|
61
|
+
| `sec-py/no-mark-safe` | `mark_safe`, `SafeString`, `Markup` and `format_html` are not given a value that is not a literal | 79 | A05 | manual | recommended |
|
|
62
|
+
| `sec-py/flask-debug` | `app.run(debug=True)` is not written in code | 489 | A02 | suggest | recommended |
|
|
63
|
+
| `sec-py/django-settings` | In a settings module, `DEBUG`, `ALLOWED_HOSTS`, `SECRET_KEY` and the cookie flags are not left in their development values | 489, 798, 614 | A02 | manual | recommended |
|
|
64
|
+
| `sec-py/no-csrf-exempt` | A view is not decorated with `@csrf_exempt` | 352 | A01 | manual | recommended |
|
|
65
|
+
| `sec-py/fastapi-cors` | `CORSMiddleware` is not given `allow_origins=["*"]` together with `allow_credentials=True` | 942 | A02 | manual | recommended |
|
|
66
|
+
| `sec-py/no-assert-for-auth` | A permission, role or authentication test is not written as an `assert` | 703 | A10 | manual | recommended |
|
|
67
|
+
| `sec-py/no-silent-except` | An `except` that catches everything (bare, `Exception` or `BaseException`) does not just `pass` or `continue` | 703 | A10 | manual | recommended |
|
|
68
|
+
| `sec-py/paramiko-host-key` | `paramiko` is not told to accept unknown host keys with `AutoAddPolicy` or `WarningPolicy` | 295 | A07 | manual | recommended |
|
|
69
|
+
| `sec-py/no-world-writable` | `os.chmod` and `os.umask` do not give write access to everyone | 732 | A01 | suggest | recommended |
|
|
70
|
+
| `sec-py/no-secret-in-log` | A value named `password`, `token` or `secret` is not written to a log | 532 | A09 | manual | recommended |
|
|
71
|
+
| `sec-py/no-llm-output-to-sink` | The reply of an OpenAI, Anthropic or LangChain call does not reach `exec`, `eval`, a subprocess, SQL or `mark_safe` | 94 | A05 | manual | strict |
|
|
72
|
+
| `sec-py/no-ldap-injection` | An LDAP search filter is not made with an f-string, `%`, `+` or `.format` | 90 | A05 | manual | recommended |
|
|
73
|
+
| `sec-py/no-xpath-injection` | An XPath expression is not made with an f-string, `%`, `+` or `.format`; values go in as XPath variables | 643 | A05 | manual | recommended |
|
|
74
|
+
| `sec-py/no-nosql-injection` | A value from the request is not used whole as a `pymongo` filter, and a `$where` expression is not built from a request value | 943 | A05 | manual | recommended |
|
|
75
|
+
| `sec-py/no-header-injection` | A value from the request is not written into a response header or a cookie value without its line breaks removed | 113 | A05 | manual | recommended |
|
|
76
|
+
| `sec-py/no-log-injection` | A value from the request is not passed to a logger without its line breaks removed | 117 | A09 | manual | strict |
|
|
77
|
+
| `sec-py/cookie-flags` | `set_cookie` passes `secure`, `httponly` and `samesite`, and none of them is `False` or `None` | 614, 1004 | A02 | suggest | recommended |
|
|
78
|
+
| `sec-py/no-insecure-protocol` | `telnetlib`, `ftplib` and SNMP community strings (v1/v2c) are not used | 319 | A04 | manual | recommended |
|
|
79
|
+
| `sec-py/urlopen-scheme` | `urllib.request.urlopen` and `urlretrieve` are not given a value that is not an `http(s)` literal | 22, 918 | A01 | manual | recommended |
|
|
80
|
+
| `sec-py/no-weak-cipher` | A weak cipher (DES, 3DES, ARC2, ARC4, Blowfish, CAST5, IDEA) or ECB mode is not used | 327 | A04 | manual | recommended |
|
|
81
|
+
| `sec-py/no-pycrypto` | `Crypto` is not imported from the unmaintained `pycrypto` package | 1104 | A03 | manual | recommended |
|
|
82
|
+
| `sec-py/weak-key-size` | An RSA or DSA key is not generated shorter than 2048 bits, and an elliptic curve is at least 224 bits | 326 | A04 | suggest | recommended |
|
|
83
|
+
| `sec-py/password-hash-effort` | A password is not hashed with a plain `hashlib` digest, `pbkdf2_hmac` runs at least 600 000 iterations, and `bcrypt` uses at least 10 rounds | 916 | A04 | manual | recommended |
|
|
84
|
+
| `sec-py/no-hardcoded-crypto-key` | A cipher key, HMAC key, IV or nonce is not a literal | 321, 329 | A04 | manual | recommended |
|
|
85
|
+
| `sec-py/no-mako` | `mako.template.Template` is not used | 79 | A05 | manual | recommended |
|
|
86
|
+
| `sec-py/logging-listen` | `logging.config.listen()` is not called | 94 | A08 | manual | recommended |
|
|
87
|
+
| `sec-py/no-wildcard-injection` | A command that runs `tar`, `chown`, `chmod` or `rsync` on `*` is not started through a shell | 155 | A05 | manual | recommended |
|
|
88
|
+
| `sec-py/no-partial-path` | A subprocess is not started with a bare program name such as `"git"` | 426 | A08 | manual | strict |
|
|
89
|
+
| `sec-py/no-xmlrpc` | An `xmlrpc` client or server is not created without `defusedxml.xmlrpc.monkey_patch()` in the program | 776 | A05 | manual | recommended |
|
|
90
|
+
| `sec-py/no-mass-assignment` | `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__"` | 915 | A01 | manual | recommended |
|
|
91
|
+
| `sec-py/no-reflected-xss` | A value from the request is not passed to `HttpResponse`, `make_response` or `HTMLResponse`, or built into a returned string, without escaping | 79 | A05 | manual | recommended |
|
|
92
|
+
| `sec-py/flask-settings` | `SECRET_KEY` is not a literal, and `WTF_CSRF_ENABLED` and `SESSION_COOKIE_SECURE` are not switched off in the app configuration | 798, 352, 614 | A02 | manual | recommended |
|
|
93
|
+
| `sec-py/no-dynamic-import` | A value from the request is not passed to `__import__`, `importlib.import_module` or `getattr` | 470 | A05 | manual | recommended |
|
|
94
|
+
| `sec-py/upload-secure-filename` | A file from a request is not saved under the name the client gave it | 434, 22 | A01 | suggest | recommended |
|
|
95
|
+
| `sec-py/ml-unsafe-load` | `numpy.load(allow_pickle=True)`, `joblib.load`, `pandas.read_pickle` and Keras `load_model(safe_mode=False)` are not used | 502 | A08 | manual | recommended |
|
|
96
|
+
| `sec-py/no-trust-remote-code` | `from_pretrained`, `load_dataset` and `pipeline` are not called with `trust_remote_code=True` | 94 | A03 | manual | recommended |
|
|
97
|
+
| `sec-py/no-prompt-injection` | A value from the request is not part of the system prompt of a language-model call | 1427 | A05 | manual | recommended |
|
|
98
|
+
| `sec-py/no-bind-all-interfaces` | A socket or server is not bound to `0.0.0.0` in the source | 605 | A02 | manual | strict |
|
|
99
|
+
| `sec-py/no-regex-injection` | A value from the request is not used as the pattern of `re.compile`, `re.search`, `re.match` or the other `re` functions | 730, 1333 | A06 | suggest | recommended |
|
|
100
|
+
| `sec-py/no-format-string-injection` | A value from the request is not the receiver of `.format` | 134 | A05 | manual | recommended |
|
|
101
|
+
| `sec-py/no-cleartext-http` | `requests` and `httpx` are not called with an `http://` URL, except for localhost | 319 | A04 | manual | recommended |
|
|
102
|
+
| `sec-py/no-stack-trace-response` | A response does not contain `traceback.format_exc()` or the text of the exception | 209 | A10 | manual | recommended |
|
|
103
|
+
|
|
104
|
+
A rule that says `suggest` has a `fix:` that `codelaw fix --unsafe` applies to
|
|
105
|
+
the common shape of the code; a match of another shape stays `manual`, with
|
|
106
|
+
the steps in the rule's *How to fix*. Three rules the catalogue called
|
|
107
|
+
`suggest` are `manual` here, because no edit of the code alone is right:
|
|
108
|
+
`sec-py/no-xpath-injection` (the expression has to be rewritten around
|
|
109
|
+
variables), `sec-py/no-xmlrpc` (the patch must run at start-up) and
|
|
110
|
+
`sec-py/no-cleartext-http` (the server must serve HTTPS first).
|
|
111
|
+
|
|
112
|
+
## What the rules see
|
|
113
|
+
|
|
114
|
+
The rules match the code **as written**. They follow an alias only where a
|
|
115
|
+
rule says so (`sec-py/no-xml-unsafe-parser` and `sec-py/no-mako` use the
|
|
116
|
+
`imported-from` predicate); `import numpy as np` is matched by its common
|
|
117
|
+
name `np`, and `from subprocess import run` is not matched at all. A value
|
|
118
|
+
that a function receives as a parameter is not a request value.
|
|
119
|
+
|
|
120
|
+
### `from_source:` is not taint analysis
|
|
121
|
+
|
|
122
|
+
A rule here that asks whether a sink's argument comes from a request
|
|
123
|
+
uses the `from_source:` predicate ([SR-RULE-FORMAT](../../records/0112_rule-format.md)
|
|
124
|
+
decision 3a). It holds when the argument **is** a listed request source
|
|
125
|
+
(or a method called on one), is built from one by concatenation,
|
|
126
|
+
interpolation or `?:`, or is a local name bound to one **in the same
|
|
127
|
+
function** — one or more assignments or destructurings away. It never
|
|
128
|
+
follows a call, a return value, a parameter, a closure's outer variable or
|
|
129
|
+
another file, so a value that crosses a function boundary is not seen
|
|
130
|
+
(cross-function flow waits on a deeper analysis, as B-049's type-aware
|
|
131
|
+
rules do). Every rule page that uses it says so. The sources are listed per
|
|
132
|
+
framework in this pack's `data/sources/<framework>.json`: Django
|
|
133
|
+
(`request.GET`, `request.POST`, `request.COOKIES`, `request.body`,
|
|
134
|
+
`request.FILES`, `request.META`, `request.headers`, DRF's `request.data` and
|
|
135
|
+
`request.query_params`), Flask (`request.args`, `form`, `values`, `json`,
|
|
136
|
+
`get_json`, `data`, `cookies`, `files`, `headers`, `view_args`) and
|
|
137
|
+
FastAPI/Starlette's `Request` (`query_params`, `path_params`, `headers`,
|
|
138
|
+
`cookies`, `form`, `json`, `body`).
|
|
139
|
+
|
|
140
|
+
Two limits follow. FastAPI's typed handler parameters (`def f(q: str)`) are
|
|
141
|
+
function parameters, so they are not sources; only the `request` object is.
|
|
142
|
+
And a method called on a source stays a source: `request.args.get("x").strip()`
|
|
143
|
+
is still the request value, so the way out of a finding is to pass the value
|
|
144
|
+
through a function of your own.
|
|
145
|
+
|
|
146
|
+
## Not ported, and why
|
|
147
|
+
|
|
148
|
+
This pack accounts for every check of
|
|
149
|
+
[Bandit](https://bandit.readthedocs.io/en/latest/plugins/index.html)
|
|
150
|
+
(Apache-2.0) and the Ruff `S` rules (MIT) that mirror it. The rules are
|
|
151
|
+
written for codelaw; `ported_from:` names the Bandit and Ruff ids they cover
|
|
152
|
+
([NOTICE](NOTICE)).
|
|
153
|
+
|
|
154
|
+
| Bandit | Ruff | status | where |
|
|
155
|
+
|---|---|---|---|
|
|
156
|
+
| B101 `assert_used` | S101 | partly | `sec-py/no-assert-for-auth`: only an `assert` that is an access check; an `assert` in general is not a security flaw |
|
|
157
|
+
| B102 `exec_used` | S102 | ported | `sec-py/no-exec-eval` |
|
|
158
|
+
| B103 `set_bad_file_permissions` | S103 | ported | `sec-py/no-world-writable` |
|
|
159
|
+
| B104 `hardcoded_bind_all_interfaces` | S104 | ported | `sec-py/no-bind-all-interfaces` (`strict`) |
|
|
160
|
+
| B105, B106, B107 hard-coded passwords | S105, S106, S107 | ported | `sec-py/no-hardcoded-secret`; Django and Flask settings in `sec-py/django-settings` and `sec-py/flask-settings` |
|
|
161
|
+
| B108 `hardcoded_tmp_directory` | S108 | ported | `sec-py/no-insecure-tempfile` |
|
|
162
|
+
| B110, B112 `try_except_pass`, `try_except_continue` | S110, S112 | ported | `sec-py/no-silent-except` |
|
|
163
|
+
| B113 `request_without_timeout` | S113 | ported | `sec-py/requests-timeout` |
|
|
164
|
+
| B201 `flask_debug_true` | S201 | ported | `sec-py/flask-debug` |
|
|
165
|
+
| B202 `tarfile_unsafe_members` | S202 | ported | `sec-py/tarfile-filter` |
|
|
166
|
+
| B301, B302 `pickle`, `marshal` | S301, S302 | ported | `sec-py/no-unsafe-deserialize` |
|
|
167
|
+
| B303 `md5`, `sha1` | S303 | ported | `sec-py/no-weak-hash` |
|
|
168
|
+
| B304, B305 ciphers and modes | S304, S305 | ported | `sec-py/no-weak-cipher` |
|
|
169
|
+
| B306 `mktemp_q` | S306 | ported | `sec-py/no-insecure-tempfile` |
|
|
170
|
+
| B307 `eval` | S307 | ported | `sec-py/no-exec-eval` |
|
|
171
|
+
| B308 `mark_safe` | S308 | ported | `sec-py/no-mark-safe` |
|
|
172
|
+
| B309 `httpsconnection` | - | not ported | Python 2.7 and early 3.x only: `http.client.HTTPSConnection` verifies certificates by default since 3.4.3 |
|
|
173
|
+
| B310 `urllib_urlopen` | S310 | ported | `sec-py/urlopen-scheme` |
|
|
174
|
+
| B311 `random` | S311 | ported | `sec-py/no-insecure-random`: only when the value is bound to a secret-like name; `random` for a dice roll is fine |
|
|
175
|
+
| B312 `telnetlib` | S312 | ported | `sec-py/no-insecure-protocol` (at the import) |
|
|
176
|
+
| B313 to B320 XML calls | S313 to S320 | ported | `sec-py/no-xml-unsafe-parser` |
|
|
177
|
+
| B321 `ftplib` | S321 | ported | `sec-py/no-insecure-protocol` (at the import) |
|
|
178
|
+
| B322 `input` | - | not ported | Python 2's `input` evaluates its input; Python 3's does not |
|
|
179
|
+
| B323 `unverified_context` | S323 | ported | `sec-py/no-tls-bypass` |
|
|
180
|
+
| B324 `hashlib` | S324 | ported | `sec-py/no-weak-hash` |
|
|
181
|
+
| B325 `tempnam` | S325 | ported | `sec-py/no-insecure-tempfile` (`os.tempnam`, `os.tmpnam`) |
|
|
182
|
+
| B401, B402 `telnetlib`, `ftplib` imports | S401, S402 | ported | `sec-py/no-insecure-protocol` |
|
|
183
|
+
| B403 `pickle` import | S403 | covered elsewhere | the load call is flagged by `sec-py/no-unsafe-deserialize`; importing `pickle` to write it is not a flaw |
|
|
184
|
+
| B404 `subprocess` import | S404 | not ported | an import alone flags every program that runs a command; the calls are `sec-py/no-shell` and `sec-py/no-partial-path` |
|
|
185
|
+
| B405 to B410 XML imports | S405 to S410 | covered elsewhere | the parse calls are flagged by `sec-py/no-xml-unsafe-parser`; the import of a parser is not |
|
|
186
|
+
| B411 `xmlrpc` import | S411 | ported | `sec-py/no-xmlrpc` (at the client or server call) |
|
|
187
|
+
| B412 `httpoxy` | S412 | not ported | CGI handlers (`wsgiref.handlers.CGIHandler`) are rare, and web servers now drop the `Proxy` header |
|
|
188
|
+
| B413 `pycrypto` | S413 | ported | `sec-py/no-pycrypto` |
|
|
189
|
+
| B414 `pycryptodome` | - | not ported | Bandit dropped it: `pycryptodome` is maintained |
|
|
190
|
+
| B415 `pyghmi` | S415 | not ported | plain-text IPMI, a niche library; `sec-py/no-insecure-protocol` covers the common ones |
|
|
191
|
+
| B501 to B504 TLS | S501 to S504 | ported | `sec-py/no-tls-bypass` |
|
|
192
|
+
| B505 `weak_cryptographic_key` | S505 | ported | `sec-py/weak-key-size` |
|
|
193
|
+
| B506 `yaml_load` | S506 | ported | `sec-py/yaml-safe-load` |
|
|
194
|
+
| B507 `ssh_no_host_key_verification` | S507 | ported | `sec-py/paramiko-host-key` |
|
|
195
|
+
| B508 `snmp_insecure_version` | S508 | ported | `sec-py/no-insecure-protocol` |
|
|
196
|
+
| B509 `snmp_weak_cryptography` | S509 | not ported | needs the `UsmUserData` protocol arguments checked; only the v1/v2c community string is flagged |
|
|
197
|
+
| B601 `paramiko_calls` | S601 | not ported | `exec_command` runs on the remote host and is flagged by Bandit for any call; no safe rewrite to name |
|
|
198
|
+
| B602, B604, B605 shell | S602, S604, S605 | ported | `sec-py/no-shell` |
|
|
199
|
+
| B603 `subprocess_without_shell_equals_true` | S603 | not ported | informational in Bandit and flags every `subprocess` call; the dangerous cases are `sec-py/no-shell` and `sec-py/no-wildcard-injection` |
|
|
200
|
+
| B606 `start_process_with_no_shell` | S606 | not ported | `os.exec*` and `os.spawn*` are rare; `sec-py/no-partial-path` covers `subprocess` and `os.system` |
|
|
201
|
+
| B607 `start_process_with_partial_path` | S607 | ported | `sec-py/no-partial-path` (`strict`) |
|
|
202
|
+
| B608 `hardcoded_sql_expressions` | S608 | ported | `sec-py/no-sql-string`: from the call that runs the statement, not from a string that looks like SQL |
|
|
203
|
+
| B609 `linux_commands_wildcard_injection` | S609 | ported | `sec-py/no-wildcard-injection` |
|
|
204
|
+
| B610, B611 Django `extra`, `RawSQL` | S610, S611 | ported | `sec-py/no-sql-string` |
|
|
205
|
+
| B612 `logging_config_insecure_listen` | S612 | ported | `sec-py/logging-listen` |
|
|
206
|
+
| B613 `trojansource` | - | not ported | bidirectional control characters in the source bytes; a rule reads the syntax tree and does not see them |
|
|
207
|
+
| B614 `pytorch_load` | - | ported | `sec-py/torch-load-weights-only` |
|
|
208
|
+
| B615 `huggingface_unsafe_download` | - | ported | `sec-py/hf-pin-revision` |
|
|
209
|
+
| B701 `jinja2_autoescape_false` | S701 | ported | `sec-py/jinja-autoescape` |
|
|
210
|
+
| B702 `use_of_mako_templates` | S702 | ported | `sec-py/no-mako` |
|
|
211
|
+
| B703 `django_mark_safe` | S703 | ported | `sec-py/no-mark-safe` |
|
|
212
|
+
| B704 `markupsafe_markup_xss` | S704 | ported | `sec-py/no-mark-safe` |
|
|
213
|
+
|
|
214
|
+
Beyond Bandit and Ruff the pack adds the framework and library rules of the
|
|
215
|
+
catalogue: Django, Flask and FastAPI settings and sinks, mass assignment,
|
|
216
|
+
uploads, header, LDAP, XPath and NoSQL injection, ML model loading
|
|
217
|
+
(`torch.load`, `joblib`, `numpy`, Hugging Face), `trust_remote_code`, prompt
|
|
218
|
+
injection and language-model output reaching a sink. Two parts of the catalogue are not
|
|
219
|
+
built: the `strict` ReDoS check for a literal pattern with nested quantifiers
|
|
220
|
+
(`sec-py/no-regex-injection` handles only a request-supplied pattern), and a
|
|
221
|
+
`from_source` check on every argument of a call (it reads the first, and for a
|
|
222
|
+
logger the second).
|
|
223
|
+
|
|
224
|
+
## Corpus
|
|
225
|
+
|
|
226
|
+
`packs/security-python/corpus.toml` holds the finding count per rule over the
|
|
227
|
+
pinned repositories (`scripts/corpus.json`): Flask, Starlette,
|
|
228
|
+
full-stack-fastapi-template and djangoproject.com, 946 files. On 2026-10-26
|
|
229
|
+
the rules gave **34 findings, all of them read by hand: one false positive**
|
|
230
|
+
(`sec-py/no-open-redirect` on a redirect to a trusted URL that had `?q=…`
|
|
231
|
+
appended from the request, which `from_source:` cannot tell from a hostile
|
|
232
|
+
target), about 3%. The other 33 are what the rules say: a `mark_safe` of a
|
|
233
|
+
non-literal, an MD5 or SHA-1 digest, an `exec` of a config file, a request
|
|
234
|
+
without a `timeout`, a `subprocess` started by a bare name (`strict`), and
|
|
235
|
+
the like. Two limits of that number: four repositories are a small sample,
|
|
236
|
+
and the noisy rules skip test files (`exclude:` in their frontmatter) because
|
|
237
|
+
the first run, before they did, was mostly asserts and `password="password"`
|
|
238
|
+
in tests. Every rule is `preview` until its count has been reviewed again
|
|
239
|
+
over a larger corpus.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
name = "codelaw-rules-security-python"
|
|
2
|
+
version = "0.3.0"
|
|
3
|
+
min_codelaw = "0.2.0"
|
|
4
|
+
languages = ["python"]
|
|
5
|
+
presets = ["sec-py/recommended", "sec-py/strict"]
|
|
6
|
+
description = "codelaw's security rules for Python: CWE and OWASP Top 10:2025 mapped; PyPI only (SR-RULE-PACKS)"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# `codelaw test --corpus` finding counts (SR-RULE-AUTHORING decision 4).
|
|
2
|
+
# Rewrite with --update-snapshot, and review the change.
|
|
3
|
+
|
|
4
|
+
[findings]
|
|
5
|
+
"sec-py/cookie-flags" = 0
|
|
6
|
+
"sec-py/django-settings" = 0
|
|
7
|
+
"sec-py/fastapi-cors" = 0
|
|
8
|
+
"sec-py/flask-debug" = 0
|
|
9
|
+
"sec-py/flask-settings" = 1
|
|
10
|
+
"sec-py/hf-pin-revision" = 0
|
|
11
|
+
"sec-py/jinja-autoescape" = 3
|
|
12
|
+
"sec-py/logging-listen" = 0
|
|
13
|
+
"sec-py/ml-unsafe-load" = 0
|
|
14
|
+
"sec-py/no-assert-for-auth" = 0
|
|
15
|
+
"sec-py/no-bind-all-interfaces" = 0
|
|
16
|
+
"sec-py/no-cleartext-http" = 0
|
|
17
|
+
"sec-py/no-csrf-exempt" = 1
|
|
18
|
+
"sec-py/no-dynamic-import" = 0
|
|
19
|
+
"sec-py/no-exec-eval" = 2
|
|
20
|
+
"sec-py/no-format-string-injection" = 0
|
|
21
|
+
"sec-py/no-hardcoded-crypto-key" = 0
|
|
22
|
+
"sec-py/no-hardcoded-secret" = 1
|
|
23
|
+
"sec-py/no-header-injection" = 0
|
|
24
|
+
"sec-py/no-insecure-protocol" = 0
|
|
25
|
+
"sec-py/no-insecure-random" = 0
|
|
26
|
+
"sec-py/no-insecure-tempfile" = 0
|
|
27
|
+
"sec-py/no-ldap-injection" = 0
|
|
28
|
+
"sec-py/no-llm-output-to-sink" = 0
|
|
29
|
+
"sec-py/no-log-injection" = 0
|
|
30
|
+
"sec-py/no-mako" = 0
|
|
31
|
+
"sec-py/no-mark-safe" = 6
|
|
32
|
+
"sec-py/no-mass-assignment" = 0
|
|
33
|
+
"sec-py/no-nosql-injection" = 0
|
|
34
|
+
"sec-py/no-open-redirect" = 1
|
|
35
|
+
"sec-py/no-partial-path" = 7
|
|
36
|
+
"sec-py/no-path-traversal" = 0
|
|
37
|
+
"sec-py/no-prompt-injection" = 0
|
|
38
|
+
"sec-py/no-pycrypto" = 0
|
|
39
|
+
"sec-py/no-reflected-xss" = 0
|
|
40
|
+
"sec-py/no-regex-injection" = 0
|
|
41
|
+
"sec-py/no-secret-in-log" = 0
|
|
42
|
+
"sec-py/no-shell" = 1
|
|
43
|
+
"sec-py/no-silent-except" = 1
|
|
44
|
+
"sec-py/no-sql-string" = 0
|
|
45
|
+
"sec-py/no-ssrf" = 0
|
|
46
|
+
"sec-py/no-stack-trace-response" = 2
|
|
47
|
+
"sec-py/no-tls-bypass" = 0
|
|
48
|
+
"sec-py/no-trust-remote-code" = 0
|
|
49
|
+
"sec-py/no-unsafe-deserialize" = 0
|
|
50
|
+
"sec-py/no-weak-cipher" = 0
|
|
51
|
+
"sec-py/no-weak-hash" = 4
|
|
52
|
+
"sec-py/no-wildcard-injection" = 0
|
|
53
|
+
"sec-py/no-world-writable" = 0
|
|
54
|
+
"sec-py/no-xml-unsafe-parser" = 0
|
|
55
|
+
"sec-py/no-xmlrpc" = 0
|
|
56
|
+
"sec-py/no-xpath-injection" = 0
|
|
57
|
+
"sec-py/paramiko-host-key" = 0
|
|
58
|
+
"sec-py/password-hash-effort" = 0
|
|
59
|
+
"sec-py/requests-timeout" = 4
|
|
60
|
+
"sec-py/tarfile-filter" = 0
|
|
61
|
+
"sec-py/torch-load-weights-only" = 0
|
|
62
|
+
"sec-py/upload-secure-filename" = 0
|
|
63
|
+
"sec-py/urlopen-scheme" = 0
|
|
64
|
+
"sec-py/weak-key-size" = 0
|
|
65
|
+
"sec-py/yaml-safe-load" = 0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[
|
|
2
|
+
"request.GET",
|
|
3
|
+
"request.POST",
|
|
4
|
+
"request.COOKIES",
|
|
5
|
+
"request.body",
|
|
6
|
+
"request.FILES",
|
|
7
|
+
"request.META",
|
|
8
|
+
"request.headers",
|
|
9
|
+
"request.data",
|
|
10
|
+
"request.query_params",
|
|
11
|
+
"self.request.GET",
|
|
12
|
+
"self.request.POST",
|
|
13
|
+
"self.request.COOKIES",
|
|
14
|
+
"self.request.body",
|
|
15
|
+
"self.request.data",
|
|
16
|
+
"self.request.query_params"
|
|
17
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
description = "high-confidence security rules for Python, at error"
|
|
2
|
+
|
|
3
|
+
[rules]
|
|
4
|
+
"sec-py/cookie-flags" = "error"
|
|
5
|
+
"sec-py/django-settings" = "error"
|
|
6
|
+
"sec-py/fastapi-cors" = "error"
|
|
7
|
+
"sec-py/flask-debug" = "error"
|
|
8
|
+
"sec-py/flask-settings" = "error"
|
|
9
|
+
"sec-py/hf-pin-revision" = "error"
|
|
10
|
+
"sec-py/jinja-autoescape" = "error"
|
|
11
|
+
"sec-py/logging-listen" = "error"
|
|
12
|
+
"sec-py/ml-unsafe-load" = "error"
|
|
13
|
+
"sec-py/no-assert-for-auth" = "error"
|
|
14
|
+
"sec-py/no-cleartext-http" = "error"
|
|
15
|
+
"sec-py/no-csrf-exempt" = "error"
|
|
16
|
+
"sec-py/no-dynamic-import" = "error"
|
|
17
|
+
"sec-py/no-exec-eval" = "error"
|
|
18
|
+
"sec-py/no-format-string-injection" = "error"
|
|
19
|
+
"sec-py/no-hardcoded-crypto-key" = "error"
|
|
20
|
+
"sec-py/no-hardcoded-secret" = "error"
|
|
21
|
+
"sec-py/no-header-injection" = "error"
|
|
22
|
+
"sec-py/no-insecure-protocol" = "error"
|
|
23
|
+
"sec-py/no-insecure-random" = "error"
|
|
24
|
+
"sec-py/no-insecure-tempfile" = "error"
|
|
25
|
+
"sec-py/no-ldap-injection" = "error"
|
|
26
|
+
"sec-py/no-mako" = "error"
|
|
27
|
+
"sec-py/no-mark-safe" = "error"
|
|
28
|
+
"sec-py/no-mass-assignment" = "error"
|
|
29
|
+
"sec-py/no-nosql-injection" = "error"
|
|
30
|
+
"sec-py/no-open-redirect" = "error"
|
|
31
|
+
"sec-py/no-path-traversal" = "error"
|
|
32
|
+
"sec-py/no-prompt-injection" = "error"
|
|
33
|
+
"sec-py/no-pycrypto" = "error"
|
|
34
|
+
"sec-py/no-reflected-xss" = "error"
|
|
35
|
+
"sec-py/no-regex-injection" = "error"
|
|
36
|
+
"sec-py/no-secret-in-log" = "error"
|
|
37
|
+
"sec-py/no-shell" = "error"
|
|
38
|
+
"sec-py/no-silent-except" = "error"
|
|
39
|
+
"sec-py/no-sql-string" = "error"
|
|
40
|
+
"sec-py/no-ssrf" = "error"
|
|
41
|
+
"sec-py/no-stack-trace-response" = "error"
|
|
42
|
+
"sec-py/no-tls-bypass" = "error"
|
|
43
|
+
"sec-py/no-trust-remote-code" = "error"
|
|
44
|
+
"sec-py/no-unsafe-deserialize" = "error"
|
|
45
|
+
"sec-py/no-weak-cipher" = "error"
|
|
46
|
+
"sec-py/no-weak-hash" = "error"
|
|
47
|
+
"sec-py/no-wildcard-injection" = "error"
|
|
48
|
+
"sec-py/no-world-writable" = "error"
|
|
49
|
+
"sec-py/no-xml-unsafe-parser" = "error"
|
|
50
|
+
"sec-py/no-xmlrpc" = "error"
|
|
51
|
+
"sec-py/no-xpath-injection" = "error"
|
|
52
|
+
"sec-py/paramiko-host-key" = "error"
|
|
53
|
+
"sec-py/password-hash-effort" = "error"
|
|
54
|
+
"sec-py/requests-timeout" = "error"
|
|
55
|
+
"sec-py/tarfile-filter" = "error"
|
|
56
|
+
"sec-py/torch-load-weights-only" = "error"
|
|
57
|
+
"sec-py/upload-secure-filename" = "error"
|
|
58
|
+
"sec-py/urlopen-scheme" = "error"
|
|
59
|
+
"sec-py/weak-key-size" = "error"
|
|
60
|
+
"sec-py/yaml-safe-load" = "error"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
description = "sec-py/recommended and the heuristic security rules, at warn"
|
|
2
|
+
extends = ["codelaw:sec-py/recommended"]
|
|
3
|
+
|
|
4
|
+
[rules]
|
|
5
|
+
"sec-py/no-bind-all-interfaces" = "warn"
|
|
6
|
+
"sec-py/no-llm-output-to-sink" = "warn"
|
|
7
|
+
"sec-py/no-log-injection" = "warn"
|
|
8
|
+
"sec-py/no-partial-path" = "warn"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# sec-py/cookie-flags — cases
|
|
2
|
+
|
|
3
|
+
### case: no flags
|
|
4
|
+
|
|
5
|
+
```py
|
|
6
|
+
resp.set_cookie("a", "b")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```expect
|
|
10
|
+
1:1 sec-py/cookie-flags
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### case: all flags
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
resp.set_cookie("a", "b", secure=True, httponly=True, samesite="Lax")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```expect
|
|
20
|
+
(none)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### case: samesite None
|
|
24
|
+
|
|
25
|
+
```py
|
|
26
|
+
resp.set_cookie("a", "b", secure=True, httponly=True, samesite=None)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```expect
|
|
30
|
+
1:1 sec-py/cookie-flags
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### case: one flag missing
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
resp.set_cookie("a", "b", secure=True, samesite="Lax")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```expect
|
|
40
|
+
1:1 sec-py/cookie-flags
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### case: flags from variables
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
resp.set_cookie("a", "b", secure=secure, httponly=httponly, samesite=samesite)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```expect
|
|
50
|
+
(none)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### case: secure=False
|
|
54
|
+
|
|
55
|
+
```py
|
|
56
|
+
resp.set_cookie("a", "b", secure=False, httponly=True, samesite="Lax")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```expect
|
|
60
|
+
1:1 sec-py/cookie-flags
|
|
61
|
+
```
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sec-py/cookie-flags
|
|
3
|
+
title: 'Cookies are `Secure`, `HttpOnly` and `SameSite`'
|
|
4
|
+
languages: [python]
|
|
5
|
+
exclude: ["**/tests/**", "**/test/**", "**/test_*.py", "**/*_test.py", "**/*_tests.py", "**/tests.py", "**/conftest.py"]
|
|
6
|
+
fix: suggest
|
|
7
|
+
fix_reason: "`secure=True` stops the cookie being set over plain HTTP in development, and `HttpOnly` hides it from scripts that read it on purpose"
|
|
8
|
+
stability: preview
|
|
9
|
+
security: { cwe: [614, 1004], owasp: ["A02:2025"], severity: medium }
|
|
10
|
+
since: 0.2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
`set_cookie` passes `secure`, `httponly` and `samesite`, and none of them is `False` or `None`.
|
|
14
|
+
|
|
15
|
+
**Why:** Without `Secure` the cookie travels over plain HTTP, without `HttpOnly` a script can read it, and without `SameSite` other sites can send it along (CWE-614, CWE-1004).
|
|
16
|
+
|
|
17
|
+
```codelaw
|
|
18
|
+
match:
|
|
19
|
+
any:
|
|
20
|
+
- all:
|
|
21
|
+
- { pattern: '$R.set_cookie($$$A)' }
|
|
22
|
+
- not:
|
|
23
|
+
has:
|
|
24
|
+
field: arguments
|
|
25
|
+
has: { kind: keyword_argument, has: { field: name, regex: '^secure$' } }
|
|
26
|
+
- not:
|
|
27
|
+
has:
|
|
28
|
+
field: arguments
|
|
29
|
+
has: { kind: keyword_argument, has: { field: name, regex: '^httponly$' } }
|
|
30
|
+
- not:
|
|
31
|
+
has:
|
|
32
|
+
field: arguments
|
|
33
|
+
has: { kind: keyword_argument, has: { field: name, regex: '^samesite$' } }
|
|
34
|
+
- all:
|
|
35
|
+
- { kind: call }
|
|
36
|
+
- { has: { field: function, regex: '\.set_cookie$' } }
|
|
37
|
+
- any:
|
|
38
|
+
- not:
|
|
39
|
+
has:
|
|
40
|
+
field: arguments
|
|
41
|
+
has: { kind: keyword_argument, has: { field: name, regex: '^secure$' } }
|
|
42
|
+
- not:
|
|
43
|
+
has:
|
|
44
|
+
field: arguments
|
|
45
|
+
has: { kind: keyword_argument, has: { field: name, regex: '^httponly$' } }
|
|
46
|
+
- not:
|
|
47
|
+
has:
|
|
48
|
+
field: arguments
|
|
49
|
+
has: { kind: keyword_argument, has: { field: name, regex: '^samesite$' } }
|
|
50
|
+
- has:
|
|
51
|
+
field: arguments
|
|
52
|
+
has:
|
|
53
|
+
kind: keyword_argument
|
|
54
|
+
all: [{ has: { field: name, regex: '^samesite$' } }, { has: { field: value, kind: none } }]
|
|
55
|
+
- has:
|
|
56
|
+
field: arguments
|
|
57
|
+
has:
|
|
58
|
+
kind: keyword_argument
|
|
59
|
+
all: [{ has: { field: name, regex: '^secure$' } }, { has: { field: value, kind: 'false' } }]
|
|
60
|
+
- has:
|
|
61
|
+
field: arguments
|
|
62
|
+
has:
|
|
63
|
+
kind: keyword_argument
|
|
64
|
+
all: [{ has: { field: name, regex: '^httponly$' } }, { has: { field: value, kind: 'false' } }]
|
|
65
|
+
message: 'This cookie is missing `Secure`, `HttpOnly` or `SameSite`'
|
|
66
|
+
fix: '$R.set_cookie($$$A, secure=True, httponly=True, samesite="Lax")'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## How to fix
|
|
70
|
+
|
|
71
|
+
1. Add `secure=True, httponly=True, samesite="Lax"`.
|
|
72
|
+
2. Use `samesite="Strict"` for a session cookie; `None` needs `secure=True` and a reason.
|
|
73
|
+
3. A cookie a script must read can omit `httponly`; say so in a comment.
|
|
74
|
+
|
|
75
|
+
## Examples
|
|
76
|
+
|
|
77
|
+
### Python
|
|
78
|
+
|
|
79
|
+
#### Don't
|
|
80
|
+
|
|
81
|
+
```py
|
|
82
|
+
resp.set_cookie("sid", sid)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### Do
|
|
86
|
+
|
|
87
|
+
```py
|
|
88
|
+
resp.set_cookie("sid", sid, secure=True, httponly=True, samesite="Lax")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Fixed
|
|
92
|
+
|
|
93
|
+
```py
|
|
94
|
+
resp.set_cookie("sid", sid, secure=True, httponly=True, samesite="Lax")
|
|
95
|
+
```
|