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