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.
Files changed (136) hide show
  1. codelaw_rules_security_python/LICENSE +23 -0
  2. codelaw_rules_security_python/NOTICE +19 -0
  3. codelaw_rules_security_python/README.md +239 -0
  4. codelaw_rules_security_python/codelaw-pack.toml +6 -0
  5. codelaw_rules_security_python/corpus.toml +65 -0
  6. codelaw_rules_security_python/data/sources/django.json +17 -0
  7. codelaw_rules_security_python/data/sources/fastapi.json +10 -0
  8. codelaw_rules_security_python/data/sources/flask.json +14 -0
  9. codelaw_rules_security_python/presets/sec-py/recommended.toml +60 -0
  10. codelaw_rules_security_python/presets/sec-py/strict.toml +8 -0
  11. codelaw_rules_security_python/rules/sec-py/cookie-flags.cases.md +61 -0
  12. codelaw_rules_security_python/rules/sec-py/cookie-flags.md +95 -0
  13. codelaw_rules_security_python/rules/sec-py/django-settings.cases.md +61 -0
  14. codelaw_rules_security_python/rules/sec-py/django-settings.md +68 -0
  15. codelaw_rules_security_python/rules/sec-py/fastapi-cors.cases.md +31 -0
  16. codelaw_rules_security_python/rules/sec-py/fastapi-cors.md +46 -0
  17. codelaw_rules_security_python/rules/sec-py/flask-debug.cases.md +31 -0
  18. codelaw_rules_security_python/rules/sec-py/flask-debug.md +59 -0
  19. codelaw_rules_security_python/rules/sec-py/flask-settings.cases.md +51 -0
  20. codelaw_rules_security_python/rules/sec-py/flask-settings.md +84 -0
  21. codelaw_rules_security_python/rules/sec-py/hf-pin-revision.cases.md +51 -0
  22. codelaw_rules_security_python/rules/sec-py/hf-pin-revision.md +58 -0
  23. codelaw_rules_security_python/rules/sec-py/jinja-autoescape.cases.md +62 -0
  24. codelaw_rules_security_python/rules/sec-py/jinja-autoescape.md +80 -0
  25. codelaw_rules_security_python/rules/sec-py/logging-listen.cases.md +31 -0
  26. codelaw_rules_security_python/rules/sec-py/logging-listen.md +47 -0
  27. codelaw_rules_security_python/rules/sec-py/ml-unsafe-load.cases.md +61 -0
  28. codelaw_rules_security_python/rules/sec-py/ml-unsafe-load.md +68 -0
  29. codelaw_rules_security_python/rules/sec-py/no-assert-for-auth.cases.md +31 -0
  30. codelaw_rules_security_python/rules/sec-py/no-assert-for-auth.md +51 -0
  31. codelaw_rules_security_python/rules/sec-py/no-bind-all-interfaces.cases.md +41 -0
  32. codelaw_rules_security_python/rules/sec-py/no-bind-all-interfaces.md +75 -0
  33. codelaw_rules_security_python/rules/sec-py/no-cleartext-http.cases.md +41 -0
  34. codelaw_rules_security_python/rules/sec-py/no-cleartext-http.md +56 -0
  35. codelaw_rules_security_python/rules/sec-py/no-csrf-exempt.cases.md +49 -0
  36. codelaw_rules_security_python/rules/sec-py/no-csrf-exempt.md +47 -0
  37. codelaw_rules_security_python/rules/sec-py/no-dynamic-import.cases.md +35 -0
  38. codelaw_rules_security_python/rules/sec-py/no-dynamic-import.md +51 -0
  39. codelaw_rules_security_python/rules/sec-py/no-exec-eval.cases.md +51 -0
  40. codelaw_rules_security_python/rules/sec-py/no-exec-eval.md +56 -0
  41. codelaw_rules_security_python/rules/sec-py/no-format-string-injection.cases.md +35 -0
  42. codelaw_rules_security_python/rules/sec-py/no-format-string-injection.md +44 -0
  43. codelaw_rules_security_python/rules/sec-py/no-hardcoded-crypto-key.cases.md +51 -0
  44. codelaw_rules_security_python/rules/sec-py/no-hardcoded-crypto-key.md +91 -0
  45. codelaw_rules_security_python/rules/sec-py/no-hardcoded-secret.cases.md +61 -0
  46. codelaw_rules_security_python/rules/sec-py/no-hardcoded-secret.md +82 -0
  47. codelaw_rules_security_python/rules/sec-py/no-header-injection.cases.md +45 -0
  48. codelaw_rules_security_python/rules/sec-py/no-header-injection.md +55 -0
  49. codelaw_rules_security_python/rules/sec-py/no-insecure-protocol.cases.md +41 -0
  50. codelaw_rules_security_python/rules/sec-py/no-insecure-protocol.md +52 -0
  51. codelaw_rules_security_python/rules/sec-py/no-insecure-random.cases.md +41 -0
  52. codelaw_rules_security_python/rules/sec-py/no-insecure-random.md +67 -0
  53. codelaw_rules_security_python/rules/sec-py/no-insecure-tempfile.cases.md +41 -0
  54. codelaw_rules_security_python/rules/sec-py/no-insecure-tempfile.md +62 -0
  55. codelaw_rules_security_python/rules/sec-py/no-ldap-injection.cases.md +41 -0
  56. codelaw_rules_security_python/rules/sec-py/no-ldap-injection.md +51 -0
  57. codelaw_rules_security_python/rules/sec-py/no-llm-output-to-sink.cases.md +31 -0
  58. codelaw_rules_security_python/rules/sec-py/no-llm-output-to-sink.md +55 -0
  59. codelaw_rules_security_python/rules/sec-py/no-log-injection.cases.md +45 -0
  60. codelaw_rules_security_python/rules/sec-py/no-log-injection.md +52 -0
  61. codelaw_rules_security_python/rules/sec-py/no-mako.cases.md +37 -0
  62. codelaw_rules_security_python/rules/sec-py/no-mako.md +52 -0
  63. codelaw_rules_security_python/rules/sec-py/no-mark-safe.cases.md +51 -0
  64. codelaw_rules_security_python/rules/sec-py/no-mark-safe.md +54 -0
  65. codelaw_rules_security_python/rules/sec-py/no-mass-assignment.cases.md +55 -0
  66. codelaw_rules_security_python/rules/sec-py/no-mass-assignment.md +58 -0
  67. codelaw_rules_security_python/rules/sec-py/no-nosql-injection.cases.md +34 -0
  68. codelaw_rules_security_python/rules/sec-py/no-nosql-injection.md +56 -0
  69. codelaw_rules_security_python/rules/sec-py/no-open-redirect.cases.md +35 -0
  70. codelaw_rules_security_python/rules/sec-py/no-open-redirect.md +51 -0
  71. codelaw_rules_security_python/rules/sec-py/no-partial-path.cases.md +41 -0
  72. codelaw_rules_security_python/rules/sec-py/no-partial-path.md +61 -0
  73. codelaw_rules_security_python/rules/sec-py/no-path-traversal.cases.md +46 -0
  74. codelaw_rules_security_python/rules/sec-py/no-path-traversal.md +54 -0
  75. codelaw_rules_security_python/rules/sec-py/no-prompt-injection.cases.md +46 -0
  76. codelaw_rules_security_python/rules/sec-py/no-prompt-injection.md +65 -0
  77. codelaw_rules_security_python/rules/sec-py/no-pycrypto.cases.md +41 -0
  78. codelaw_rules_security_python/rules/sec-py/no-pycrypto.md +47 -0
  79. codelaw_rules_security_python/rules/sec-py/no-reflected-xss.cases.md +45 -0
  80. codelaw_rules_security_python/rules/sec-py/no-reflected-xss.md +56 -0
  81. codelaw_rules_security_python/rules/sec-py/no-regex-injection.cases.md +33 -0
  82. codelaw_rules_security_python/rules/sec-py/no-regex-injection.md +49 -0
  83. codelaw_rules_security_python/rules/sec-py/no-secret-in-log.cases.md +41 -0
  84. codelaw_rules_security_python/rules/sec-py/no-secret-in-log.md +55 -0
  85. codelaw_rules_security_python/rules/sec-py/no-shell.cases.md +55 -0
  86. codelaw_rules_security_python/rules/sec-py/no-shell.md +63 -0
  87. codelaw_rules_security_python/rules/sec-py/no-silent-except.cases.md +53 -0
  88. codelaw_rules_security_python/rules/sec-py/no-silent-except.md +54 -0
  89. codelaw_rules_security_python/rules/sec-py/no-sql-string.cases.md +91 -0
  90. codelaw_rules_security_python/rules/sec-py/no-sql-string.md +78 -0
  91. codelaw_rules_security_python/rules/sec-py/no-ssrf.cases.md +34 -0
  92. codelaw_rules_security_python/rules/sec-py/no-ssrf.md +54 -0
  93. codelaw_rules_security_python/rules/sec-py/no-stack-trace-response.cases.md +48 -0
  94. codelaw_rules_security_python/rules/sec-py/no-stack-trace-response.md +67 -0
  95. codelaw_rules_security_python/rules/sec-py/no-tls-bypass.cases.md +71 -0
  96. codelaw_rules_security_python/rules/sec-py/no-tls-bypass.md +66 -0
  97. codelaw_rules_security_python/rules/sec-py/no-trust-remote-code.cases.md +31 -0
  98. codelaw_rules_security_python/rules/sec-py/no-trust-remote-code.md +50 -0
  99. codelaw_rules_security_python/rules/sec-py/no-unsafe-deserialize.cases.md +63 -0
  100. codelaw_rules_security_python/rules/sec-py/no-unsafe-deserialize.md +51 -0
  101. codelaw_rules_security_python/rules/sec-py/no-weak-cipher.cases.md +51 -0
  102. codelaw_rules_security_python/rules/sec-py/no-weak-cipher.md +60 -0
  103. codelaw_rules_security_python/rules/sec-py/no-weak-hash.cases.md +41 -0
  104. codelaw_rules_security_python/rules/sec-py/no-weak-hash.md +54 -0
  105. codelaw_rules_security_python/rules/sec-py/no-wildcard-injection.cases.md +41 -0
  106. codelaw_rules_security_python/rules/sec-py/no-wildcard-injection.md +53 -0
  107. codelaw_rules_security_python/rules/sec-py/no-world-writable.cases.md +51 -0
  108. codelaw_rules_security_python/rules/sec-py/no-world-writable.md +57 -0
  109. codelaw_rules_security_python/rules/sec-py/no-xml-unsafe-parser.cases.md +83 -0
  110. codelaw_rules_security_python/rules/sec-py/no-xml-unsafe-parser.md +86 -0
  111. codelaw_rules_security_python/rules/sec-py/no-xmlrpc.cases.md +31 -0
  112. codelaw_rules_security_python/rules/sec-py/no-xmlrpc.md +50 -0
  113. codelaw_rules_security_python/rules/sec-py/no-xpath-injection.cases.md +41 -0
  114. codelaw_rules_security_python/rules/sec-py/no-xpath-injection.md +52 -0
  115. codelaw_rules_security_python/rules/sec-py/paramiko-host-key.cases.md +21 -0
  116. codelaw_rules_security_python/rules/sec-py/paramiko-host-key.md +44 -0
  117. codelaw_rules_security_python/rules/sec-py/password-hash-effort.cases.md +61 -0
  118. codelaw_rules_security_python/rules/sec-py/password-hash-effort.md +74 -0
  119. codelaw_rules_security_python/rules/sec-py/requests-timeout.cases.md +41 -0
  120. codelaw_rules_security_python/rules/sec-py/requests-timeout.md +63 -0
  121. codelaw_rules_security_python/rules/sec-py/tarfile-filter.cases.md +51 -0
  122. codelaw_rules_security_python/rules/sec-py/tarfile-filter.md +71 -0
  123. codelaw_rules_security_python/rules/sec-py/torch-load-weights-only.cases.md +41 -0
  124. codelaw_rules_security_python/rules/sec-py/torch-load-weights-only.md +73 -0
  125. codelaw_rules_security_python/rules/sec-py/upload-secure-filename.cases.md +41 -0
  126. codelaw_rules_security_python/rules/sec-py/upload-secure-filename.md +65 -0
  127. codelaw_rules_security_python/rules/sec-py/urlopen-scheme.cases.md +41 -0
  128. codelaw_rules_security_python/rules/sec-py/urlopen-scheme.md +54 -0
  129. codelaw_rules_security_python/rules/sec-py/weak-key-size.cases.md +51 -0
  130. codelaw_rules_security_python/rules/sec-py/weak-key-size.md +82 -0
  131. codelaw_rules_security_python/rules/sec-py/yaml-safe-load.cases.md +67 -0
  132. codelaw_rules_security_python/rules/sec-py/yaml-safe-load.md +51 -0
  133. codelaw_rules_security_python-0.3.0.dist-info/METADATA +249 -0
  134. codelaw_rules_security_python-0.3.0.dist-info/RECORD +136 -0
  135. codelaw_rules_security_python-0.3.0.dist-info/WHEEL +4 -0
  136. 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,10 @@
1
+ [
2
+ "request.query_params",
3
+ "request.path_params",
4
+ "request.headers",
5
+ "request.cookies",
6
+ "request.form",
7
+ "request.json",
8
+ "request.body",
9
+ "request.stream"
10
+ ]
@@ -0,0 +1,14 @@
1
+ [
2
+ "request.args",
3
+ "request.form",
4
+ "request.values",
5
+ "request.json",
6
+ "request.get_json",
7
+ "request.data",
8
+ "request.get_data",
9
+ "request.cookies",
10
+ "request.files",
11
+ "request.headers",
12
+ "request.view_args",
13
+ "request.stream"
14
+ ]
@@ -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
+ ```