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,54 @@
1
+ ---
2
+ id: sec-py/no-weak-hash
3
+ title: 'No MD5 or SHA-1 for security'
4
+ languages: [python]
5
+ fix: suggest
6
+ fix_reason: "whether the digest is used for security (use SHA-256) or only as a checksum (say so) is the author's call"
7
+ stability: preview
8
+ ported_from: [bandit/B303, bandit/B324, ruff/S324]
9
+ security: { cwe: [327], owasp: ["A04:2025"], severity: medium }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ `hashlib.md5` and `hashlib.sha1` pass `usedforsecurity=False`, or are replaced by SHA-256.
14
+
15
+ **Why:** Both can be forged by collision, so they prove nothing about who wrote the data (CWE-327).
16
+
17
+ ```codelaw
18
+ match:
19
+ all:
20
+ - { pattern: 'hashlib.$H($$$A)' }
21
+ - not:
22
+ has:
23
+ field: arguments
24
+ has: { kind: keyword_argument, has: { field: name, regex: '^usedforsecurity$' } }
25
+ constraints: { H: { regex: '^(md5|sha1)$' } }
26
+ message: '`hashlib.$H` is not collision resistant; use SHA-256, or pass `usedforsecurity=False` for a checksum'
27
+ fix: 'hashlib.sha256($$$A)'
28
+ ```
29
+
30
+ ## How to fix
31
+
32
+ 1. If the digest guards integrity or identity, use `hashlib.sha256` or `hashlib.blake2b`.
33
+ 2. If it is only a cache key or checksum, pass `usedforsecurity=False` and say so in a comment.
34
+ 3. For passwords see `sec-py/password-hash-effort`.
35
+
36
+ ## Examples
37
+
38
+ ### Python
39
+
40
+ #### Don't
41
+
42
+ ```py
43
+ import hashlib
44
+
45
+ digest = hashlib.md5(data).hexdigest()
46
+ ```
47
+
48
+ #### Do
49
+
50
+ ```py
51
+ import hashlib
52
+
53
+ digest = hashlib.sha256(data).hexdigest()
54
+ ```
@@ -0,0 +1,41 @@
1
+ # sec-py/no-wildcard-injection — cases
2
+
3
+ ### case: tar *
4
+
5
+ ```py
6
+ os.system("tar cf a.tar *")
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-wildcard-injection
11
+ ```
12
+
13
+ ### case: chmod *
14
+
15
+ ```py
16
+ subprocess.run("chmod 600 *", shell=True)
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/no-wildcard-injection
21
+ ```
22
+
23
+ ### case: ls *
24
+
25
+ ```py
26
+ os.system("ls *")
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
32
+
33
+ ### case: a list
34
+
35
+ ```py
36
+ subprocess.run(["tar", "cf", "a"])
37
+ ```
38
+
39
+ ```expect
40
+ (none)
41
+ ```
@@ -0,0 +1,53 @@
1
+ ---
2
+ id: sec-py/no-wildcard-injection
3
+ title: 'A shell wildcard after `tar`, `chown`, `chmod` or `rsync`'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "the files have to be listed (`glob`) and passed after `--`, which changes the call"
7
+ stability: preview
8
+ ported_from: [bandit/B609, ruff/S609]
9
+ security: { cwe: [155], owasp: ["A05:2025"], severity: medium }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ A command that runs `tar`, `chown`, `chmod` or `rsync` on `*` is not started through a shell.
14
+
15
+ **Why:** A file named `--checkpoint-action=exec=sh x.sh` in the folder is expanded by the shell into an option, so whoever can create files there runs commands (CWE-155).
16
+
17
+ ```codelaw
18
+ match:
19
+ kind: call
20
+ all:
21
+ - has:
22
+ field: function
23
+ regex: '^(os\.system|os\.popen|subprocess\.(run|call|check_call|check_output|Popen))$'
24
+ - has:
25
+ field: arguments
26
+ has: { nthChild: 1, kind: string, regex: '\b(tar|chown|chmod|rsync)\b[^''\"]*\*' }
27
+ message: '`*` is expanded by the shell, so a file named like an option changes the command'
28
+ ```
29
+
30
+ ## How to fix
31
+
32
+ 1. List the files in Python: `files = sorted(glob.glob("*.txt"))`.
33
+ 2. Pass them as a list after `--`: `subprocess.run(["tar", "cf", out, "--", *files])`.
34
+
35
+ ## Examples
36
+
37
+ ### Python
38
+
39
+ #### Don't
40
+
41
+ ```py
42
+ import os
43
+
44
+ os.system("tar cf backup.tar *")
45
+ ```
46
+
47
+ #### Do
48
+
49
+ ```py
50
+ import subprocess
51
+
52
+ subprocess.run(["tar", "cf", "backup.tar", "--", *files], check=True)
53
+ ```
@@ -0,0 +1,51 @@
1
+ # sec-py/no-world-writable — cases
2
+
3
+ ### case: 777
4
+
5
+ ```py
6
+ os.chmod(path, 0o777)
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-world-writable
11
+ ```
12
+
13
+ ### case: 666
14
+
15
+ ```py
16
+ os.chmod(path, 0o666)
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/no-world-writable
21
+ ```
22
+
23
+ ### case: 600
24
+
25
+ ```py
26
+ os.chmod(path, 0o600)
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
32
+
33
+ ### case: 755
34
+
35
+ ```py
36
+ os.chmod(path, 0o755)
37
+ ```
38
+
39
+ ```expect
40
+ (none)
41
+ ```
42
+
43
+ ### case: S_IWOTH
44
+
45
+ ```py
46
+ os.chmod(path, stat.S_IWOTH)
47
+ ```
48
+
49
+ ```expect
50
+ 1:1 sec-py/no-world-writable
51
+ ```
@@ -0,0 +1,57 @@
1
+ ---
2
+ id: sec-py/no-world-writable
3
+ title: 'No world-writable files'
4
+ languages: [python]
5
+ fix: suggest
6
+ fix_reason: "the right mode depends on who has to write the file"
7
+ stability: preview
8
+ ported_from: [bandit/B103, ruff/S103]
9
+ security: { cwe: [732], owasp: ["A01:2025"], severity: medium }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ `os.chmod` and `os.umask` do not give write access to everyone.
14
+
15
+ **Why:** Any local user can then change the file, which for a script, config or socket means running code as its owner (CWE-732).
16
+
17
+ ```codelaw
18
+ match: { all: [{ pattern: 'os.chmod($P, $M)' }] }
19
+ constraints:
20
+ M:
21
+ regex: '^(0o?[0-7]*[2367]|0o?[0-7]*7[0-7]?[2367]|stat\.S_IWOTH|stat\.S_IRWXO|0o?[0-7]{0,2}77[0-7]|0o?[0-7]*[2367])$'
22
+ message: '`$M` makes the file writable by everyone'
23
+ fix: 'os.chmod($P, 0o600)'
24
+ ```
25
+
26
+ ## How to fix
27
+
28
+ 1. Use `0o600` for a private file, `0o644` for one others read, `0o755` for a program.
29
+ 2. Never set the others' write bit.
30
+
31
+ ## Examples
32
+
33
+ ### Python
34
+
35
+ #### Don't
36
+
37
+ ```py
38
+ import os
39
+
40
+ os.chmod(path, 0o777)
41
+ ```
42
+
43
+ #### Do
44
+
45
+ ```py
46
+ import os
47
+
48
+ os.chmod(path, 0o600)
49
+ ```
50
+
51
+ #### Fixed
52
+
53
+ ```py
54
+ import os
55
+
56
+ os.chmod(path, 0o600)
57
+ ```
@@ -0,0 +1,83 @@
1
+ # sec-py/no-xml-unsafe-parser — cases
2
+
3
+ ### case: ElementTree through an alias
4
+
5
+ ```py
6
+ import xml.etree.ElementTree as ET
7
+
8
+ ET.fromstring(text)
9
+ ```
10
+
11
+ ```expect
12
+ 3:1 sec-py/no-xml-unsafe-parser
13
+ ```
14
+
15
+ ### case: ElementTree imported from xml.etree
16
+
17
+ ```py
18
+ from xml.etree import ElementTree
19
+
20
+ ElementTree.parse(p)
21
+ ```
22
+
23
+ ```expect
24
+ 3:1 sec-py/no-xml-unsafe-parser
25
+ ```
26
+
27
+ ### case: defusedxml
28
+
29
+ ```py
30
+ import defusedxml.ElementTree as ET
31
+
32
+ ET.parse(p)
33
+ ```
34
+
35
+ ```expect
36
+ (none)
37
+ ```
38
+
39
+ ### case: minidom
40
+
41
+ ```py
42
+ from xml.dom import minidom
43
+
44
+ minidom.parseString(text)
45
+ ```
46
+
47
+ ```expect
48
+ 3:1 sec-py/no-xml-unsafe-parser
49
+ ```
50
+
51
+ ### case: lxml
52
+
53
+ ```py
54
+ from lxml import etree
55
+
56
+ etree.fromstring(text)
57
+ ```
58
+
59
+ ```expect
60
+ 3:1 sec-py/no-xml-unsafe-parser
61
+ ```
62
+
63
+ ### case: an unrelated etree
64
+
65
+ ```py
66
+ from mylib import etree
67
+
68
+ etree.parse(text)
69
+ ```
70
+
71
+ ```expect
72
+ (none)
73
+ ```
74
+
75
+ ### case: an unrelated parse
76
+
77
+ ```py
78
+ parser.parse(text)
79
+ ```
80
+
81
+ ```expect
82
+ (none)
83
+ ```
@@ -0,0 +1,86 @@
1
+ ---
2
+ id: sec-py/no-xml-unsafe-parser
3
+ title: 'XML is parsed with `defusedxml`'
4
+ languages: [python]
5
+ fix: suggest
6
+ fix_reason: "`defusedxml` has to be installed and imported, and it refuses documents the standard parser accepts"
7
+ stability: preview
8
+ ported_from: [bandit/B313, bandit/B314, bandit/B315, bandit/B316, bandit/B317, bandit/B318, bandit/B319, bandit/B320, bandit/B405, bandit/B406, bandit/B407, bandit/B408, bandit/B409, bandit/B410]
9
+ security: { cwe: [611], owasp: ["A05:2025"], severity: high }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ XML from outside the program is parsed with `defusedxml`, not with `xml.etree`, `minidom`, `sax` or `lxml`.
14
+
15
+ **Why:** The standard parsers expand entities, so a crafted document can read local files, reach the network or exhaust memory (CWE-611).
16
+
17
+ ```codelaw
18
+ match:
19
+ any:
20
+ - all:
21
+ - { pattern: '$M.$F($$$A)' }
22
+ - has:
23
+ field: function
24
+ has:
25
+ field: object
26
+ regex: '^(ET|ElementTree|cElementTree|xml\.etree\.(c)?ElementTree)$'
27
+ pattern: $ET
28
+ - { pattern: '$M.$F($$$A)' }
29
+ constraints:
30
+ F:
31
+ regex: '^(parse|parseString|iterparse|fromstring|fromstringlist|XML|XMLParser|make_parser)$'
32
+ where:
33
+ $M:
34
+ any:
35
+ - imported-from:
36
+ - xml.etree.ElementTree
37
+ - xml.etree.cElementTree
38
+ - xml.dom.minidom
39
+ - xml.dom.pulldom
40
+ - xml.sax
41
+ - xml.dom.expatbuilder
42
+ - lxml.etree
43
+ - { imported-from: { module: xml.etree, name: ElementTree } }
44
+ - { imported-from: { module: xml.etree, name: cElementTree } }
45
+ - { imported-from: { module: xml.dom, name: minidom } }
46
+ - { imported-from: { module: xml.dom, name: pulldom } }
47
+ - { imported-from: { module: xml, name: sax } }
48
+ - { imported-from: { module: lxml, name: etree } }
49
+ message: 'This parser expands XML entities; use `defusedxml`'
50
+ fix:
51
+ - { replace: { target: $ET, with: defusedxml.ElementTree } }
52
+ ```
53
+
54
+ ## How to fix
55
+
56
+ 1. Install `defusedxml` and import it: `import defusedxml.ElementTree as ET`.
57
+ 2. Call the same function on it: `ET.fromstring(text)`.
58
+ 3. For `minidom`, `sax` or `pulldom`, import the module of the same name from `defusedxml`; for `lxml`, build the parser with `resolve_entities=False, no_network=True`.
59
+
60
+ ## Examples
61
+
62
+ ### Python
63
+
64
+ #### Don't
65
+
66
+ ```py
67
+ import xml.etree.ElementTree as ET
68
+
69
+ tree = ET.parse(path)
70
+ ```
71
+
72
+ #### Do
73
+
74
+ ```py
75
+ import defusedxml.ElementTree as ET
76
+
77
+ tree = ET.parse(path)
78
+ ```
79
+
80
+ #### Fixed
81
+
82
+ ```py
83
+ import xml.etree.ElementTree as ET
84
+
85
+ tree = defusedxml.ElementTree.parse(path)
86
+ ```
@@ -0,0 +1,31 @@
1
+ # sec-py/no-xmlrpc — cases
2
+
3
+ ### case: ServerProxy
4
+
5
+ ```py
6
+ xmlrpc.client.ServerProxy(url)
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-xmlrpc
11
+ ```
12
+
13
+ ### case: a server
14
+
15
+ ```py
16
+ SimpleXMLRPCServer(("localhost", 8000))
17
+ ```
18
+
19
+ ```expect
20
+ 1:1 sec-py/no-xmlrpc
21
+ ```
22
+
23
+ ### case: requests
24
+
25
+ ```py
26
+ requests.post(url)
27
+ ```
28
+
29
+ ```expect
30
+ (none)
31
+ ```
@@ -0,0 +1,50 @@
1
+ ---
2
+ id: sec-py/no-xmlrpc
3
+ title: 'No `xmlrpc` without `defusedxml`'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "the patch has to run once before any xmlrpc object exists, and where that is depends on the program's start-up"
7
+ stability: preview
8
+ ported_from: [bandit/B411, ruff/S411]
9
+ security: { cwe: [776], owasp: ["A05:2025"], severity: medium }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ An `xmlrpc` client or server is not created without `defusedxml.xmlrpc.monkey_patch()` in the program.
14
+
15
+ **Why:** `xmlrpc` parses XML with the standard parser, so a crafted request or response carries an entity expansion bomb or reads files (CWE-776).
16
+
17
+ ```codelaw
18
+ match:
19
+ kind: call
20
+ all:
21
+ - has:
22
+ field: function
23
+ regex: '^(xmlrpc\.(client|server)\.)?(ServerProxy|Server|SimpleXMLRPCServer|SimpleXMLRPCRequestHandler|MultiCall|DocXMLRPCServer|CGIXMLRPCRequestHandler)$'
24
+ message: '`xmlrpc` parses XML unsafely; call `defusedxml.xmlrpc.monkey_patch()` at start-up, or use JSON'
25
+ ```
26
+
27
+ ## How to fix
28
+
29
+ 1. Add `import defusedxml.xmlrpc` and call `defusedxml.xmlrpc.monkey_patch()` once, before any `xmlrpc` object is made.
30
+ 2. Or move the API to JSON over HTTP.
31
+
32
+ ## Examples
33
+
34
+ ### Python
35
+
36
+ #### Don't
37
+
38
+ ```py
39
+ from xmlrpc.client import ServerProxy
40
+
41
+ proxy = ServerProxy(url)
42
+ ```
43
+
44
+ #### Do
45
+
46
+ ```py
47
+ import requests
48
+
49
+ resp = requests.post(url, json=payload, timeout=10)
50
+ ```
@@ -0,0 +1,41 @@
1
+ # sec-py/no-xpath-injection — cases
2
+
3
+ ### case: an f-string
4
+
5
+ ```py
6
+ tree.xpath(f"//user[@name='{name}']")
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/no-xpath-injection
11
+ ```
12
+
13
+ ### case: variables
14
+
15
+ ```py
16
+ tree.xpath("//user[@name=$n]", n=name)
17
+ ```
18
+
19
+ ```expect
20
+ (none)
21
+ ```
22
+
23
+ ### case: a concatenation
24
+
25
+ ```py
26
+ root.findall("./user[@id='" + uid + "']")
27
+ ```
28
+
29
+ ```expect
30
+ 1:1 sec-py/no-xpath-injection
31
+ ```
32
+
33
+ ### case: a plain path
34
+
35
+ ```py
36
+ root.findall("./user")
37
+ ```
38
+
39
+ ```expect
40
+ (none)
41
+ ```
@@ -0,0 +1,52 @@
1
+ ---
2
+ id: sec-py/no-xpath-injection
3
+ title: 'XPath expressions are not built from strings'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "the expression has to be rewritten around `$name` variables, and which parts are values is the author's call"
7
+ stability: preview
8
+ security: { cwe: [643], owasp: ["A05:2025"], severity: high }
9
+ since: 0.2.0
10
+ ---
11
+
12
+ An XPath expression is not made with an f-string, `%`, `+` or `.format`; values go in as XPath variables.
13
+
14
+ **Why:** A quote in the value ends the string in the expression and the rest of the value is read as XPath (CWE-643).
15
+
16
+ ```codelaw
17
+ match:
18
+ kind: call
19
+ all:
20
+ - { has: { field: function, regex: '\.(xpath|findall|find|findtext|iterfind)$' } }
21
+ - has:
22
+ field: arguments
23
+ has:
24
+ nthChild: 1
25
+ any:
26
+ - { kind: string, has: { kind: interpolation } }
27
+ - { kind: binary_operator, has: { field: operator, regex: '^(%|\+)$' } }
28
+ - { kind: call, has: { field: function, regex: '\.format$' } }
29
+ regex: '(//|/|\[@)'
30
+ message: 'This XPath is built from a string; pass the values as variables'
31
+ ```
32
+
33
+ ## How to fix
34
+
35
+ 1. Write the expression with variables: `tree.xpath("//user[@name=$name]", name=name)` (lxml).
36
+ 2. ElementTree has no variables: select by a simple path and compare the attribute in Python.
37
+
38
+ ## Examples
39
+
40
+ ### Python
41
+
42
+ #### Don't
43
+
44
+ ```py
45
+ nodes = tree.xpath(f"//user[@name='{name}']")
46
+ ```
47
+
48
+ #### Do
49
+
50
+ ```py
51
+ nodes = tree.xpath("//user[@name=$name]", name=name)
52
+ ```
@@ -0,0 +1,21 @@
1
+ # sec-py/paramiko-host-key — cases
2
+
3
+ ### case: auto add
4
+
5
+ ```py
6
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
7
+ ```
8
+
9
+ ```expect
10
+ 1:1 sec-py/paramiko-host-key
11
+ ```
12
+
13
+ ### case: reject
14
+
15
+ ```py
16
+ client.set_missing_host_key_policy(paramiko.RejectPolicy())
17
+ ```
18
+
19
+ ```expect
20
+ (none)
21
+ ```
@@ -0,0 +1,44 @@
1
+ ---
2
+ id: sec-py/paramiko-host-key
3
+ title: 'SSH host keys are checked'
4
+ languages: [python]
5
+ fix: manual
6
+ fix_reason: "where the known host keys come from is deployment-specific"
7
+ stability: preview
8
+ ported_from: [bandit/B507, ruff/S507]
9
+ security: { cwe: [295], owasp: ["A07:2025"], severity: high }
10
+ since: 0.2.0
11
+ ---
12
+
13
+ `paramiko` is not told to accept unknown host keys with `AutoAddPolicy` or `WarningPolicy`.
14
+
15
+ **Why:** A connection to an impostor goes through unnoticed, and the credentials are handed to it (CWE-295).
16
+
17
+ ```codelaw
18
+ match: { any: [{ pattern: '$C.set_missing_host_key_policy($P)' }] }
19
+ constraints: { P: { regex: '(AutoAddPolicy|WarningPolicy)' } }
20
+ message: 'This accepts any SSH host key; load known hosts and reject unknown ones'
21
+ ```
22
+
23
+ ## How to fix
24
+
25
+ 1. Call `client.load_system_host_keys()` or `load_host_keys(path)`.
26
+ 2. Use `client.set_missing_host_key_policy(paramiko.RejectPolicy())`.
27
+ 3. Put the server's key in `known_hosts` ahead of time.
28
+
29
+ ## Examples
30
+
31
+ ### Python
32
+
33
+ #### Don't
34
+
35
+ ```py
36
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
37
+ ```
38
+
39
+ #### Do
40
+
41
+ ```py
42
+ client.load_system_host_keys()
43
+ client.set_missing_host_key_policy(paramiko.RejectPolicy())
44
+ ```