narvy-cli 1.0.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 (93) hide show
  1. narvy/__init__.py +3 -0
  2. narvy/android/__init__.py +0 -0
  3. narvy/android/source_analyzer.py +268 -0
  4. narvy/android/split_bundle.py +475 -0
  5. narvy/android_rule_context.py +196 -0
  6. narvy/apk_memory_preflight.py +248 -0
  7. narvy/auth.py +40 -0
  8. narvy/ci_templates/bitbucket.yml +25 -0
  9. narvy/ci_templates/github.yml +60 -0
  10. narvy/ci_templates/gitlab.yml +32 -0
  11. narvy/cloud/__init__.py +0 -0
  12. narvy/cloud/aws_scan.py +1188 -0
  13. narvy/comment_filter.py +134 -0
  14. narvy/crypto_taint_lite.py +82 -0
  15. narvy/decompiler.py +337 -0
  16. narvy/doctor.py +244 -0
  17. narvy/host/__init__.py +0 -0
  18. narvy/host/audit.py +157 -0
  19. narvy/host/host_knowledge.py +982 -0
  20. narvy/host/lynis_bootstrap.py +400 -0
  21. narvy/host/lynis_parser.py +358 -0
  22. narvy/host/narvy_checks.py +789 -0
  23. narvy/host/report.py +69 -0
  24. narvy/host/ssh_exec.py +219 -0
  25. narvy/ios/__init__.py +0 -0
  26. narvy/ios/binary_analyzer.py +1201 -0
  27. narvy/ios/plist_checks.py +249 -0
  28. narvy/ios/source_analyzer.py +301 -0
  29. narvy/ios/third_party_filter.py +242 -0
  30. narvy/ios/trust_all_context.py +66 -0
  31. narvy/main.py +1983 -0
  32. narvy/native_hardening.py +503 -0
  33. narvy/reporter.py +151 -0
  34. narvy/rule_engine.py +57 -0
  35. narvy/rules/android/config.yml +77 -0
  36. narvy/rules/android/crypto.yml +34 -0
  37. narvy/rules/android/secrets.yml +161 -0
  38. narvy/rules/android/storage.yml +24 -0
  39. narvy/rules/android/webview.yml +23 -0
  40. narvy/rules/android.yml +219 -0
  41. narvy/rules/ios/objc/crypto.yml +56 -0
  42. narvy/rules/ios/objc/network.yml +67 -0
  43. narvy/rules/ios/objc/secrets.yml +79 -0
  44. narvy/rules/ios/objc/storage.yml +45 -0
  45. narvy/rules/ios/objc/webview.yml +45 -0
  46. narvy/rules/ios_swift.yml +327 -0
  47. narvy/rules/web/go.yml +2837 -0
  48. narvy/rules/web/java.yml +1576 -0
  49. narvy/rules/web/javascript.yml +3683 -0
  50. narvy/rules/web/kotlin.yml +413 -0
  51. narvy/rules/web/local/csharp_narvy/config.yml +61 -0
  52. narvy/rules/web/local/csharp_narvy/crypto.yml +64 -0
  53. narvy/rules/web/local/csharp_narvy/deserialization.yml +59 -0
  54. narvy/rules/web/local/csharp_narvy/injection.yml +122 -0
  55. narvy/rules/web/local/csharp_narvy/xxe.yml +48 -0
  56. narvy/rules/web/local/java_narvy/auth_jwt.yml +134 -0
  57. narvy/rules/web/local/java_narvy/deserialization.yml +108 -0
  58. narvy/rules/web/local/java_narvy/mybatis.yml +39 -0
  59. narvy/rules/web/local/java_narvy/snakeyaml.yml +34 -0
  60. narvy/rules/web/local/java_narvy/spring_authz.yml +32 -0
  61. narvy/rules/web/local/java_narvy/spring_config.yml +67 -0
  62. narvy/rules/web/local/java_narvy/spring_hardening.yml +291 -0
  63. narvy/rules/web/local/java_narvy/sqli.yml +235 -0
  64. narvy/rules/web/local/java_narvy/xxe.yml +212 -0
  65. narvy/rules/web/php.yml +1644 -0
  66. narvy/rules/web/python.yml +3967 -0
  67. narvy/rules/web/ruby.yml +703 -0
  68. narvy/rules/web/rust.yml +258 -0
  69. narvy/rules/web/secrets.yml +1420 -0
  70. narvy/rules/web/secrets_supplement.yml +383 -0
  71. narvy/sca/__init__.py +1 -0
  72. narvy/sca/android_deps.py +349 -0
  73. narvy/sca/ios_deps.py +578 -0
  74. narvy/sca/osv_client.py +617 -0
  75. narvy/sca/web_deps.py +955 -0
  76. narvy/scope_config.py +195 -0
  77. narvy/semgrep_engine.py +219 -0
  78. narvy/stack_protector_evidence.py +96 -0
  79. narvy/third_party_filter.py +211 -0
  80. narvy/uploader.py +92 -0
  81. narvy/weak_prng_context.py +270 -0
  82. narvy/web/__init__.py +0 -0
  83. narvy/web/nuclei_binary.py +105 -0
  84. narvy/web/scan_blocklist.py +96 -0
  85. narvy/web/scanner.py +842 -0
  86. narvy/web/source_analyzer.py +714 -0
  87. narvy/web/ssrf_guard.py +374 -0
  88. narvy_cli-1.0.0.dist-info/METADATA +165 -0
  89. narvy_cli-1.0.0.dist-info/RECORD +93 -0
  90. narvy_cli-1.0.0.dist-info/WHEEL +5 -0
  91. narvy_cli-1.0.0.dist-info/entry_points.txt +2 -0
  92. narvy_cli-1.0.0.dist-info/licenses/LICENSE +202 -0
  93. narvy_cli-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3967 @@
1
+ # Python security rules, authored by Narvy.
2
+ # Injection, deserialization, SSRF, XSS, path traversal, weak crypto, secrets.
3
+
4
+ rules:
5
+ - id: narvy.python.injection.os-popen-concat.os-popen-concat
6
+ message: 'Detected `os.popen()` called with a dynamically constructed command string. This is equivalent
7
+ to os.system() for command injection purposes (CWE-78). Use subprocess with a list and `shell=False`.
8
+
9
+ '
10
+ severity: ERROR
11
+ languages:
12
+ - python
13
+ metadata:
14
+ category: security
15
+ cwe:
16
+ - 'CWE-78: Improper Neutralization of Special Elements used in an OS Command (''OS Command Injection'')'
17
+ owasp:
18
+ - A03:2021 - Injection
19
+ subcategory:
20
+ - vuln
21
+ likelihood: HIGH
22
+ impact: HIGH
23
+ confidence: MEDIUM
24
+ patterns:
25
+ - pattern-either:
26
+ - pattern: os.popen("..." + $VAR)
27
+ - pattern: os.popen($VAR + "...")
28
+ - pattern: os.popen($VAR)
29
+ - pattern: os.popen(f"...{$VAR}...")
30
+ - pattern-not: os.popen("...")
31
+ - id: narvy.python.injection.os-system-concat.os-system-concat
32
+ message: 'Detected `os.system()` called with a string that includes a non-literal variable via concatenation
33
+ or f-string. If the variable originates from user input this enables OS command injection (CWE-78).
34
+ Use the `subprocess` module with a list of arguments and `shell=False` instead.
35
+
36
+ '
37
+ severity: ERROR
38
+ languages:
39
+ - python
40
+ metadata:
41
+ category: security
42
+ cwe:
43
+ - 'CWE-78: Improper Neutralization of Special Elements used in an OS Command (''OS Command Injection'')'
44
+ owasp:
45
+ - A03:2021 - Injection
46
+ subcategory:
47
+ - vuln
48
+ likelihood: HIGH
49
+ impact: HIGH
50
+ confidence: MEDIUM
51
+ patterns:
52
+ - pattern-either:
53
+ - pattern: os.system("..." + $VAR)
54
+ - pattern: os.system($VAR + "...")
55
+ - pattern: os.system($VAR)
56
+ - pattern: os.system(f"...{$VAR}...")
57
+ - pattern: os.system("...".format(..., $VAR, ...))
58
+ - pattern: os.system("..." % $VAR)
59
+ - pattern-not: os.system("...")
60
+ - id: narvy.python.injection.subprocess-shell-true-with-var.subprocess-shell-true-with-var
61
+ message: 'Detected subprocess call with `shell=True` and a dynamically constructed command. Using `shell=True`
62
+ with user input enables OS command injection (CWE-78). Pass a list of arguments with `shell=False`
63
+ instead.
64
+
65
+ '
66
+ severity: ERROR
67
+ languages:
68
+ - python
69
+ metadata:
70
+ category: security
71
+ cwe:
72
+ - 'CWE-78: Improper Neutralization of Special Elements used in an OS Command (''OS Command Injection'')'
73
+ owasp:
74
+ - A03:2021 - Injection
75
+ subcategory:
76
+ - vuln
77
+ likelihood: HIGH
78
+ impact: HIGH
79
+ confidence: MEDIUM
80
+ patterns:
81
+ - pattern-either:
82
+ - pattern: subprocess.run("..." + $VAR, ..., shell=True, ...)
83
+ - pattern: subprocess.run($VAR, ..., shell=True, ...)
84
+ - pattern: subprocess.run(f"...{$VAR}...", ..., shell=True, ...)
85
+ - pattern: subprocess.Popen("..." + $VAR, ..., shell=True, ...)
86
+ - pattern: subprocess.Popen($VAR, ..., shell=True, ...)
87
+ - pattern: subprocess.Popen(f"...{$VAR}...", ..., shell=True, ...)
88
+ - pattern: subprocess.call("..." + $VAR, ..., shell=True, ...)
89
+ - pattern: subprocess.call($VAR, ..., shell=True, ...)
90
+ - pattern: subprocess.check_output("..." + $VAR, ..., shell=True, ...)
91
+ - pattern: subprocess.check_output($VAR, ..., shell=True, ...)
92
+ - pattern-not: subprocess.run("...", ..., shell=True, ...)
93
+ - pattern-not: subprocess.Popen("...", ..., shell=True, ...)
94
+ - id: narvy.python.deserialization.yaml-load-unsafe.yaml-load-unsafe
95
+ message: 'Detected `yaml.load()` without an explicit Loader. Without a Loader, PyYAML defaults to FullLoader
96
+ (PyYAML >= 5.1) or an even more dangerous loader in older versions, both of which can deserialise
97
+ arbitrary Python objects when the input is untrusted (CWE-502). Use `yaml.safe_load()` for untrusted
98
+ input.
99
+
100
+ '
101
+ severity: ERROR
102
+ languages:
103
+ - python
104
+ metadata:
105
+ category: security
106
+ cwe:
107
+ - 'CWE-502: Deserialization of Untrusted Data'
108
+ owasp:
109
+ - A08:2021 - Software and Data Integrity Failures
110
+ subcategory:
111
+ - vuln
112
+ likelihood: HIGH
113
+ impact: HIGH
114
+ confidence: HIGH
115
+ patterns:
116
+ - pattern: yaml.load($DATA)
117
+ - pattern-not: yaml.load($DATA, Loader=yaml.SafeLoader)
118
+ - pattern-not: yaml.load($DATA, Loader=yaml.BaseLoader)
119
+ - pattern-not: yaml.safe_load(...)
120
+ - id: narvy.python.auth.django-set-password-without-validation.django-set-password-without-validation
121
+ message: '`set_password()` is called without a call to `django.contrib.auth.password_validation.validate_password()`
122
+ anywhere else in the same function, or in any other method of the same class. Django''s password strength
123
+ rules (`AUTH_PASSWORD_VALIDATORS` - minimum length, common-password check, similarity-to-user-attributes
124
+ check) are opt-in per call site, not enforced automatically by `set_password()` itself (CWE-521).
125
+ Call `validate_password(raw_password, user=$INSTANCE)` before `set_password()` and handle `ValidationError`.'
126
+ severity: WARNING
127
+ languages:
128
+ - python
129
+ metadata:
130
+ cwe:
131
+ - 'CWE-521: Weak Password Requirements'
132
+ owasp:
133
+ - A07:2021 - Identification and Authentication Failures
134
+ category: security
135
+ subcategory:
136
+ - audit
137
+ technology:
138
+ - python
139
+ - django
140
+ likelihood: LOW
141
+ impact: MEDIUM
142
+ confidence: LOW
143
+ vulnerability_class:
144
+ - Improper Authentication
145
+ patterns:
146
+ - pattern: $INSTANCE.set_password($RAW)
147
+ - pattern-not: $INSTANCE.set_password("...")
148
+ - pattern-not-inside: "def $FUNC(...):\n ...\n password_validation.validate_password(...)\n \
149
+ \ ...\n"
150
+ - pattern-not-inside: "def $FUNC(...):\n ...\n django.contrib.auth.password_validation.validate_password(...)\n\
151
+ \ ...\n"
152
+ - pattern-not-inside: "class $CLS(...):\n ...\n def $ANYMETHOD(...):\n ...\n password_validation.validate_password(...)\n\
153
+ \ ...\n ...\n"
154
+ - pattern-not-inside: "class $CLS(...):\n ...\n def $ANYMETHOD(...):\n ...\n django.contrib.auth.password_validation.validate_password(...)\n\
155
+ \ ...\n ...\n"
156
+ - id: narvy.python.code-injection.compile-exec-tainted.compile-exec-tainted
157
+ message: '`compile()` output is passed to `exec()`/`eval()` where the source text passed to `compile()`
158
+ is not a fixed literal. This is the same RCE risk as calling `eval()`/`exec()` directly on tainted
159
+ text (CWE-94), just split across two calls, which sometimes slips past naive eval/exec-only detectors.'
160
+ severity: ERROR
161
+ languages:
162
+ - python
163
+ metadata:
164
+ cwe:
165
+ - 'CWE-94: Improper Control of Generation of Code (''Code Injection'')'
166
+ owasp:
167
+ - A03:2021 - Injection
168
+ category: security
169
+ subcategory:
170
+ - vuln
171
+ technology:
172
+ - python
173
+ likelihood: MEDIUM
174
+ impact: HIGH
175
+ confidence: MEDIUM
176
+ vulnerability_class:
177
+ - Code Injection
178
+ patterns:
179
+ - pattern-either:
180
+ - pattern: '$CODE = compile($SRC, ...)
181
+
182
+ ...
183
+
184
+ exec($CODE)
185
+
186
+ '
187
+ - pattern: '$CODE = compile($SRC, ...)
188
+
189
+ ...
190
+
191
+ eval($CODE)
192
+
193
+ '
194
+ - pattern-not: '$CODE = compile("...", ...)
195
+
196
+ ...
197
+
198
+ exec($CODE)
199
+
200
+ '
201
+ - pattern-not: '$CODE = compile("...", ...)
202
+
203
+ ...
204
+
205
+ eval($CODE)
206
+
207
+ '
208
+ - id: narvy.python.code-injection.eval-format-string.eval-format-string
209
+ message: '`eval()` called on a dynamically built string (f-string, `%` formatting, or `.format()`) rather
210
+ than a fixed literal. Even if the immediate source is not obviously a request object, building code
211
+ text by interpolation is a strong code-injection smell (CWE-94) whenever any interpolated part is
212
+ not fully trusted. Prefer `ast.literal_eval` for literals, or refactor to avoid building code as text.'
213
+ severity: WARNING
214
+ languages:
215
+ - python
216
+ metadata:
217
+ cwe:
218
+ - 'CWE-94: Improper Control of Generation of Code (''Code Injection'')'
219
+ owasp:
220
+ - A03:2021 - Injection
221
+ category: security
222
+ subcategory:
223
+ - vuln
224
+ technology:
225
+ - python
226
+ likelihood: MEDIUM
227
+ impact: HIGH
228
+ confidence: LOW
229
+ vulnerability_class:
230
+ - Code Injection
231
+ patterns:
232
+ - pattern-either:
233
+ - pattern: eval(f"...{$VAR}...")
234
+ - pattern: eval("...".format($VAR))
235
+ - pattern: eval("..." % $VAR)
236
+ - pattern: exec(f"...{$VAR}...")
237
+ - pattern: exec("...".format($VAR))
238
+ - pattern: exec("..." % $VAR)
239
+ - id: narvy.python.code-injection.eval-on-request-data.eval-on-request-data
240
+ message: '`eval()` is called on data that originates from an HTTP request (Flask `request.*`, Django
241
+ `request.*`, or a function argument that is clearly user-controlled). `eval()` executes arbitrary
242
+ Python - this is full remote code execution (CWE-94). There is no safe way to sandbox `eval()` against
243
+ a malicious string; replace with `ast.literal_eval()` if only literals are expected, or a proper parser/allowlisted
244
+ DSL otherwise.'
245
+ severity: ERROR
246
+ languages:
247
+ - python
248
+ metadata:
249
+ cwe:
250
+ - 'CWE-94: Improper Control of Generation of Code (''Code Injection'')'
251
+ owasp:
252
+ - A03:2021 - Injection
253
+ category: security
254
+ subcategory:
255
+ - vuln
256
+ technology:
257
+ - python
258
+ - flask
259
+ - django
260
+ likelihood: HIGH
261
+ impact: HIGH
262
+ confidence: HIGH
263
+ vulnerability_class:
264
+ - Code Injection
265
+ patterns:
266
+ - pattern-either:
267
+ - pattern: eval(request.$KIND.$ATTR(...))
268
+ - pattern: eval(request.$KIND.$ATTR)
269
+ - pattern: eval(request.$KIND[$X])
270
+ - pattern: eval(request.$KIND.get(...))
271
+ - pattern: '$V = request.$KIND.$ATTR(...)
272
+
273
+ ...
274
+
275
+ eval($V)
276
+
277
+ '
278
+ - pattern: '$V = request.$KIND[$X]
279
+
280
+ ...
281
+
282
+ eval($V)
283
+
284
+ '
285
+ - metavariable-regex:
286
+ metavariable: $KIND
287
+ regex: ^(GET|POST|args|form|json|values|data|cookies|headers|params)$
288
+ - id: narvy.python.code-injection.exec-on-request-data.exec-on-request-data
289
+ message: '`exec()` is called on data that originates from an HTTP request. `exec()` runs arbitrary Python
290
+ statements - full remote code execution (CWE-94). Never execute request-derived strings as code.'
291
+ severity: ERROR
292
+ languages:
293
+ - python
294
+ metadata:
295
+ cwe:
296
+ - 'CWE-94: Improper Control of Generation of Code (''Code Injection'')'
297
+ owasp:
298
+ - A03:2021 - Injection
299
+ category: security
300
+ subcategory:
301
+ - vuln
302
+ technology:
303
+ - python
304
+ - flask
305
+ - django
306
+ likelihood: HIGH
307
+ impact: HIGH
308
+ confidence: HIGH
309
+ vulnerability_class:
310
+ - Code Injection
311
+ patterns:
312
+ - pattern-either:
313
+ - pattern: exec(request.$KIND.$ATTR(...))
314
+ - pattern: exec(request.$KIND.$ATTR)
315
+ - pattern: exec(request.$KIND[$X])
316
+ - pattern: exec(request.$KIND.get(...))
317
+ - pattern: '$V = request.$KIND.$ATTR(...)
318
+
319
+ ...
320
+
321
+ exec($V)
322
+
323
+ '
324
+ - pattern: '$V = request.$KIND[$X]
325
+
326
+ ...
327
+
328
+ exec($V)
329
+
330
+ '
331
+ - metavariable-regex:
332
+ metavariable: $KIND
333
+ regex: ^(GET|POST|args|form|json|values|data|cookies|headers|params)$
334
+ - id: narvy.python.command-injection.asyncio-subprocess-shell.asyncio-subprocess-shell
335
+ message: '`asyncio.create_subprocess_shell()` runs its argument through a real shell (like os.system).
336
+ If `$CMD` is built from a variable instead of a pure literal, this is OS command injection (CWE-78).
337
+ Use `asyncio.create_subprocess_exec()` with a list of arguments instead.'
338
+ severity: ERROR
339
+ languages:
340
+ - python
341
+ metadata:
342
+ cwe:
343
+ - 'CWE-78: Improper Neutralization of Special Elements used in an OS Command (''OS Command Injection'')'
344
+ owasp:
345
+ - A03:2021 - Injection
346
+ category: security
347
+ subcategory:
348
+ - vuln
349
+ technology:
350
+ - python
351
+ - asyncio
352
+ likelihood: HIGH
353
+ impact: HIGH
354
+ confidence: MEDIUM
355
+ vulnerability_class:
356
+ - Command Injection
357
+ patterns:
358
+ - pattern-either:
359
+ - pattern: asyncio.create_subprocess_shell($CMD)
360
+ - pattern: asyncio.create_subprocess_shell("..." + $VAR)
361
+ - pattern: asyncio.create_subprocess_shell(f"...{$VAR}...")
362
+ - pattern-not: asyncio.create_subprocess_shell("...")
363
+ - id: narvy.python.command-injection.os-exec-family-tainted.os-exec-family-tainted
364
+ message: '`os.$FN()` replaces the current process image with a command built from a non-literal argument.
365
+ If the argument (or one of its list elements) is influenced by external input, this is command injection
366
+ / arbitrary process execution (CWE-78, CWE-88). Validate against an allowlist of known binaries before
367
+ calling.'
368
+ severity: ERROR
369
+ languages:
370
+ - python
371
+ metadata:
372
+ cwe:
373
+ - 'CWE-78: Improper Neutralization of Special Elements used in an OS Command (''OS Command Injection'')'
374
+ - 'CWE-88: Improper Neutralization of Argument Delimiters in a Command (''Argument Injection'')'
375
+ owasp:
376
+ - A03:2021 - Injection
377
+ category: security
378
+ subcategory:
379
+ - vuln
380
+ technology:
381
+ - python
382
+ likelihood: MEDIUM
383
+ impact: HIGH
384
+ confidence: MEDIUM
385
+ vulnerability_class:
386
+ - Command Injection
387
+ patterns:
388
+ - pattern-either:
389
+ - pattern: os.execl($VAR, ...)
390
+ - pattern: os.execle($VAR, ...)
391
+ - pattern: os.execlp($VAR, ...)
392
+ - pattern: os.execv($VAR, ...)
393
+ - pattern: os.execve($VAR, ...)
394
+ - pattern: os.execvp($VAR, ...)
395
+ - pattern: os.spawnl(..., $VAR, ...)
396
+ - pattern: os.spawnv(..., $VAR, ...)
397
+ - pattern: os.posix_spawn($VAR, ...)
398
+ - pattern-not: os.$FN("...", ...)
399
+ - id: narvy.python.command-injection.paramiko-exec-command-tainted.paramiko-exec-command-tainted
400
+ message: '`paramiko` SSH client `exec_command()` invoked with a non-literal command string. The remote
401
+ shell will interpret this string, so any external input reaching `$CMD` is remote command injection
402
+ on the target host (CWE-78). Build the command from a fixed template and shell-quote any variable
403
+ parts with `shlex.quote()`.'
404
+ severity: ERROR
405
+ languages:
406
+ - python
407
+ metadata:
408
+ cwe:
409
+ - 'CWE-78: Improper Neutralization of Special Elements used in an OS Command (''OS Command Injection'')'
410
+ owasp:
411
+ - A03:2021 - Injection
412
+ category: security
413
+ subcategory:
414
+ - vuln
415
+ technology:
416
+ - python
417
+ - paramiko
418
+ likelihood: MEDIUM
419
+ impact: HIGH
420
+ confidence: MEDIUM
421
+ vulnerability_class:
422
+ - Command Injection
423
+ patterns:
424
+ - pattern-either:
425
+ - pattern: $CLIENT.exec_command($CMD)
426
+ - pattern: $CLIENT.exec_command("..." + $VAR)
427
+ - pattern: $CLIENT.exec_command(f"...{$VAR}...")
428
+ - pattern-not: $CLIENT.exec_command("...")
429
+ - metavariable-regex:
430
+ metavariable: $CLIENT
431
+ regex: (?i)(ssh|client)
432
+ - id: narvy.python.command-injection.shell-metacharacter-in-fstring-subprocess.shell-metacharacter-fstring-subprocess
433
+ message: 'subprocess call built via an f-string / % / .format() that embeds a variable directly into
434
+ the command text (rather than passing it as a separate argv element). Even without `shell=True`, if
435
+ this string is later split by shlex or passed to a shell, or if the call site is actually `shell=True`
436
+ elsewhere in this branch, unescaped shell metacharacters in the variable let an attacker chain commands.
437
+ Pass arguments as a list: `subprocess.run(["cmd", user_arg])`. CWE-78.'
438
+ severity: WARNING
439
+ languages:
440
+ - python
441
+ metadata:
442
+ cwe:
443
+ - 'CWE-78: Improper Neutralization of Special Elements used in an OS Command (''OS Command Injection'')'
444
+ owasp:
445
+ - A03:2021 - Injection
446
+ category: security
447
+ subcategory:
448
+ - vuln
449
+ technology:
450
+ - python
451
+ - subprocess
452
+ likelihood: MEDIUM
453
+ impact: HIGH
454
+ confidence: LOW
455
+ vulnerability_class:
456
+ - Command Injection
457
+ patterns:
458
+ - pattern-either:
459
+ - pattern: subprocess.run(f"...{$VAR}...", ...)
460
+ - pattern: subprocess.Popen(f"...{$VAR}...", ...)
461
+ - pattern: subprocess.call(f"...{$VAR}...", ...)
462
+ - pattern: subprocess.check_output(f"...{$VAR}...", ...)
463
+ - pattern: subprocess.check_call(f"...{$VAR}...", ...)
464
+ - id: narvy.python.command-injection.subprocess-list-arg-tainted.subprocess-list-arg-tainted
465
+ message: A value read from the HTTP request reaches a subprocess call as part of the argv list, with
466
+ no shell=True in sight and no allow-list check in between. List-form subprocess calls are often assumed
467
+ safe because there is no shell to interpret metacharacters, but that only protects against injecting
468
+ *additional* commands via `;`/`|`/backticks - it does nothing to stop the attacker from choosing which
469
+ flags or arguments reach the target binary, and if the tainted value is split into several argv elements
470
+ (e.g. via `.split(' ')`) the attacker effectively controls an arbitrary number of arguments to that
471
+ binary. CWE-78, CWE-88. Validate the value against a strict allow-list of expected tokens, or don't
472
+ let request data reach argv at all.
473
+ severity: ERROR
474
+ languages:
475
+ - python
476
+ metadata:
477
+ cwe:
478
+ - 'CWE-78: Improper Neutralization of Special Elements used in an OS Command (''OS Command Injection'')'
479
+ - 'CWE-88: Improper Neutralization of Argument Delimiters in a Command (''Argument Injection'')'
480
+ owasp:
481
+ - A03:2021 - Injection
482
+ category: security
483
+ subcategory:
484
+ - vuln
485
+ technology:
486
+ - python
487
+ - subprocess
488
+ - django
489
+ - flask
490
+ likelihood: HIGH
491
+ impact: HIGH
492
+ confidence: MEDIUM
493
+ vulnerability_class:
494
+ - Command Injection
495
+ references:
496
+ - https://cwe.mitre.org/data/definitions/78.html
497
+ - https://cwe.mitre.org/data/definitions/88.html
498
+ mode: taint
499
+ pattern-sources:
500
+ - patterns:
501
+ - pattern-either:
502
+ - pattern: request.POST[...]
503
+ - pattern: request.POST.get(...)
504
+ - pattern: request.GET[...]
505
+ - pattern: request.GET.get(...)
506
+ - pattern: request.args[...]
507
+ - pattern: request.args.get(...)
508
+ - pattern: request.form[...]
509
+ - pattern: request.form.get(...)
510
+ - pattern: request.values[...]
511
+ - pattern: request.values.get(...)
512
+ - pattern: request.json[...]
513
+ - pattern: request.json.get(...)
514
+ - pattern: request.data
515
+ pattern-sinks:
516
+ - patterns:
517
+ - pattern-either:
518
+ - pattern: subprocess.Popen($SINK, ...)
519
+ - pattern: subprocess.run($SINK, ...)
520
+ - pattern: subprocess.call($SINK, ...)
521
+ - pattern: subprocess.check_output($SINK, ...)
522
+ - pattern: subprocess.check_call($SINK, ...)
523
+ pattern-sanitizers:
524
+ - patterns:
525
+ - pattern: shlex.quote(...)
526
+ - patterns:
527
+ - pattern: int(...)
528
+ - id: narvy.python.cors.django-cors-allow-all-origins.django-cors-allow-all-origins
529
+ message: '`django-cors-headers` is configured with `CORS_ALLOW_ALL_ORIGINS = True` (or the deprecated
530
+ `CORS_ORIGIN_ALLOW_ALL = True`). Any website can now make cross-origin requests to this API from a
531
+ victim''s browser; if session cookies are also allowed (`CORS_ALLOW_CREDENTIALS = True`), this is
532
+ a full CSRF-equivalent hole on every endpoint. Set `CORS_ALLOWED_ORIGINS` to an explicit list instead.
533
+ CWE-942.'
534
+ severity: WARNING
535
+ languages:
536
+ - python
537
+ metadata:
538
+ cwe:
539
+ - 'CWE-942: Permissive Cross-domain Policy with Untrusted Domains'
540
+ owasp:
541
+ - A05:2021 - Security Misconfiguration
542
+ category: security
543
+ subcategory:
544
+ - vuln
545
+ technology:
546
+ - django
547
+ likelihood: MEDIUM
548
+ impact: MEDIUM
549
+ confidence: HIGH
550
+ vulnerability_class:
551
+ - Cookie Security
552
+ patterns:
553
+ - pattern-either:
554
+ - pattern: CORS_ALLOW_ALL_ORIGINS = True
555
+ - pattern: CORS_ORIGIN_ALLOW_ALL = True
556
+ - id: narvy.python.cors.fastapi-wildcard-with-credentials.fastapi-wildcard-with-credentials
557
+ message: FastAPI/Starlette `CORSMiddleware` is added with `allow_origins=["*"]` and `allow_credentials=True`.
558
+ Starlette will raise at request time for exactly this combination when it can, but variants that pass
559
+ a wildcard through `allow_origin_regex` slip past that guard and effectively allow any origin to make
560
+ credentialed requests. Use an explicit origin allowlist whenever `allow_credentials=True`. CWE-942.
561
+ severity: ERROR
562
+ languages:
563
+ - python
564
+ metadata:
565
+ cwe:
566
+ - 'CWE-942: Permissive Cross-domain Policy with Untrusted Domains'
567
+ owasp:
568
+ - A05:2021 - Security Misconfiguration
569
+ category: security
570
+ subcategory:
571
+ - vuln
572
+ technology:
573
+ - fastapi
574
+ - starlette
575
+ likelihood: MEDIUM
576
+ impact: HIGH
577
+ confidence: HIGH
578
+ vulnerability_class:
579
+ - Cookie Security
580
+ patterns:
581
+ - pattern-either:
582
+ - pattern: '$APP.add_middleware(CORSMiddleware, allow_origins=["*"], ..., allow_credentials=True,
583
+ ...)
584
+
585
+ '
586
+ - pattern: '$APP.add_middleware(CORSMiddleware, allow_credentials=True, ..., allow_origins=["*"],
587
+ ...)
588
+
589
+ '
590
+ - pattern: '$APP.add_middleware(CORSMiddleware, allow_origin_regex=".*", ..., allow_credentials=True,
591
+ ...)
592
+
593
+ '
594
+ - id: narvy.python.cors.flask-cors-wildcard-with-credentials.flask-cors-wildcard-with-credentials
595
+ message: flask-cors is configured with `origins="*"` (or unset, which defaults to "*") together with
596
+ `supports_credentials=True`. Per the Fetch/CORS spec browsers should reject this combination, but
597
+ some older or misconfigured setups still leak it, and the wildcard alone already allows any site to
598
+ read non-credentialed responses. Pin `origins=` to an explicit allowlist when `supports_credentials=True`.
599
+ CWE-942.
600
+ severity: ERROR
601
+ languages:
602
+ - python
603
+ metadata:
604
+ cwe:
605
+ - 'CWE-942: Permissive Cross-domain Policy with Untrusted Domains'
606
+ owasp:
607
+ - A05:2021 - Security Misconfiguration
608
+ category: security
609
+ subcategory:
610
+ - vuln
611
+ technology:
612
+ - flask
613
+ - flask-cors
614
+ likelihood: MEDIUM
615
+ impact: HIGH
616
+ confidence: HIGH
617
+ vulnerability_class:
618
+ - Cookie Security
619
+ patterns:
620
+ - pattern-either:
621
+ - pattern: CORS($APP, origins="*", ..., supports_credentials=True, ...)
622
+ - pattern: CORS($APP, supports_credentials=True, ..., origins="*", ...)
623
+ - pattern: CORS($APP, supports_credentials=True)
624
+ - id: narvy.python.cors.reflected-origin-header.reflected-origin-header
625
+ message: The response's `Access-Control-Allow-Origin` header is set to the value of the incoming `Origin`
626
+ request header verbatim, instead of to a fixed value or a value checked against an allowlist. This
627
+ effectively grants every origin CORS access - identical in effect to a wildcard, but easy to miss
628
+ in review because the header value looks "dynamic" rather than "*". CWE-942.
629
+ severity: ERROR
630
+ languages:
631
+ - python
632
+ metadata:
633
+ cwe:
634
+ - 'CWE-942: Permissive Cross-domain Policy with Untrusted Domains'
635
+ owasp:
636
+ - A05:2021 - Security Misconfiguration
637
+ category: security
638
+ subcategory:
639
+ - vuln
640
+ technology:
641
+ - python
642
+ - flask
643
+ - django
644
+ likelihood: MEDIUM
645
+ impact: HIGH
646
+ confidence: MEDIUM
647
+ vulnerability_class:
648
+ - Cookie Security
649
+ patterns:
650
+ - pattern-either:
651
+ - pattern: $RESP.headers["Access-Control-Allow-Origin"] = request.headers.get("Origin")
652
+ - pattern: $RESP.headers["Access-Control-Allow-Origin"] = request.headers["Origin"]
653
+ - pattern: $RESP["Access-Control-Allow-Origin"] = request.headers.get("Origin")
654
+ - pattern: $RESP["Access-Control-Allow-Origin"] = request.META.get("HTTP_ORIGIN")
655
+ - id: narvy.python.crypto.broken-cipher-algorithm.broken-cipher-algorithm
656
+ message: Broken or deprecated symmetric cipher '$ALGO'. DES (56-bit key) and RC2 are trivially brute-forceable
657
+ on modern hardware; RC4/ARC4 has known keystream biases; Blowfish's 64-bit block size is vulnerable
658
+ to birthday-bound attacks (SWEET32) on large ciphertexts. Use AES-256-GCM or ChaCha20-Poly1305. CWE-327.
659
+ severity: ERROR
660
+ languages:
661
+ - python
662
+ metadata:
663
+ cwe:
664
+ - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm'
665
+ owasp:
666
+ - A02:2021 - Cryptographic Failures
667
+ category: security
668
+ subcategory:
669
+ - vuln
670
+ technology:
671
+ - pycryptodome
672
+ - cryptography
673
+ likelihood: MEDIUM
674
+ impact: HIGH
675
+ confidence: HIGH
676
+ vulnerability_class:
677
+ - Cryptographic Issues
678
+ patterns:
679
+ - pattern-either:
680
+ - pattern: $ALGO.new(...)
681
+ - pattern: algorithms.$ALGO(...)
682
+ - metavariable-regex:
683
+ metavariable: $ALGO
684
+ regex: ^(DES|DES3|ARC2|ARC4|RC2|RC4|Blowfish|IDEA|CAST)$
685
+ - id: narvy.python.crypto.django-secret-key-env-fallback.django-secret-key-env-fallback
686
+ message: Django `SECRET_KEY` is set with a hardcoded fallback '$LITERAL' when the environment variable
687
+ is unset. SECRET_KEY is used for session signing, CSRF token derivation, password reset tokens, and
688
+ cryptographic signing of cookies. If the env var is missing in any deployment (e.g. local dev sneaking
689
+ into prod, CI), the SECRET_KEY reduces to a public constant and ALL session-signed data is forgeable.
690
+ ALWAYS fail fast in settings.py (`SECRET_KEY = os.environ['DJANGO_SECRET_KEY']` - KeyError on missing
691
+ - or raise ImproperlyConfigured).
692
+ severity: ERROR
693
+ languages:
694
+ - python
695
+ metadata:
696
+ cwe:
697
+ - 'CWE-321: Use of Hard-coded Cryptographic Key'
698
+ - 'CWE-798: Use of Hard-coded Credentials'
699
+ owasp:
700
+ - A02:2021 - Cryptographic Failures
701
+ - A07:2021 - Identification and Authentication Failures
702
+ category: security
703
+ subcategory:
704
+ - vuln
705
+ technology:
706
+ - django
707
+ - python
708
+ likelihood: HIGH
709
+ impact: HIGH
710
+ confidence: HIGH
711
+ vulnerability_class:
712
+ - Cryptographic Issues
713
+ - Hard-coded Credentials
714
+ patterns:
715
+ - pattern-either:
716
+ - pattern: 'SECRET_KEY = os.getenv($ENV, "$LITERAL")
717
+
718
+ '
719
+ - pattern: 'SECRET_KEY = os.environ.get($ENV, "$LITERAL")
720
+
721
+ '
722
+ - pattern: 'SECRET_KEY = os.environ.get($ENV) or "$LITERAL"
723
+
724
+ '
725
+ - pattern: 'SECRET_KEY = os.getenv($ENV) or "$LITERAL"
726
+
727
+ '
728
+ - metavariable-regex:
729
+ metavariable: $LITERAL
730
+ regex: ^[A-Za-z0-9 _!@#$%^&*()+=,.:?<>\[\]{}|/\\-]{1,64}$
731
+ - id: narvy.python.crypto.ecb-cipher-mode.ecb-cipher-mode
732
+ message: ECB cipher mode encrypts identical plaintext blocks to identical ciphertext blocks, leaking
733
+ structural patterns in the data (the classic "ECB penguin"). Use GCM (authenticated) or, at minimum,
734
+ CBC with a random IV and a separate MAC. CWE-327.
735
+ severity: ERROR
736
+ languages:
737
+ - python
738
+ metadata:
739
+ cwe:
740
+ - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm'
741
+ owasp:
742
+ - A02:2021 - Cryptographic Failures
743
+ category: security
744
+ subcategory:
745
+ - vuln
746
+ technology:
747
+ - pycryptodome
748
+ - cryptography
749
+ likelihood: MEDIUM
750
+ impact: HIGH
751
+ confidence: HIGH
752
+ vulnerability_class:
753
+ - Cryptographic Issues
754
+ patterns:
755
+ - pattern-either:
756
+ - pattern: $ALGO.new($KEY, $ALGO.MODE_ECB)
757
+ - pattern: Cipher($ALGO, modes.ECB(), ...)
758
+ - id: narvy.python.crypto.empty-or-literal-aes-key.empty-or-literal-aes-key
759
+ message: AES key is an empty byte string or a short string literal, not a securely generated key. This
760
+ is either a debug leftover or a hardcoded key; both defeat the cipher. Generate keys with os.urandom(32)
761
+ or secrets.token_bytes(32) and load from a secret store. CWE-321, CWE-1204.
762
+ severity: ERROR
763
+ languages:
764
+ - python
765
+ metadata:
766
+ cwe:
767
+ - 'CWE-321: Use of Hard-coded Cryptographic Key'
768
+ - 'CWE-1204: Generation of Weak Initialization Vector'
769
+ owasp:
770
+ - A02:2021 - Cryptographic Failures
771
+ category: security
772
+ subcategory:
773
+ - vuln
774
+ technology:
775
+ - pycryptodome
776
+ - cryptography
777
+ likelihood: MEDIUM
778
+ impact: HIGH
779
+ confidence: HIGH
780
+ vulnerability_class:
781
+ - Cryptographic Issues
782
+ - Hard-coded Credentials
783
+ patterns:
784
+ - pattern-either:
785
+ - pattern: AES.new(b"", ...)
786
+ - pattern: AES.new("", ...)
787
+ - pattern: AES.new(b"$LIT", ...)
788
+ - pattern: AES.new("$LIT", ...)
789
+ - id: narvy.python.crypto.flask-secret-key-env-fallback.flask-secret-key-env-fallback
790
+ message: Flask `SECRET_KEY` is set with a hardcoded fallback '$LITERAL' when the environment variable
791
+ is unset. Used for session-cookie signing and CSRF tokens. If the env var is missing the SECRET_KEY
792
+ reduces to a public constant and ALL session cookies can be forged. Fail fast at startup with `app.config['SECRET_KEY']
793
+ = os.environ['FLASK_SECRET']`.
794
+ severity: ERROR
795
+ languages:
796
+ - python
797
+ metadata:
798
+ cwe:
799
+ - 'CWE-321: Use of Hard-coded Cryptographic Key'
800
+ - 'CWE-798: Use of Hard-coded Credentials'
801
+ owasp:
802
+ - A02:2021 - Cryptographic Failures
803
+ - A07:2021 - Identification and Authentication Failures
804
+ category: security
805
+ subcategory:
806
+ - vuln
807
+ technology:
808
+ - flask
809
+ - python
810
+ likelihood: HIGH
811
+ impact: HIGH
812
+ confidence: HIGH
813
+ vulnerability_class:
814
+ - Cryptographic Issues
815
+ - Hard-coded Credentials
816
+ patterns:
817
+ - pattern-either:
818
+ - pattern: '$APP.config[$KEY] = os.getenv($ENV, "$LITERAL")
819
+
820
+ '
821
+ - pattern: '$APP.config[$KEY] = os.environ.get($ENV, "$LITERAL")
822
+
823
+ '
824
+ - pattern: '$APP.config[$KEY] = os.environ.get($ENV) or "$LITERAL"
825
+
826
+ '
827
+ - pattern: '$APP.config[$KEY] = os.getenv($ENV) or "$LITERAL"
828
+
829
+ '
830
+ - pattern: '$APP.config.update(..., SECRET_KEY=os.environ.get($ENV, "$LITERAL"), ...)
831
+
832
+ '
833
+ - pattern: '$APP.config.update(..., SECRET_KEY=os.getenv($ENV, "$LITERAL"), ...)
834
+
835
+ '
836
+ - metavariable-regex:
837
+ metavariable: $LITERAL
838
+ regex: ^[A-Za-z0-9 _!@#$%^&*()+=,.:?<>\[\]{}|/\\-]{1,64}$
839
+ - metavariable-regex:
840
+ metavariable: $KEY
841
+ regex: ^['"]SECRET_KEY['"]$
842
+ - id: narvy.python.crypto.hardcoded-env-fallback-to-cipher.hardcoded-env-fallback-to-cipher
843
+ message: Hardcoded fallback secret '$LITERAL' is passed as a symmetric cipher key when the env var is
844
+ unset. Defeats the entire crypto operation. Fail fast on missing env at startup and derive the key
845
+ via HKDF / PBKDF2 with a per-record salt.
846
+ severity: ERROR
847
+ languages:
848
+ - python
849
+ metadata:
850
+ cwe:
851
+ - 'CWE-321: Use of Hard-coded Cryptographic Key'
852
+ - 'CWE-798: Use of Hard-coded Credentials'
853
+ owasp:
854
+ - A02:2021 - Cryptographic Failures
855
+ category: security
856
+ subcategory:
857
+ - vuln
858
+ technology:
859
+ - python
860
+ - cryptography
861
+ - pycryptodome
862
+ likelihood: HIGH
863
+ impact: HIGH
864
+ confidence: HIGH
865
+ vulnerability_class:
866
+ - Cryptographic Issues
867
+ - Hard-coded Credentials
868
+ patterns:
869
+ - pattern-either:
870
+ - pattern: 'AES.new(os.getenv($ENV, "$LITERAL").encode(...), ...)
871
+
872
+ '
873
+ - pattern: 'AES.new(os.environ.get($ENV, "$LITERAL").encode(...), ...)
874
+
875
+ '
876
+ - pattern: 'AES.new((os.environ.get($ENV) or "$LITERAL").encode(...), ...)
877
+
878
+ '
879
+ - pattern: 'algorithms.AES(os.getenv($ENV, "$LITERAL").encode(...))
880
+
881
+ '
882
+ - pattern: 'algorithms.AES(os.environ.get($ENV, "$LITERAL").encode(...))
883
+
884
+ '
885
+ - pattern: '$KDF.derive(os.getenv($ENV, "$LITERAL").encode(...))
886
+
887
+ '
888
+ - pattern: '$KDF.derive(os.environ.get($ENV, "$LITERAL").encode(...))
889
+
890
+ '
891
+ - pattern: '$K = os.getenv($ENV, "$LITERAL")
892
+
893
+ ...
894
+
895
+ AES.new($K.encode(...), ...)
896
+
897
+ '
898
+ - pattern: '$K = os.environ.get($ENV, "$LITERAL")
899
+
900
+ ...
901
+
902
+ AES.new($K.encode(...), ...)
903
+
904
+ '
905
+ - metavariable-regex:
906
+ metavariable: $LITERAL
907
+ regex: ^[A-Za-z0-9 _!@#$%^&*()+=,.:?<>\[\]{}|/\\-]{1,64}$
908
+ - id: narvy.python.crypto.hardcoded-env-fallback-to-hash.hardcoded-env-fallback-to-hash
909
+ message: Hardcoded fallback secret '$LITERAL' is used when the environment variable is unset, and is
910
+ then fed into a cryptographic hash. If the env var is missing in any deployment, the hash input reduces
911
+ to a public constant. Use HKDF / PBKDF2 / scrypt / Argon2, and fail fast on missing env at startup.
912
+ severity: ERROR
913
+ languages:
914
+ - python
915
+ metadata:
916
+ cwe:
917
+ - 'CWE-321: Use of Hard-coded Cryptographic Key'
918
+ - 'CWE-798: Use of Hard-coded Credentials'
919
+ - 'CWE-916: Use of Password Hash With Insufficient Computational Effort'
920
+ owasp:
921
+ - A02:2021 - Cryptographic Failures
922
+ category: security
923
+ subcategory:
924
+ - vuln
925
+ technology:
926
+ - python
927
+ - hashlib
928
+ likelihood: HIGH
929
+ impact: HIGH
930
+ confidence: HIGH
931
+ vulnerability_class:
932
+ - Cryptographic Issues
933
+ - Hard-coded Credentials
934
+ patterns:
935
+ - pattern-either:
936
+ - pattern: 'hashlib.$ALGO(os.getenv($ENV, "$LITERAL").encode(...))
937
+
938
+ '
939
+ - pattern: 'hashlib.$ALGO(os.getenv($ENV, "$LITERAL").encode())
940
+
941
+ '
942
+ - pattern: 'hashlib.$ALGO(os.environ.get($ENV, "$LITERAL").encode(...))
943
+
944
+ '
945
+ - pattern: 'hashlib.$ALGO(os.environ.get($ENV, "$LITERAL").encode())
946
+
947
+ '
948
+ - pattern: 'hashlib.$ALGO((os.environ.get($ENV) or "$LITERAL").encode(...))
949
+
950
+ '
951
+ - pattern: 'hashlib.$ALGO((os.getenv($ENV) or "$LITERAL").encode(...))
952
+
953
+ '
954
+ - pattern: '$K = os.getenv($ENV, "$LITERAL")
955
+
956
+ ...
957
+
958
+ hashlib.$ALGO($K.encode(...))
959
+
960
+ '
961
+ - pattern: '$K = os.environ.get($ENV, "$LITERAL")
962
+
963
+ ...
964
+
965
+ hashlib.$ALGO($K.encode(...))
966
+
967
+ '
968
+ - pattern: '$K = os.environ.get($ENV) or "$LITERAL"
969
+
970
+ ...
971
+
972
+ hashlib.$ALGO($K.encode(...))
973
+
974
+ '
975
+ - pattern: '$K = os.getenv($ENV) or "$LITERAL"
976
+
977
+ ...
978
+
979
+ hashlib.$ALGO($K.encode(...))
980
+
981
+ '
982
+ - pattern: "$K = os.getenv($ENV)\nif not $K:\n $K = \"$LITERAL\"\n...\nhashlib.$ALGO($K.encode(...))\n"
983
+ - pattern: "$K = os.environ.get($ENV)\nif not $K:\n $K = \"$LITERAL\"\n...\nhashlib.$ALGO($K.encode(...))\n"
984
+ - metavariable-regex:
985
+ metavariable: $LITERAL
986
+ regex: ^[A-Za-z0-9 _!@#$%^&*()+=,.:?<>\[\]{}|/\\-]{1,64}$
987
+ - id: narvy.python.crypto.hardcoded-env-fallback-to-hmac.hardcoded-env-fallback-to-hmac
988
+ message: Hardcoded fallback secret '$LITERAL' is used as the HMAC key when the environment variable
989
+ is unset. Anyone reading this source can forge valid MACs. Fail fast on missing env at startup.
990
+ severity: ERROR
991
+ languages:
992
+ - python
993
+ metadata:
994
+ cwe:
995
+ - 'CWE-321: Use of Hard-coded Cryptographic Key'
996
+ - 'CWE-798: Use of Hard-coded Credentials'
997
+ owasp:
998
+ - A02:2021 - Cryptographic Failures
999
+ category: security
1000
+ subcategory:
1001
+ - vuln
1002
+ technology:
1003
+ - python
1004
+ - hmac
1005
+ likelihood: HIGH
1006
+ impact: HIGH
1007
+ confidence: HIGH
1008
+ vulnerability_class:
1009
+ - Cryptographic Issues
1010
+ - Hard-coded Credentials
1011
+ patterns:
1012
+ - pattern-either:
1013
+ - pattern: 'hmac.new(os.getenv($ENV, "$LITERAL").encode(...), ...)
1014
+
1015
+ '
1016
+ - pattern: 'hmac.new(os.environ.get($ENV, "$LITERAL").encode(...), ...)
1017
+
1018
+ '
1019
+ - pattern: 'hmac.new((os.environ.get($ENV) or "$LITERAL").encode(...), ...)
1020
+
1021
+ '
1022
+ - pattern: 'hmac.new((os.getenv($ENV) or "$LITERAL").encode(...), ...)
1023
+
1024
+ '
1025
+ - pattern: '$K = os.getenv($ENV, "$LITERAL")
1026
+
1027
+ ...
1028
+
1029
+ hmac.new($K.encode(...), ...)
1030
+
1031
+ '
1032
+ - pattern: '$K = os.environ.get($ENV, "$LITERAL")
1033
+
1034
+ ...
1035
+
1036
+ hmac.new($K.encode(...), ...)
1037
+
1038
+ '
1039
+ - pattern: '$K = os.environ.get($ENV) or "$LITERAL"
1040
+
1041
+ ...
1042
+
1043
+ hmac.new($K.encode(...), ...)
1044
+
1045
+ '
1046
+ - pattern: "$K = os.getenv($ENV)\nif not $K:\n $K = \"$LITERAL\"\n...\nhmac.new($K.encode(...),\
1047
+ \ ...)\n"
1048
+ - metavariable-regex:
1049
+ metavariable: $LITERAL
1050
+ regex: ^[A-Za-z0-9 _!@#$%^&*()+=,.:?<>\[\]{}|/\\-]{1,64}$
1051
+ - id: narvy.python.crypto.insecure-uuid4-substitute.insecure-uuid-from-weak-random
1052
+ message: A UUID-like identifier is being built from `random`/`os.urandom` seeded predictably or from
1053
+ `uuid.uuid1` (which embeds the MAC address and a timestamp - both guessable/enumerable). For anything
1054
+ used as a security token (session id, password-reset id, API key), use `uuid.uuid4()` backed by `os.urandom`,
1055
+ or `secrets.token_urlsafe()`. CWE-330.
1056
+ severity: WARNING
1057
+ languages:
1058
+ - python
1059
+ metadata:
1060
+ cwe:
1061
+ - 'CWE-330: Use of Insufficiently Random Values'
1062
+ owasp:
1063
+ - A02:2021 - Cryptographic Failures
1064
+ category: security
1065
+ subcategory:
1066
+ - vuln
1067
+ technology:
1068
+ - python
1069
+ likelihood: LOW
1070
+ impact: MEDIUM
1071
+ confidence: MEDIUM
1072
+ vulnerability_class:
1073
+ - Cryptographic Issues
1074
+ patterns:
1075
+ - pattern: uuid.uuid1(...)
1076
+ - id: narvy.python.crypto.md5-as-password-hash.md5-as-password-hash
1077
+ message: 'MD5 digest used to hash what looks like a password/credential value. MD5 is fast to brute-force
1078
+ with commodity GPUs and has no built-in salt. Use a slow, salted KDF: bcrypt, scrypt, PBKDF2-HMAC-SHA256,
1079
+ or Argon2. CWE-916.'
1080
+ severity: ERROR
1081
+ languages:
1082
+ - python
1083
+ metadata:
1084
+ cwe:
1085
+ - 'CWE-916: Use of Password Hash With Insufficient Computational Effort'
1086
+ owasp:
1087
+ - A02:2021 - Cryptographic Failures
1088
+ category: security
1089
+ subcategory:
1090
+ - vuln
1091
+ technology:
1092
+ - python
1093
+ - hashlib
1094
+ likelihood: HIGH
1095
+ impact: HIGH
1096
+ confidence: MEDIUM
1097
+ vulnerability_class:
1098
+ - Cryptographic Issues
1099
+ patterns:
1100
+ - pattern-either:
1101
+ - pattern: hashlib.md5($PW.encode(...)).hexdigest()
1102
+ - pattern: hashlib.md5($PW.encode(...)).digest()
1103
+ - metavariable-regex:
1104
+ metavariable: $PW
1105
+ regex: (?i)(pass|pwd|secret|credential)
1106
+ - id: narvy.python.crypto.non-cryptographic-random-for-security.non-cryptographic-random-for-security
1107
+ message: The `random` module is not a cryptographically secure PRNG - it is a Mersenne Twister, fully
1108
+ predictable from a handful of outputs. Using it to build a token/password/reset-code/secret value
1109
+ lets an attacker predict future or past outputs. Use `secrets.token_urlsafe()` / `secrets.token_hex()`
1110
+ / `os.urandom()` instead. CWE-338.
1111
+ severity: ERROR
1112
+ languages:
1113
+ - python
1114
+ metadata:
1115
+ cwe:
1116
+ - 'CWE-338: Use of Cryptographically Weak PRNG'
1117
+ owasp:
1118
+ - A02:2021 - Cryptographic Failures
1119
+ category: security
1120
+ subcategory:
1121
+ - vuln
1122
+ technology:
1123
+ - python
1124
+ likelihood: MEDIUM
1125
+ impact: HIGH
1126
+ confidence: MEDIUM
1127
+ vulnerability_class:
1128
+ - Cryptographic Issues
1129
+ patterns:
1130
+ - pattern-either:
1131
+ - pattern: '$TOK = random.$FN(...)
1132
+
1133
+ '
1134
+ - pattern: '$TOK = "".join(random.$FN(...) for $I in range(...))
1135
+
1136
+ '
1137
+ - metavariable-regex:
1138
+ metavariable: $FN
1139
+ regex: ^(random|randint|randrange|choice|choices|getrandbits)$
1140
+ - metavariable-regex:
1141
+ metavariable: $TOK
1142
+ regex: (?i)(token|secret|password|passwd|reset_code|otp|api_key|session_id|csrf)
1143
+ - id: narvy.python.crypto.ssl-cert-verification-disabled.ssl-cert-verification-disabled
1144
+ message: TLS certificate verification is explicitly disabled (CERT_NONE / check_hostname=False / verify=False).
1145
+ This makes the connection trivially interceptable by an on-path attacker (MITM), even over HTTPS.
1146
+ Only ever acceptable for local, throwaway test fixtures - never for anything that touches real credentials
1147
+ or production data. CWE-295.
1148
+ severity: ERROR
1149
+ languages:
1150
+ - python
1151
+ metadata:
1152
+ cwe:
1153
+ - 'CWE-295: Improper Certificate Validation'
1154
+ owasp:
1155
+ - A02:2021 - Cryptographic Failures
1156
+ category: security
1157
+ subcategory:
1158
+ - vuln
1159
+ technology:
1160
+ - python
1161
+ - ssl
1162
+ - requests
1163
+ likelihood: HIGH
1164
+ impact: HIGH
1165
+ confidence: HIGH
1166
+ vulnerability_class:
1167
+ - Cryptographic Issues
1168
+ - Improper Authentication
1169
+ patterns:
1170
+ - pattern-either:
1171
+ - pattern: $CTX.verify_mode = ssl.CERT_NONE
1172
+ - pattern: $CTX.check_hostname = False
1173
+ - pattern: requests.$METHOD(..., verify=False, ...)
1174
+ - pattern: requests.Session().request(..., verify=False, ...)
1175
+ - pattern: ssl._create_unverified_context(...)
1176
+ - pattern-not: requests.$METHOD("http://localhost$REST", ..., verify=False, ...)
1177
+ - id: narvy.python.crypto.unauthenticated-cbc-no-mac.unauthenticated-cbc-no-mac
1178
+ message: CBC-mode encryption with no accompanying HMAC/authentication tag detected nearby. Unauthenticated
1179
+ CBC is vulnerable to padding-oracle and bit-flipping attacks. Prefer AES-GCM (authenticated) or add
1180
+ an encrypt-then-MAC (HMAC-SHA256) construction. CWE-327.
1181
+ severity: WARNING
1182
+ languages:
1183
+ - python
1184
+ metadata:
1185
+ cwe:
1186
+ - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm'
1187
+ owasp:
1188
+ - A02:2021 - Cryptographic Failures
1189
+ category: security
1190
+ subcategory:
1191
+ - vuln
1192
+ technology:
1193
+ - pycryptodome
1194
+ likelihood: LOW
1195
+ impact: HIGH
1196
+ confidence: LOW
1197
+ vulnerability_class:
1198
+ - Cryptographic Issues
1199
+ patterns:
1200
+ - pattern: $CIPHER = AES.new($KEY, AES.MODE_CBC, ...)
1201
+ - pattern-not-inside: '$CIPHER = AES.new($KEY, AES.MODE_CBC, ...)
1202
+
1203
+ ...
1204
+
1205
+ hmac.new(...)
1206
+
1207
+ '
1208
+ - pattern-not-inside: '$CIPHER = AES.new($KEY, AES.MODE_CBC, ...)
1209
+
1210
+ ...
1211
+
1212
+ $HM.update(...)
1213
+
1214
+ '
1215
+ - id: narvy.python.crypto.weak-dsa-key-size.weak-dsa-key-size
1216
+ message: DSA key size $BITS is below the 2048-bit minimum. Undersized DSA keys are vulnerable to discrete-log
1217
+ attacks with modern compute. CWE-326.
1218
+ severity: WARNING
1219
+ languages:
1220
+ - python
1221
+ metadata:
1222
+ cwe:
1223
+ - 'CWE-326: Inadequate Encryption Strength'
1224
+ owasp:
1225
+ - A02:2021 - Cryptographic Failures
1226
+ category: security
1227
+ subcategory:
1228
+ - vuln
1229
+ technology:
1230
+ - cryptography
1231
+ - pycryptodome
1232
+ likelihood: LOW
1233
+ impact: HIGH
1234
+ confidence: HIGH
1235
+ vulnerability_class:
1236
+ - Cryptographic Issues
1237
+ patterns:
1238
+ - pattern-either:
1239
+ - pattern: dsa.generate_private_key(key_size=$BITS, ...)
1240
+ - pattern: DSA.generate($BITS, ...)
1241
+ - metavariable-comparison:
1242
+ metavariable: $BITS
1243
+ comparison: int($BITS) < 2048
1244
+ - id: narvy.python.crypto.weak-ec-curve.weak-ec-curve
1245
+ message: Elliptic-curve key generated on curve '$CURVE', which provides less than 128 bits of security.
1246
+ Use SECP256R1 (P-256) or a stronger curve. CWE-326.
1247
+ severity: WARNING
1248
+ languages:
1249
+ - python
1250
+ metadata:
1251
+ cwe:
1252
+ - 'CWE-326: Inadequate Encryption Strength'
1253
+ owasp:
1254
+ - A02:2021 - Cryptographic Failures
1255
+ category: security
1256
+ subcategory:
1257
+ - vuln
1258
+ technology:
1259
+ - cryptography
1260
+ likelihood: LOW
1261
+ impact: HIGH
1262
+ confidence: MEDIUM
1263
+ vulnerability_class:
1264
+ - Cryptographic Issues
1265
+ patterns:
1266
+ - pattern: ec.generate_private_key(ec.$CURVE(), ...)
1267
+ - metavariable-regex:
1268
+ metavariable: $CURVE
1269
+ regex: ^(SECP192R1|SECT163K1|SECT163R2)$
1270
+ - id: narvy.python.crypto.weak-hash-cryptography-hazmat.weak-hash-cryptography-hazmat
1271
+ message: Weak hash algorithm '$ALGO' instantiated from cryptography.hazmat.primitives.hashes. MD5 and
1272
+ SHA1 are broken for collision resistance, whether the digest is used standalone (hashes.Hash), inside
1273
+ an HMAC, or as a signature padding hash. Use hashes.SHA256() or stronger. CWE-327.
1274
+ severity: WARNING
1275
+ languages:
1276
+ - python
1277
+ metadata:
1278
+ cwe:
1279
+ - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm'
1280
+ owasp:
1281
+ - A02:2021 - Cryptographic Failures
1282
+ category: security
1283
+ subcategory:
1284
+ - vuln
1285
+ technology:
1286
+ - python
1287
+ - cryptography
1288
+ likelihood: MEDIUM
1289
+ impact: MEDIUM
1290
+ confidence: HIGH
1291
+ vulnerability_class:
1292
+ - Cryptographic Issues
1293
+ references:
1294
+ - https://cwe.mitre.org/data/definitions/327.html
1295
+ patterns:
1296
+ - pattern: hashes.$ALGO()
1297
+ - metavariable-regex:
1298
+ metavariable: $ALGO
1299
+ regex: ^(MD5|SHA1)$
1300
+ - id: narvy.python.crypto.weak-hash-hashlib.weak-hash-hashlib
1301
+ message: Weak hash algorithm '$ALGO' from hashlib. MD5, SHA1, MD4 and MD2 are broken for collision resistance
1302
+ and unsuitable for integrity checks, digital signatures, or password storage. Use SHA-256 or better
1303
+ for integrity, and Argon2/bcrypt/scrypt for passwords. CWE-327.
1304
+ severity: WARNING
1305
+ languages:
1306
+ - python
1307
+ metadata:
1308
+ cwe:
1309
+ - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm'
1310
+ owasp:
1311
+ - A02:2021 - Cryptographic Failures
1312
+ category: security
1313
+ subcategory:
1314
+ - vuln
1315
+ technology:
1316
+ - python
1317
+ - hashlib
1318
+ likelihood: MEDIUM
1319
+ impact: MEDIUM
1320
+ confidence: HIGH
1321
+ vulnerability_class:
1322
+ - Cryptographic Issues
1323
+ references:
1324
+ - https://cwe.mitre.org/data/definitions/327.html
1325
+ patterns:
1326
+ - pattern-either:
1327
+ - pattern: hashlib.$ALGO(...)
1328
+ - pattern: hashlib.new("$ALGO", ...)
1329
+ - pattern: hashlib.new($X, name="$ALGO", ...)
1330
+ - metavariable-regex:
1331
+ metavariable: $ALGO
1332
+ regex: (?i)^(md5|md4|md2|sha1|sha)$
1333
+ - pattern-not: hashlib.$ALGO(..., usedforsecurity=False, ...)
1334
+ - pattern-not: hashlib.new("$ALGO", ..., usedforsecurity=False, ...)
1335
+ - id: narvy.python.crypto.weak-hash-pycryptodome.weak-hash-pycryptodome
1336
+ message: Weak hash algorithm '$ALGO' from PyCryptodome/PyCrypto. MD5/MD2/MD4/SHA1 are broken for collision
1337
+ resistance. Use SHA256, SHA3_256, or BLAKE2 instead. CWE-327.
1338
+ severity: WARNING
1339
+ languages:
1340
+ - python
1341
+ metadata:
1342
+ cwe:
1343
+ - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm'
1344
+ owasp:
1345
+ - A02:2021 - Cryptographic Failures
1346
+ category: security
1347
+ subcategory:
1348
+ - vuln
1349
+ technology:
1350
+ - pycryptodome
1351
+ - pycrypto
1352
+ likelihood: MEDIUM
1353
+ impact: MEDIUM
1354
+ confidence: HIGH
1355
+ vulnerability_class:
1356
+ - Cryptographic Issues
1357
+ patterns:
1358
+ - pattern-either:
1359
+ - pattern: Crypto.Hash.$ALGO.new(...)
1360
+ - pattern: $ALGO.new(...)
1361
+ - metavariable-regex:
1362
+ metavariable: $ALGO
1363
+ regex: ^(MD5|MD4|MD2|SHA1|SHA)$
1364
+ - id: narvy.python.crypto.weak-rsa-key-size.weak-rsa-key-size
1365
+ message: RSA key size $BITS is below 3072 bits. NIST SP 800-57 rates 2048-bit RSA as acceptable only
1366
+ through 2030; 3072 bits is the current recommendation for keys expected to remain in use beyond that,
1367
+ and is a safer default for any key generated today. CWE-326.
1368
+ severity: WARNING
1369
+ languages:
1370
+ - python
1371
+ metadata:
1372
+ cwe:
1373
+ - 'CWE-326: Inadequate Encryption Strength'
1374
+ owasp:
1375
+ - A02:2021 - Cryptographic Failures
1376
+ category: security
1377
+ subcategory:
1378
+ - vuln
1379
+ technology:
1380
+ - cryptography
1381
+ - pycryptodome
1382
+ likelihood: LOW
1383
+ impact: HIGH
1384
+ confidence: HIGH
1385
+ vulnerability_class:
1386
+ - Cryptographic Issues
1387
+ patterns:
1388
+ - pattern-either:
1389
+ - pattern: rsa.generate_private_key(..., key_size=$BITS, ...)
1390
+ - pattern: RSA.generate($BITS, ...)
1391
+ - metavariable-comparison:
1392
+ metavariable: $BITS
1393
+ comparison: int($BITS) < 3072
1394
+ - id: narvy.python.crypto.weak-tls-protocol-version.weak-tls-protocol-version
1395
+ message: Deprecated/insecure TLS protocol version '$PROTO' requested explicitly. SSLv2, SSLv3, and TLSv1.0/1.1
1396
+ all have known cryptographic weaknesses (POODLE, BEAST) and are disallowed by PCI-DSS since mid-2018.
1397
+ Use `ssl.PROTOCOL_TLS_CLIENT` or explicitly require TLS 1.2+. CWE-327.
1398
+ severity: ERROR
1399
+ languages:
1400
+ - python
1401
+ metadata:
1402
+ cwe:
1403
+ - 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm'
1404
+ owasp:
1405
+ - A02:2021 - Cryptographic Failures
1406
+ category: security
1407
+ subcategory:
1408
+ - vuln
1409
+ technology:
1410
+ - python
1411
+ - ssl
1412
+ likelihood: MEDIUM
1413
+ impact: HIGH
1414
+ confidence: HIGH
1415
+ vulnerability_class:
1416
+ - Cryptographic Issues
1417
+ patterns:
1418
+ - pattern: ssl.$PROTO
1419
+ - metavariable-regex:
1420
+ metavariable: $PROTO
1421
+ regex: ^PROTOCOL_(SSLv2|SSLv3|TLSv1|TLSv1_1)$
1422
+ - id: narvy.python.csrf.flask-wtf-csrf-enabled-false.flask-wtf-csrf-enabled-false
1423
+ message: '`WTF_CSRF_ENABLED` is explicitly set to `False`. This disables CSRF token validation on every
1424
+ form submitted through this Flask app, making every state-changing POST/PUT/DELETE endpoint that relies
1425
+ on cookie-based session auth forgeable from any other site the victim has open (CWE-352). Remove this
1426
+ line, or scope the exemption to the one view that genuinely needs it with `@csrf.exempt`.'
1427
+ severity: ERROR
1428
+ languages:
1429
+ - python
1430
+ metadata:
1431
+ cwe:
1432
+ - 'CWE-352: Cross-Site Request Forgery (CSRF)'
1433
+ owasp:
1434
+ - A01:2021 - Broken Access Control
1435
+ category: security
1436
+ subcategory:
1437
+ - vuln
1438
+ technology:
1439
+ - flask
1440
+ - flask-wtf
1441
+ likelihood: MEDIUM
1442
+ impact: HIGH
1443
+ confidence: HIGH
1444
+ vulnerability_class:
1445
+ - Cross-Site Request Forgery (CSRF)
1446
+ patterns:
1447
+ - pattern-either:
1448
+ - pattern: $APP.config["WTF_CSRF_ENABLED"] = False
1449
+ - pattern: $APP.config['WTF_CSRF_ENABLED'] = False
1450
+ - pattern: $APP.config.WTF_CSRF_ENABLED = False
1451
+ - pattern: WTF_CSRF_ENABLED = False
1452
+ - id: narvy.python.csrf.flask-wtf-csrf-exempt-usage.flask-wtf-csrf-exempt-usage
1453
+ message: '`csrf.exempt(...)` opts its argument out of CSRF protection. When the argument is a Blueprint
1454
+ (rather than a single view function), every state-changing route registered on it - present and future
1455
+ - loses CSRF protection at once, which is easy to lose track of as the blueprint grows (CWE-352).
1456
+ Semgrep cannot tell a Blueprint apart from a view function here without symbol resolution, so this
1457
+ is flagged for manual review either way: confirm every exempted route is genuinely authenticated by
1458
+ something other than cookies (e.g. a redirect-based external IdP callback, or a token-checked API
1459
+ view).'
1460
+ severity: WARNING
1461
+ languages:
1462
+ - python
1463
+ metadata:
1464
+ cwe:
1465
+ - 'CWE-352: Cross-Site Request Forgery (CSRF)'
1466
+ owasp:
1467
+ - A01:2021 - Broken Access Control
1468
+ category: security
1469
+ subcategory:
1470
+ - audit
1471
+ technology:
1472
+ - flask
1473
+ - flask-wtf
1474
+ likelihood: LOW
1475
+ impact: MEDIUM
1476
+ confidence: LOW
1477
+ vulnerability_class:
1478
+ - Cross-Site Request Forgery (CSRF)
1479
+ patterns:
1480
+ - pattern: $CSRF.exempt($ARG)
1481
+ - metavariable-regex:
1482
+ metavariable: $CSRF
1483
+ regex: ^csrf$
1484
+ - id: narvy.python.csrf.flask-wtf-imported-but-never-initialized.flask-wtf-imported-but-never-initialized
1485
+ message: '`CSRFProtect` is imported from `flask_wtf.csrf` but the module never constructs it against
1486
+ the app (`CSRFProtect(app)`) or calls `.init_app(app)` on an instance of it. The import alone protects
1487
+ nothing - every form in the app is submitted without CSRF token validation (CWE-352). Either wire
1488
+ it up (`CSRFProtect(app)`/`csrf.init_app(app)`) or remove the dead import if CSRF protection is intentionally
1489
+ handled elsewhere.'
1490
+ severity: WARNING
1491
+ languages:
1492
+ - python
1493
+ metadata:
1494
+ cwe:
1495
+ - 'CWE-352: Cross-Site Request Forgery (CSRF)'
1496
+ owasp:
1497
+ - A01:2021 - Broken Access Control
1498
+ category: security
1499
+ subcategory:
1500
+ - audit
1501
+ technology:
1502
+ - flask
1503
+ - flask-wtf
1504
+ likelihood: LOW
1505
+ impact: MEDIUM
1506
+ confidence: LOW
1507
+ vulnerability_class:
1508
+ - Cross-Site Request Forgery (CSRF)
1509
+ patterns:
1510
+ - pattern-either:
1511
+ - pattern: from flask_wtf.csrf import CSRFProtect
1512
+ - pattern: from flask_wtf import CSRFProtect
1513
+ - pattern-not-inside: '$IMPORT
1514
+
1515
+ ...
1516
+
1517
+ CSRFProtect($APP)
1518
+
1519
+ '
1520
+ - pattern-not-inside: '$IMPORT
1521
+
1522
+ ...
1523
+
1524
+ $CSRF = CSRFProtect()
1525
+
1526
+ ...
1527
+
1528
+ $CSRF.init_app($APP)
1529
+
1530
+ '
1531
+ - pattern-not-inside: '$IMPORT
1532
+
1533
+ ...
1534
+
1535
+ $CSRF = CSRFProtect(...)
1536
+
1537
+ '
1538
+ - id: narvy.python.csv-injection.csv-writer-formula-injection.csv-writer-formula-injection
1539
+ message: A value from request data is written to a CSV via `csv.writer` without checking for a leading
1540
+ `=`, `+`, `-`, or `@`. Spreadsheet applications (Excel, LibreOffice, Google Sheets) treat a cell starting
1541
+ with one of those characters as a formula; a value like `=cmd|'/c calc'!A1` opened by a victim who
1542
+ later imports this export can trigger formula execution (CSV/Formula Injection, CWE-1236). Prefix
1543
+ any cell beginning with `=+-@` with a single quote or tab character before writing.
1544
+ severity: WARNING
1545
+ languages:
1546
+ - python
1547
+ metadata:
1548
+ cwe:
1549
+ - 'CWE-1236: Improper Neutralization of Formula Elements in a CSV File'
1550
+ owasp:
1551
+ - A03:2021 - Injection
1552
+ category: security
1553
+ subcategory:
1554
+ - vuln
1555
+ technology:
1556
+ - python
1557
+ likelihood: LOW
1558
+ impact: MEDIUM
1559
+ confidence: LOW
1560
+ vulnerability_class:
1561
+ - Improper Validation
1562
+ patterns:
1563
+ - pattern-either:
1564
+ - pattern: $W.writerow([..., request.$KIND.$ATTR(...), ...])
1565
+ - pattern: $W.writerow([..., request.$KIND[$X], ...])
1566
+ - metavariable-regex:
1567
+ metavariable: $KIND
1568
+ regex: ^(args|form|values|GET|POST|json)$
1569
+ - id: narvy.python.ldap-injection.python-ldap-bind-dn-tainted.python-ldap-bind-dn-tainted
1570
+ message: A distinguished name (DN) passed to `simple_bind_s()` is built by concatenating request data.
1571
+ Beyond filter injection, an unsanitized DN can let an attacker manipulate the identity the bind authenticates
1572
+ as, or - on some LDAP servers - bind with an empty password against a partial/malformed DN to achieve
1573
+ an unauthenticated (anonymous) bind. CWE-90. Look the DN up from a trusted directory of known usernames
1574
+ instead of building it from request input.
1575
+ severity: WARNING
1576
+ languages:
1577
+ - python
1578
+ metadata:
1579
+ cwe:
1580
+ - 'CWE-90: Improper Neutralization of Special Elements used in an LDAP Query (''LDAP Injection'')'
1581
+ owasp:
1582
+ - A07:2021 - Identification and Authentication Failures
1583
+ category: security
1584
+ subcategory:
1585
+ - vuln
1586
+ technology:
1587
+ - python-ldap
1588
+ likelihood: LOW
1589
+ impact: HIGH
1590
+ confidence: LOW
1591
+ vulnerability_class:
1592
+ - Improper Authentication
1593
+ patterns:
1594
+ - pattern-either:
1595
+ - pattern: $CONN.simple_bind_s(f"...{$VAR}...", $PW)
1596
+ - pattern: $CONN.simple_bind_s("..." + $VAR, $PW)
1597
+ - id: narvy.python.ldap-injection.python-ldap-search-filter-tainted.python-ldap-search-filter-tainted
1598
+ message: An LDAP search filter is built by string interpolation with a value from request data, then
1599
+ passed to `search_s`/`search_ext_s`. LDAP filter syntax uses `*`, `(`, `)`, `\`, and `NUL` as metacharacters;
1600
+ an unescaped value lets an attacker widen the filter (`*)(uid=*))(|(uid=*`) to bypass an intended
1601
+ restriction or enumerate unrelated records (CWE-90, LDAP injection). Escape the value with `ldap.filter.escape_filter_chars()`
1602
+ before interpolating it into the filter string.
1603
+ severity: ERROR
1604
+ languages:
1605
+ - python
1606
+ metadata:
1607
+ cwe:
1608
+ - 'CWE-90: Improper Neutralization of Special Elements used in an LDAP Query (''LDAP Injection'')'
1609
+ owasp:
1610
+ - A03:2021 - Injection
1611
+ category: security
1612
+ subcategory:
1613
+ - vuln
1614
+ technology:
1615
+ - python-ldap
1616
+ - ldap3
1617
+ likelihood: MEDIUM
1618
+ impact: HIGH
1619
+ confidence: MEDIUM
1620
+ vulnerability_class:
1621
+ - Improper Validation
1622
+ patterns:
1623
+ - pattern-either:
1624
+ - pattern: $CONN.search_s($BASE, $SCOPE, f"...{$VAR}...")
1625
+ - pattern: $CONN.search_s($BASE, $SCOPE, "..." + $VAR)
1626
+ - pattern: $CONN.search_s($BASE, $SCOPE, "...".format($VAR))
1627
+ - pattern: $CONN.search_ext_s($BASE, $SCOPE, f"...{$VAR}...")
1628
+ - pattern: $CONN.search($BASE, f"...{$VAR}...")
1629
+ - pattern-not: $CONN.search_s($BASE, $SCOPE, "...")
1630
+ - pattern-not-inside: '$ESCAPED = ldap.filter.escape_filter_chars($VAR)
1631
+
1632
+ ...
1633
+
1634
+ '
1635
+ - id: narvy.python.mass_assignment.django-model-create-kwargs-from-request.django-model-create-kwargs-from-request
1636
+ message: A Django model is created with `**request.$KIND` unpacked directly as keyword arguments. Every
1637
+ key present in the request dict is assigned to a model field with no allowlist, so a client can set
1638
+ fields the view never intended to expose - privilege flags (`is_staff`, `is_superuser`), foreign keys,
1639
+ or price/quantity fields owned by server-side logic (CWE-915, mass assignment). Assign each expected
1640
+ field explicitly, or validate through a form/ serializer that declares its own field list.
1641
+ severity: WARNING
1642
+ languages:
1643
+ - python
1644
+ metadata:
1645
+ cwe:
1646
+ - 'CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes'
1647
+ owasp:
1648
+ - A08:2021 - Software and Data Integrity Failures
1649
+ category: security
1650
+ subcategory:
1651
+ - vuln
1652
+ technology:
1653
+ - python
1654
+ - django
1655
+ likelihood: LOW
1656
+ impact: HIGH
1657
+ confidence: MEDIUM
1658
+ vulnerability_class:
1659
+ - Improper Access Control
1660
+ patterns:
1661
+ - pattern-either:
1662
+ - pattern: $MODEL.objects.create(**request.$KIND)
1663
+ - pattern: $MODEL.objects.create(**request.$KIND.dict())
1664
+ - metavariable-regex:
1665
+ metavariable: $KIND
1666
+ regex: ^(GET|POST|data|query_params)$
1667
+ - id: narvy.python.mass_assignment.django-orm-bulk-mutation-kwargs-from-request.django-orm-bulk-mutation-kwargs-from-request
1668
+ message: A Django ORM call mutates a model with `**request.$KIND` unpacked directly as keyword arguments
1669
+ - an instance/model-utils `.update()`, a `QuerySet.filter(...).update()` bulk update, or a `get_or_create()`/`update_or_create()`
1670
+ convenience constructor. Every key present in the request dict overwrites the matching field with
1671
+ no allowlist - a client can set fields the view never intended to expose (CWE-915, mass assignment).
1672
+ Assign each expected field explicitly instead of unpacking the request dict.
1673
+ severity: WARNING
1674
+ languages:
1675
+ - python
1676
+ metadata:
1677
+ cwe:
1678
+ - 'CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes'
1679
+ owasp:
1680
+ - A08:2021 - Software and Data Integrity Failures
1681
+ category: security
1682
+ subcategory:
1683
+ - vuln
1684
+ technology:
1685
+ - python
1686
+ - django
1687
+ likelihood: LOW
1688
+ impact: HIGH
1689
+ confidence: MEDIUM
1690
+ vulnerability_class:
1691
+ - Improper Access Control
1692
+ patterns:
1693
+ - pattern-either:
1694
+ - pattern: $TARGET.update(**request.$KIND)
1695
+ - pattern: $MODEL.objects.get_or_create(**request.$KIND)
1696
+ - pattern: $MODEL.objects.update_or_create(**request.$KIND)
1697
+ - metavariable-regex:
1698
+ metavariable: $KIND
1699
+ regex: ^(GET|POST|data|query_params)$
1700
+ - id: narvy.python.misc.drf-serializer-fields-all-sensitive-model.drf-serializer-fields-all-sensitive-model
1701
+ message: A Django REST Framework `ModelSerializer` sets `fields = "__all__"` inside a class whose name
1702
+ suggests it wraps a security-sensitive model (User/Account/Admin/Permission/Role). Combined with a
1703
+ writable serializer, this lets a client set every model field - including ones like `is_staff`, `is_admin`,
1704
+ or `role` - directly from request JSON (mass assignment, CWE-915). Enumerate fields explicitly and
1705
+ add privileged fields to `read_only_fields`.
1706
+ severity: WARNING
1707
+ languages:
1708
+ - python
1709
+ metadata:
1710
+ cwe:
1711
+ - 'CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes'
1712
+ owasp:
1713
+ - A08:2021 - Software and Data Integrity Failures
1714
+ category: security
1715
+ subcategory:
1716
+ - audit
1717
+ technology:
1718
+ - python
1719
+ - django
1720
+ - djangorestframework
1721
+ likelihood: LOW
1722
+ impact: HIGH
1723
+ confidence: LOW
1724
+ vulnerability_class:
1725
+ - Improper Access Control
1726
+ patterns:
1727
+ - pattern-either:
1728
+ - pattern-inside: "class $SER(serializers.ModelSerializer):\n ...\n class Meta:\n ...\n"
1729
+ - pattern-inside: "class $SER(serializers.HyperlinkedModelSerializer):\n ...\n class Meta:\n\
1730
+ \ ...\n"
1731
+ - pattern-inside: "class $SER(ModelSerializer):\n ...\n class Meta:\n ...\n"
1732
+ - pattern-inside: "class $SER(HyperlinkedModelSerializer):\n ...\n class Meta:\n ...\n"
1733
+ - pattern: fields = "__all__"
1734
+ - pattern-not-inside: "class $SER(...):\n ...\n class Meta:\n ...\n read_only_fields\
1735
+ \ = (...)\n"
1736
+ - id: narvy.python.misc.insecure-file-permission.insecure-file-permission
1737
+ message: '`os.chmod()` / `os.open()` sets world-writable (and often world-readable) permissions (0o777
1738
+ / 0o666). Any local user or process can then modify or read this file, which is dangerous for config
1739
+ files, key material, or scripts that get executed later (CWE-732). Use the narrowest mode that works,
1740
+ e.g. 0o600 for secrets or 0o644 for regular files.'
1741
+ severity: WARNING
1742
+ languages:
1743
+ - python
1744
+ metadata:
1745
+ cwe:
1746
+ - 'CWE-732: Incorrect Permission Assignment for Critical Resource'
1747
+ owasp:
1748
+ - A01:2021 - Broken Access Control
1749
+ category: security
1750
+ subcategory:
1751
+ - audit
1752
+ technology:
1753
+ - python
1754
+ likelihood: LOW
1755
+ impact: MEDIUM
1756
+ confidence: HIGH
1757
+ vulnerability_class:
1758
+ - Improper Access Control
1759
+ patterns:
1760
+ - pattern-either:
1761
+ - pattern: os.chmod($PATH, 0o777)
1762
+ - pattern: os.chmod($PATH, 0o666)
1763
+ - pattern: os.chmod($PATH, 0777)
1764
+ - pattern: os.chmod($PATH, 0666)
1765
+ - pattern: os.chmod($PATH, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
1766
+ - pattern: os.open($PATH, $FLAGS, 0o777)
1767
+ - pattern: os.open($PATH, $FLAGS, 0o666)
1768
+ - id: narvy.python.misc.insecure-tempfile-mktemp.insecure-tempfile-mktemp
1769
+ message: '`tempfile.mktemp()` returns a filename but does not create the file. Between the name being
1770
+ returned and the caller opening it, another local process can create a file (or symlink) at that path,
1771
+ leading to a TOCTOU race that can be used for data tampering or symlink attacks (CWE-377). The Python
1772
+ docs themselves deprecate this function for exactly this reason. Use `tempfile.NamedTemporaryFile()`
1773
+ or `tempfile.mkstemp()`, which create the file atomically.'
1774
+ severity: WARNING
1775
+ languages:
1776
+ - python
1777
+ metadata:
1778
+ cwe:
1779
+ - 'CWE-377: Insecure Temporary File'
1780
+ owasp:
1781
+ - A01:2021 - Broken Access Control
1782
+ category: security
1783
+ subcategory:
1784
+ - vuln
1785
+ technology:
1786
+ - python
1787
+ likelihood: LOW
1788
+ impact: MEDIUM
1789
+ confidence: HIGH
1790
+ vulnerability_class:
1791
+ - Insecure Temporary File
1792
+ patterns:
1793
+ - pattern: tempfile.mktemp(...)
1794
+ - id: narvy.python.misc.redos-dynamic-regex-pattern.redos-dynamic-regex-pattern
1795
+ message: A regular-expression pattern is compiled/matched directly from request-derived data (`request.args`/`request.form`/`request.GET`/
1796
+ `request.POST`). An attacker who controls the pattern can supply a catastrophically-backtracking regex,
1797
+ causing a denial of service when Python's backtracking `re` engine evaluates it (ReDoS, CWE-1333).
1798
+ Validate/allow-list the pattern, cap its length, or run matching in a subprocess with a hard timeout.
1799
+ severity: WARNING
1800
+ languages:
1801
+ - python
1802
+ metadata:
1803
+ cwe:
1804
+ - 'CWE-1333: Inefficient Regular Expression Complexity'
1805
+ - 'CWE-400: Uncontrolled Resource Consumption'
1806
+ owasp:
1807
+ - A05:2021 - Security Misconfiguration
1808
+ category: security
1809
+ subcategory:
1810
+ - audit
1811
+ technology:
1812
+ - python
1813
+ - flask
1814
+ - django
1815
+ likelihood: LOW
1816
+ impact: MEDIUM
1817
+ confidence: MEDIUM
1818
+ vulnerability_class:
1819
+ - Denial-of-Service (DoS)
1820
+ patterns:
1821
+ - pattern-either:
1822
+ - pattern: re.compile(request.$KIND.$ATTR(...))
1823
+ - pattern: re.compile(request.$KIND[$X])
1824
+ - pattern: re.match(request.$KIND.$ATTR(...), ...)
1825
+ - pattern: re.search(request.$KIND.$ATTR(...), ...)
1826
+ - metavariable-regex:
1827
+ metavariable: $KIND
1828
+ regex: ^(args|form|values|GET|POST|json)$
1829
+ - id: narvy.python.nosqli.mongoengine-raw-query-tainted.mongoengine-raw-query-tainted
1830
+ message: '`mongoengine`''s `.objects(__raw__=...)` escape hatch is populated with a dict built from
1831
+ request data. `__raw__` bypasses mongoengine''s field validation/coercion entirely and is passed to
1832
+ MongoDB near-verbatim, so the same operator-injection risk applies (CWE-943). Prefer mongoengine''s
1833
+ typed field lookups (`Model.objects(field=value)`) which validate and coerce input.'
1834
+ severity: WARNING
1835
+ languages:
1836
+ - python
1837
+ metadata:
1838
+ cwe:
1839
+ - 'CWE-943: Improper Neutralization of Special Elements in Data Query Logic'
1840
+ owasp:
1841
+ - A03:2021 - Injection
1842
+ category: security
1843
+ subcategory:
1844
+ - vuln
1845
+ technology:
1846
+ - mongoengine
1847
+ - mongodb
1848
+ likelihood: MEDIUM
1849
+ impact: MEDIUM
1850
+ confidence: MEDIUM
1851
+ vulnerability_class:
1852
+ - SQL Injection
1853
+ patterns:
1854
+ - pattern-either:
1855
+ - pattern: $MODEL.objects(__raw__=request.get_json())
1856
+ - pattern: $MODEL.objects(__raw__=request.json)
1857
+ - id: narvy.python.nosqli.pymongo-filter-from-raw-request-json.pymongo-filter-from-raw-request-json
1858
+ message: 'A pymongo `find`/`find_one`/`update_one`/`delete_one` filter dict is built directly from `request.json`/`request.get_json()`
1859
+ without restricting it to expected scalar fields. MongoDB interprets keys starting with `$` inside
1860
+ a value position as query operators, so a client sending `{"password": {"$ne": null}}` instead of
1861
+ a string turns an equality check into "password is not null" - a classic NoSQL authentication-bypass
1862
+ primitive (CWE-943). Build the filter explicitly from named, type-checked fields instead of passing
1863
+ the request body through.'
1864
+ severity: ERROR
1865
+ languages:
1866
+ - python
1867
+ metadata:
1868
+ cwe:
1869
+ - 'CWE-943: Improper Neutralization of Special Elements in Data Query Logic'
1870
+ owasp:
1871
+ - A03:2021 - Injection
1872
+ category: security
1873
+ subcategory:
1874
+ - vuln
1875
+ technology:
1876
+ - pymongo
1877
+ - mongodb
1878
+ - flask
1879
+ likelihood: HIGH
1880
+ impact: HIGH
1881
+ confidence: MEDIUM
1882
+ vulnerability_class:
1883
+ - SQL Injection
1884
+ patterns:
1885
+ - pattern-either:
1886
+ - pattern: $COLL.find(request.get_json())
1887
+ - pattern: $COLL.find_one(request.get_json())
1888
+ - pattern: $COLL.find(request.json)
1889
+ - pattern: $COLL.find_one(request.json)
1890
+ - pattern: $COLL.find(request.json[$X])
1891
+ - pattern: $COLL.find_one(request.json[$X])
1892
+ - pattern: $COLL.update_one(request.get_json(), ...)
1893
+ - pattern: $COLL.delete_one(request.get_json())
1894
+ - metavariable-regex:
1895
+ metavariable: $COLL
1896
+ regex: (?i)(collection|coll|db\.\w+)
1897
+ - id: narvy.python.nosqli.pymongo-filter-value-from-request-arg.pymongo-filter-value-from-request-arg
1898
+ message: A pymongo query filter's *value* (not just the whole filter dict) is set directly to a raw
1899
+ request field (`request.args.get("id")`/`request.form["x"]`) without type coercion. If the client
1900
+ passes a JSON body instead of a query string, or the framework parses the value into a dict/list rather
1901
+ than a plain string, this reopens the same MongoDB operator-injection bypass (`$ne`, `$gt`, `$regex`)
1902
+ as a full raw filter would (CWE-943). Coerce the expected type explicitly (`str(...)`, `ObjectId(...)`)
1903
+ before using it in a filter.
1904
+ severity: WARNING
1905
+ languages:
1906
+ - python
1907
+ metadata:
1908
+ cwe:
1909
+ - 'CWE-943: Improper Neutralization of Special Elements in Data Query Logic'
1910
+ owasp:
1911
+ - A03:2021 - Injection
1912
+ category: security
1913
+ subcategory:
1914
+ - vuln
1915
+ technology:
1916
+ - pymongo
1917
+ - mongodb
1918
+ - flask
1919
+ likelihood: MEDIUM
1920
+ impact: MEDIUM
1921
+ confidence: LOW
1922
+ vulnerability_class:
1923
+ - SQL Injection
1924
+ patterns:
1925
+ - pattern-either:
1926
+ - pattern: '{$KEY: request.args.get($X)}'
1927
+ - pattern: '{$KEY: request.json.get($X)}'
1928
+ - pattern: '{$KEY: request.json[$X]}'
1929
+ - pattern-not: '{$KEY: str(request.args.get($X))}'
1930
+ - pattern-not: '{$KEY: int(request.args.get($X))}'
1931
+ - metavariable-regex:
1932
+ metavariable: $KEY
1933
+ regex: (?i)("|')(username|user|email|password|role|admin|id|_id)("|')
1934
+ - id: narvy.python.open-redirect.django-httpresponseredirect-tainted.django-httpresponseredirect-tainted
1935
+ message: Django `HttpResponseRedirect()`/`redirect()` target comes directly from request data with no
1936
+ host/path validation. Same phishing pivot as the Flask case (CWE-601) - an attacker-controlled `next`
1937
+ parameter sends an authenticated user off-site. Use `django.utils.http.url_has_allowed_host_and_scheme()`
1938
+ to validate before redirecting.
1939
+ severity: WARNING
1940
+ languages:
1941
+ - python
1942
+ metadata:
1943
+ cwe:
1944
+ - 'CWE-601: URL Redirection to Untrusted Site (''Open Redirect'')'
1945
+ owasp:
1946
+ - A01:2021 - Broken Access Control
1947
+ category: security
1948
+ subcategory:
1949
+ - vuln
1950
+ technology:
1951
+ - django
1952
+ likelihood: MEDIUM
1953
+ impact: MEDIUM
1954
+ confidence: MEDIUM
1955
+ vulnerability_class:
1956
+ - Open Redirect
1957
+ patterns:
1958
+ - pattern-either:
1959
+ - pattern: HttpResponseRedirect(request.$KIND.$ATTR(...))
1960
+ - pattern: HttpResponseRedirect(request.$KIND[$X])
1961
+ - pattern: redirect(request.$KIND.$ATTR(...))
1962
+ - metavariable-regex:
1963
+ metavariable: $KIND
1964
+ regex: ^(GET|POST)$
1965
+ - id: narvy.python.open-redirect.flask-redirect-tainted.flask-redirect-tainted
1966
+ message: Flask `redirect()` target URL comes directly from request data (`request.args.get("next")`,
1967
+ etc.) with no check that it points somewhere on this site. An attacker sends a link like `/login?next=https://evil.example/phish`
1968
+ and the app helpfully redirects a just-authenticated user straight to the phishing page (CWE-601).
1969
+ Validate the target is a relative path or matches an allowlisted host before redirecting.
1970
+ severity: WARNING
1971
+ languages:
1972
+ - python
1973
+ metadata:
1974
+ cwe:
1975
+ - 'CWE-601: URL Redirection to Untrusted Site (''Open Redirect'')'
1976
+ owasp:
1977
+ - A01:2021 - Broken Access Control
1978
+ category: security
1979
+ subcategory:
1980
+ - vuln
1981
+ technology:
1982
+ - flask
1983
+ likelihood: MEDIUM
1984
+ impact: MEDIUM
1985
+ confidence: MEDIUM
1986
+ vulnerability_class:
1987
+ - Open Redirect
1988
+ patterns:
1989
+ - pattern-either:
1990
+ - pattern: redirect(request.$KIND.$ATTR(...))
1991
+ - pattern: redirect(request.$KIND[$X])
1992
+ - pattern: redirect(request.$KIND.get(...))
1993
+ - pattern: '$URL = request.$KIND.$ATTR(...)
1994
+
1995
+ ...
1996
+
1997
+ redirect($URL)
1998
+
1999
+ '
2000
+ - metavariable-regex:
2001
+ metavariable: $KIND
2002
+ regex: ^(args|form|values|GET|POST)$
2003
+ - id: narvy.python.paramiko.connect-without-host-key-check.connect-without-host-key-check
2004
+ message: '`paramiko.client.SSHClient()` is used to connect without ever calling `set_missing_host_key_policy()`
2005
+ on it and without loading system/user known-hosts (`load_system_host_keys()` / `load_host_keys()`).
2006
+ paramiko''s own default policy in this state rejects unknown hosts, but a client built this way frequently
2007
+ gets "fixed" later by someone adding `AutoAddPolicy()` the first time it throws on an unrecognized
2008
+ host, silently reopening the man-in-the-middle exposure this rule''s sibling flags (CWE-322). Load
2009
+ a known_hosts file explicitly and keep the default `RejectPolicy`, or pin expected host keys.'
2010
+ severity: INFO
2011
+ languages:
2012
+ - python
2013
+ metadata:
2014
+ cwe:
2015
+ - 'CWE-322: Key Exchange without Entity Authentication'
2016
+ owasp:
2017
+ - A02:2021 - Cryptographic Failures
2018
+ category: security
2019
+ subcategory:
2020
+ - audit
2021
+ technology:
2022
+ - python
2023
+ - paramiko
2024
+ likelihood: LOW
2025
+ impact: MEDIUM
2026
+ confidence: LOW
2027
+ vulnerability_class:
2028
+ - Improper Authentication
2029
+ patterns:
2030
+ - pattern: '$CLIENT = paramiko.SSHClient()
2031
+
2032
+ ...
2033
+
2034
+ $CLIENT.connect(...)
2035
+
2036
+ '
2037
+ - pattern-not-inside: '$CLIENT = paramiko.SSHClient()
2038
+
2039
+ ...
2040
+
2041
+ $CLIENT.set_missing_host_key_policy(...)
2042
+
2043
+ ...
2044
+
2045
+ '
2046
+ - pattern-not-inside: '$CLIENT = paramiko.SSHClient()
2047
+
2048
+ ...
2049
+
2050
+ $CLIENT.load_system_host_keys(...)
2051
+
2052
+ ...
2053
+
2054
+ '
2055
+ - pattern-not-inside: '$CLIENT = paramiko.SSHClient()
2056
+
2057
+ ...
2058
+
2059
+ $CLIENT.load_host_keys(...)
2060
+
2061
+ ...
2062
+
2063
+ '
2064
+ - id: narvy.python.paramiko.implicit-trust-host-key.implicit-trust-host-key
2065
+ message: '`set_missing_host_key_policy()` is called with `AutoAddPolicy` or `WarningPolicy`. Both accept
2066
+ a host key the client has never seen before without any verification - `AutoAddPolicy` saves it silently,
2067
+ `WarningPolicy` only logs. Either way, a machine-in-the-middle presenting its own key instead of the
2068
+ real server''s is accepted transparently, defeating SSH host authentication (CWE-322). Use the default
2069
+ `RejectPolicy`, or pin the expected host key and verify it explicitly with a custom `paramiko.MissingHostKeyPolicy`
2070
+ subclass.'
2071
+ severity: ERROR
2072
+ languages:
2073
+ - python
2074
+ metadata:
2075
+ cwe:
2076
+ - 'CWE-322: Key Exchange without Entity Authentication'
2077
+ owasp:
2078
+ - A02:2021 - Cryptographic Failures
2079
+ category: security
2080
+ subcategory:
2081
+ - vuln
2082
+ technology:
2083
+ - python
2084
+ - paramiko
2085
+ likelihood: MEDIUM
2086
+ impact: HIGH
2087
+ confidence: HIGH
2088
+ vulnerability_class:
2089
+ - Improper Authentication
2090
+ patterns:
2091
+ - pattern-either:
2092
+ - pattern: $CLIENT.set_missing_host_key_policy(paramiko.AutoAddPolicy())
2093
+ - pattern: $CLIENT.set_missing_host_key_policy(paramiko.AutoAddPolicy)
2094
+ - pattern: $CLIENT.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
2095
+ - pattern: $CLIENT.set_missing_host_key_policy(paramiko.client.AutoAddPolicy)
2096
+ - pattern: $CLIENT.set_missing_host_key_policy(AutoAddPolicy())
2097
+ - pattern: $CLIENT.set_missing_host_key_policy(AutoAddPolicy)
2098
+ - pattern: $CLIENT.set_missing_host_key_policy(paramiko.WarningPolicy())
2099
+ - pattern: $CLIENT.set_missing_host_key_policy(paramiko.WarningPolicy)
2100
+ - pattern: $CLIENT.set_missing_host_key_policy(paramiko.client.WarningPolicy())
2101
+ - pattern: $CLIENT.set_missing_host_key_policy(paramiko.client.WarningPolicy)
2102
+ - pattern: $CLIENT.set_missing_host_key_policy(WarningPolicy())
2103
+ - pattern: $CLIENT.set_missing_host_key_policy(WarningPolicy)
2104
+ - id: narvy.python.path-traversal.django-fileresponse-tainted.django-fileresponse-tainted
2105
+ message: Django `FileResponse(open(...))` opens a file whose path is built from request data. Same arbitrary-file-read
2106
+ risk as the generic `open()` case (CWE-22), but the response streams the content straight back to
2107
+ the client, so a successful traversal is immediately readable rather than just server-side. Validate
2108
+ the resolved path stays under the intended media root before opening.
2109
+ severity: ERROR
2110
+ languages:
2111
+ - python
2112
+ metadata:
2113
+ cwe:
2114
+ - 'CWE-22: Improper Limitation of a Pathname to a Restricted Directory (''Path Traversal'')'
2115
+ owasp:
2116
+ - A01:2021 - Broken Access Control
2117
+ category: security
2118
+ subcategory:
2119
+ - vuln
2120
+ technology:
2121
+ - django
2122
+ likelihood: HIGH
2123
+ impact: HIGH
2124
+ confidence: MEDIUM
2125
+ vulnerability_class:
2126
+ - Path Traversal
2127
+ patterns:
2128
+ - pattern-either:
2129
+ - pattern: FileResponse(open(request.$KIND.$ATTR(...), ...))
2130
+ - pattern: FileResponse(open(request.$KIND[$X], ...))
2131
+ - metavariable-regex:
2132
+ metavariable: $KIND
2133
+ regex: ^(GET|POST)$
2134
+ - id: narvy.python.path-traversal.flask-send-file-tainted.flask-send-file-tainted
2135
+ message: '`send_file()`/`send_from_directory()` is called with a path/filename argument built directly
2136
+ from request data without validation. `send_file()` does not sanitize `../` sequences on the caller''s
2137
+ behalf when the path is fully attacker-supplied (only `send_from_directory`''s `filename` argument
2138
+ is checked, and only if you actually use that API correctly). Arbitrary file read (CWE-22). Validate
2139
+ the filename against an allowlist or strip path separators before use.'
2140
+ severity: ERROR
2141
+ languages:
2142
+ - python
2143
+ metadata:
2144
+ cwe:
2145
+ - 'CWE-22: Improper Limitation of a Pathname to a Restricted Directory (''Path Traversal'')'
2146
+ owasp:
2147
+ - A01:2021 - Broken Access Control
2148
+ category: security
2149
+ subcategory:
2150
+ - vuln
2151
+ technology:
2152
+ - flask
2153
+ likelihood: HIGH
2154
+ impact: HIGH
2155
+ confidence: MEDIUM
2156
+ vulnerability_class:
2157
+ - Path Traversal
2158
+ patterns:
2159
+ - pattern-either:
2160
+ - pattern: send_file(request.$KIND.$ATTR(...))
2161
+ - pattern: send_file(request.$KIND[$X])
2162
+ - pattern: send_file(f"...{request.$KIND.$ATTR(...)}...")
2163
+ - metavariable-regex:
2164
+ metavariable: $KIND
2165
+ regex: ^(args|form|values|GET|POST|json)$
2166
+ - id: narvy.python.path-traversal.open-tainted-path.open-tainted-path
2167
+ message: '`open()` is called with a filesystem path built directly from request data. Without normalizing
2168
+ and validating the path stays inside an intended base directory, an attacker can supply `../../etc/passwd`-style
2169
+ segments to read (or, on a write-mode open, overwrite) arbitrary files reachable by the process (CWE-22).
2170
+ Resolve the path with `os.path.realpath()`, then verify it is a descendant of the intended base directory
2171
+ before opening.'
2172
+ severity: ERROR
2173
+ languages:
2174
+ - python
2175
+ metadata:
2176
+ cwe:
2177
+ - 'CWE-22: Improper Limitation of a Pathname to a Restricted Directory (''Path Traversal'')'
2178
+ owasp:
2179
+ - A01:2021 - Broken Access Control
2180
+ category: security
2181
+ subcategory:
2182
+ - vuln
2183
+ technology:
2184
+ - python
2185
+ - flask
2186
+ - django
2187
+ likelihood: HIGH
2188
+ impact: HIGH
2189
+ confidence: MEDIUM
2190
+ vulnerability_class:
2191
+ - Path Traversal
2192
+ patterns:
2193
+ - pattern-either:
2194
+ - pattern: open(request.$KIND.$ATTR(...), ...)
2195
+ - pattern: open(request.$KIND[$X], ...)
2196
+ - pattern: open(f"...{request.$KIND.$ATTR(...)}...", ...)
2197
+ - pattern: open(os.path.join($BASE, request.$KIND.$ATTR(...)), ...)
2198
+ - pattern: '$P = request.$KIND.$ATTR(...)
2199
+
2200
+ ...
2201
+
2202
+ open($P, ...)
2203
+
2204
+ '
2205
+ - metavariable-regex:
2206
+ metavariable: $KIND
2207
+ regex: ^(args|form|values|GET|POST|json)$
2208
+ - id: narvy.python.path-traversal.zipfile-extractall-untrusted.zipfile-extractall-untrusted-archive
2209
+ message: '`ZipFile.extractall()` is called on an archive that came from an upload/request/network fetch,
2210
+ with no per-member path check first. A malicious zip can contain entries with `../` path segments
2211
+ or absolute paths ("Zip Slip") that extract outside the target directory, overwriting arbitrary files
2212
+ (CWE-22). Iterate `zip.namelist()` and reject any entry whose resolved path escapes the destination
2213
+ directory before calling `extractall()`.'
2214
+ severity: ERROR
2215
+ languages:
2216
+ - python
2217
+ metadata:
2218
+ cwe:
2219
+ - 'CWE-22: Improper Limitation of a Pathname to a Restricted Directory (''Path Traversal'')'
2220
+ owasp:
2221
+ - A01:2021 - Broken Access Control
2222
+ category: security
2223
+ subcategory:
2224
+ - vuln
2225
+ technology:
2226
+ - python
2227
+ likelihood: MEDIUM
2228
+ impact: HIGH
2229
+ confidence: LOW
2230
+ vulnerability_class:
2231
+ - Path Traversal
2232
+ patterns:
2233
+ - pattern: $ZIP.extractall(...)
2234
+ - pattern-inside: '$ZIP = zipfile.ZipFile($SRC, ...)
2235
+
2236
+ ...
2237
+
2238
+ '
2239
+ - metavariable-regex:
2240
+ metavariable: $SRC
2241
+ regex: (?i)(request|upload|response|download)
2242
+ - id: narvy.python.pyramid.csrf-disabled-globally.csrf-disabled-globally
2243
+ message: Pyramid's default CSRF check is turned off application-wide via `set_default_csrf_options(require_csrf=False)`.
2244
+ Every unsafe-method view (POST/PUT/PATCH/DELETE) in the app loses its automatic CSRF token check unless
2245
+ it re-enables it individually. Pass `require_csrf=True` (the framework default), or drop the kwarg
2246
+ entirely and only opt specific views out. CWE-352.
2247
+ severity: ERROR
2248
+ languages:
2249
+ - python
2250
+ metadata:
2251
+ cwe:
2252
+ - 'CWE-352: Cross-Site Request Forgery (CSRF)'
2253
+ owasp:
2254
+ - A01:2021 - Broken Access Control
2255
+ category: security
2256
+ subcategory:
2257
+ - vuln
2258
+ technology:
2259
+ - pyramid
2260
+ likelihood: MEDIUM
2261
+ impact: HIGH
2262
+ confidence: HIGH
2263
+ vulnerability_class:
2264
+ - Cross-Site Request Forgery (CSRF)
2265
+ patterns:
2266
+ - pattern-either:
2267
+ - pattern: $CONFIGURATOR.set_default_csrf_options(..., require_csrf=False, ...)
2268
+ - pattern: config.set_default_csrf_options(..., require_csrf=False, ...)
2269
+ - id: narvy.python.pyramid.csrf-disabled-per-view.csrf-disabled-per-view
2270
+ message: This view opts out of Pyramid's automatic CSRF check via `require_csrf=False` on `@view_config`/`add_view`.
2271
+ If the view accepts an unsafe HTTP method (POST/PUT/PATCH/DELETE) and changes state, this reopens
2272
+ it to cross-site request forgery even if CSRF protection is enabled globally elsewhere. Confirm this
2273
+ view is either read-only or protected by an equivalent check (API token, custom header requirement)
2274
+ before disabling it. CWE-352.
2275
+ severity: WARNING
2276
+ languages:
2277
+ - python
2278
+ metadata:
2279
+ cwe:
2280
+ - 'CWE-352: Cross-Site Request Forgery (CSRF)'
2281
+ owasp:
2282
+ - A01:2021 - Broken Access Control
2283
+ category: security
2284
+ subcategory:
2285
+ - vuln
2286
+ technology:
2287
+ - pyramid
2288
+ likelihood: LOW
2289
+ impact: HIGH
2290
+ confidence: MEDIUM
2291
+ vulnerability_class:
2292
+ - Cross-Site Request Forgery (CSRF)
2293
+ patterns:
2294
+ - pattern-either:
2295
+ - pattern: view_config(..., require_csrf=False, ...)
2296
+ - pattern: pyramid.view.view_config(..., require_csrf=False, ...)
2297
+ - pattern: $CONFIGURATOR.add_view(..., require_csrf=False, ...)
2298
+ - id: narvy.python.pyramid.response-object-echoes-request-data.response-object-echoes-request-data
2299
+ message: A Pyramid `Response` is built (or its body/text attribute set) by directly interpolating request-derived
2300
+ data (`request.GET`/`request.POST`/`request.params`/`request.matchdict`) instead of rendering it through
2301
+ a Chameleon/Jinja2/Mako template, which auto-escapes. `Response` does no HTML escaping of its own,
2302
+ so any HTML/JS in the request value reaches the browser verbatim. Reflected XSS (CWE-79). Render through
2303
+ a template, or escape the value with `markupsafe.escape()`/`html.escape()` before it reaches the response
2304
+ body.
2305
+ severity: ERROR
2306
+ languages:
2307
+ - python
2308
+ metadata:
2309
+ cwe:
2310
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
2311
+ owasp:
2312
+ - A03:2021 - Injection
2313
+ category: security
2314
+ subcategory:
2315
+ - vuln
2316
+ technology:
2317
+ - pyramid
2318
+ likelihood: HIGH
2319
+ impact: MEDIUM
2320
+ confidence: MEDIUM
2321
+ vulnerability_class:
2322
+ - Cross-Site-Scripting (XSS)
2323
+ mode: taint
2324
+ pattern-sources:
2325
+ - patterns:
2326
+ - pattern-either:
2327
+ - pattern: request.GET
2328
+ - pattern: request.GET.get(...)
2329
+ - pattern: request.GET[...]
2330
+ - pattern: request.POST
2331
+ - pattern: request.POST.get(...)
2332
+ - pattern: request.POST[...]
2333
+ - pattern: request.params
2334
+ - pattern: request.params.get(...)
2335
+ - pattern: request.params[...]
2336
+ - pattern: request.matchdict
2337
+ - pattern: request.matchdict.get(...)
2338
+ - pattern: request.matchdict[...]
2339
+ pattern-sinks:
2340
+ - patterns:
2341
+ - pattern-either:
2342
+ - pattern: Response($SINK)
2343
+ - pattern: pyramid.response.Response($SINK)
2344
+ - pattern: $RESP.text = $SINK
2345
+ - pattern: $RESP.body = $SINK
2346
+ - pattern: $RESP.ubody = $SINK
2347
+ - pattern: $RESP.unicode_body = $SINK
2348
+ pattern-sanitizers:
2349
+ - patterns:
2350
+ - pattern: escape(...)
2351
+ - patterns:
2352
+ - pattern: markupsafe.escape(...)
2353
+ - patterns:
2354
+ - pattern: html.escape(...)
2355
+ - id: narvy.python.pyramid.session-cookie-not-httponly.session-cookie-not-httponly
2356
+ message: Pyramid auth-ticket or session cookie constructed without `httponly=True` (either omitted,
2357
+ which is not the secure choice for this kind of cookie, or explicitly set to False). Client-side JavaScript
2358
+ can read this cookie, so any XSS elsewhere in the app can be used to steal the session/auth-ticket
2359
+ value directly. Pass `httponly=True`. CWE-1004.
2360
+ severity: WARNING
2361
+ languages:
2362
+ - python
2363
+ metadata:
2364
+ cwe:
2365
+ - 'CWE-1004: Sensitive Cookie Without ''HttpOnly'' Flag'
2366
+ owasp:
2367
+ - A05:2021 - Security Misconfiguration
2368
+ category: security
2369
+ subcategory:
2370
+ - vuln
2371
+ technology:
2372
+ - pyramid
2373
+ likelihood: LOW
2374
+ impact: MEDIUM
2375
+ confidence: MEDIUM
2376
+ vulnerability_class:
2377
+ - Cookie Security
2378
+ patterns:
2379
+ - pattern-either:
2380
+ - pattern: AuthTktAuthenticationPolicy(..., httponly=False, ...)
2381
+ - pattern: pyramid.authentication.AuthTktAuthenticationPolicy(..., httponly=False, ...)
2382
+ - pattern: SignedCookieSessionFactory(..., httponly=False, ...)
2383
+ - pattern: pyramid.session.SignedCookieSessionFactory(..., httponly=False, ...)
2384
+ - patterns:
2385
+ - pattern-either:
2386
+ - pattern: AuthTktAuthenticationPolicy(...)
2387
+ - pattern: pyramid.authentication.AuthTktAuthenticationPolicy(...)
2388
+ - pattern: SignedCookieSessionFactory(...)
2389
+ - pattern: pyramid.session.SignedCookieSessionFactory(...)
2390
+ - pattern-not: AuthTktAuthenticationPolicy(..., httponly=$H, ...)
2391
+ - pattern-not: pyramid.authentication.AuthTktAuthenticationPolicy(..., httponly=$H, ...)
2392
+ - pattern-not: SignedCookieSessionFactory(..., httponly=$H, ...)
2393
+ - pattern-not: pyramid.session.SignedCookieSessionFactory(..., httponly=$H, ...)
2394
+ - pattern-not: AuthTktAuthenticationPolicy(..., **$KW)
2395
+ - pattern-not: pyramid.authentication.AuthTktAuthenticationPolicy(..., **$KW)
2396
+ - pattern-not: SignedCookieSessionFactory(..., **$KW)
2397
+ - pattern-not: pyramid.session.SignedCookieSessionFactory(..., **$KW)
2398
+ - id: narvy.python.pyramid.session-cookie-not-secure.session-cookie-not-secure
2399
+ message: Pyramid auth-ticket or session cookie constructed without `secure=True` (either omitted or
2400
+ explicitly set to False). Without it, the browser will happily send this cookie over a plaintext HTTP
2401
+ connection, exposing the session/auth-ticket value to anyone on the network path. Pass `secure=True`
2402
+ for any deployment that is reachable over HTTPS (which should be all of them). CWE-614.
2403
+ severity: WARNING
2404
+ languages:
2405
+ - python
2406
+ metadata:
2407
+ cwe:
2408
+ - 'CWE-614: Sensitive Cookie in HTTPS Session Without ''Secure'' Attribute'
2409
+ owasp:
2410
+ - A05:2021 - Security Misconfiguration
2411
+ category: security
2412
+ subcategory:
2413
+ - vuln
2414
+ technology:
2415
+ - pyramid
2416
+ likelihood: LOW
2417
+ impact: MEDIUM
2418
+ confidence: MEDIUM
2419
+ vulnerability_class:
2420
+ - Cookie Security
2421
+ patterns:
2422
+ - pattern-either:
2423
+ - pattern: AuthTktAuthenticationPolicy(..., secure=False, ...)
2424
+ - pattern: pyramid.authentication.AuthTktAuthenticationPolicy(..., secure=False, ...)
2425
+ - pattern: SignedCookieSessionFactory(..., secure=False, ...)
2426
+ - pattern: pyramid.session.SignedCookieSessionFactory(..., secure=False, ...)
2427
+ - patterns:
2428
+ - pattern-either:
2429
+ - pattern: AuthTktAuthenticationPolicy(...)
2430
+ - pattern: pyramid.authentication.AuthTktAuthenticationPolicy(...)
2431
+ - pattern: SignedCookieSessionFactory(...)
2432
+ - pattern: pyramid.session.SignedCookieSessionFactory(...)
2433
+ - pattern-not: AuthTktAuthenticationPolicy(..., secure=$S, ...)
2434
+ - pattern-not: pyramid.authentication.AuthTktAuthenticationPolicy(..., secure=$S, ...)
2435
+ - pattern-not: SignedCookieSessionFactory(..., secure=$S, ...)
2436
+ - pattern-not: pyramid.session.SignedCookieSessionFactory(..., secure=$S, ...)
2437
+ - pattern-not: AuthTktAuthenticationPolicy(..., **$KW)
2438
+ - pattern-not: pyramid.authentication.AuthTktAuthenticationPolicy(..., **$KW)
2439
+ - pattern-not: SignedCookieSessionFactory(..., **$KW)
2440
+ - pattern-not: pyramid.session.SignedCookieSessionFactory(..., **$KW)
2441
+ - id: narvy.python.pyramid.session-cookie-samesite-unset.session-cookie-samesite-unset
2442
+ message: Pyramid auth-ticket or session cookie constructed without a `samesite` policy (omitted, or
2443
+ explicitly `samesite=None`). Without SameSite=Lax/Strict, this cookie is sent on cross-site requests
2444
+ too, which is what makes CSRF against session-authenticated endpoints possible in the first place.
2445
+ Pass `samesite="Lax"` (or `"Strict"` for the most sensitive cookies). CWE-352.
2446
+ severity: WARNING
2447
+ languages:
2448
+ - python
2449
+ metadata:
2450
+ cwe:
2451
+ - 'CWE-352: Cross-Site Request Forgery (CSRF)'
2452
+ owasp:
2453
+ - A05:2021 - Security Misconfiguration
2454
+ category: security
2455
+ subcategory:
2456
+ - vuln
2457
+ technology:
2458
+ - pyramid
2459
+ likelihood: LOW
2460
+ impact: MEDIUM
2461
+ confidence: MEDIUM
2462
+ vulnerability_class:
2463
+ - Cookie Security
2464
+ - Cross-Site Request Forgery (CSRF)
2465
+ patterns:
2466
+ - pattern-either:
2467
+ - pattern: AuthTktAuthenticationPolicy(..., samesite=None, ...)
2468
+ - pattern: pyramid.authentication.AuthTktAuthenticationPolicy(..., samesite=None, ...)
2469
+ - pattern: SignedCookieSessionFactory(..., samesite=None, ...)
2470
+ - pattern: pyramid.session.SignedCookieSessionFactory(..., samesite=None, ...)
2471
+ - patterns:
2472
+ - pattern-either:
2473
+ - pattern: AuthTktAuthenticationPolicy(...)
2474
+ - pattern: pyramid.authentication.AuthTktAuthenticationPolicy(...)
2475
+ - pattern: SignedCookieSessionFactory(...)
2476
+ - pattern: pyramid.session.SignedCookieSessionFactory(...)
2477
+ - pattern-not: AuthTktAuthenticationPolicy(..., samesite=$SS, ...)
2478
+ - pattern-not: pyramid.authentication.AuthTktAuthenticationPolicy(..., samesite=$SS, ...)
2479
+ - pattern-not: SignedCookieSessionFactory(..., samesite=$SS, ...)
2480
+ - pattern-not: pyramid.session.SignedCookieSessionFactory(..., samesite=$SS, ...)
2481
+ - pattern-not: AuthTktAuthenticationPolicy(..., **$KW)
2482
+ - pattern-not: pyramid.authentication.AuthTktAuthenticationPolicy(..., **$KW)
2483
+ - pattern-not: SignedCookieSessionFactory(..., **$KW)
2484
+ - pattern-not: pyramid.session.SignedCookieSessionFactory(..., **$KW)
2485
+ - id: narvy.python.secrets.hardcoded-credential-assignment.hardcoded-credential-assignment
2486
+ message: A variable named like a credential (password/api_key/secret/token) is assigned a plain string
2487
+ literal rather than being read from an environment variable or secret store. Anyone with source or
2488
+ VCS history access recovers the value (CWE-798). Load it from `os.environ` and fail startup if missing.
2489
+ severity: WARNING
2490
+ languages:
2491
+ - python
2492
+ metadata:
2493
+ cwe:
2494
+ - 'CWE-798: Use of Hard-coded Credentials'
2495
+ owasp:
2496
+ - A07:2021 - Identification and Authentication Failures
2497
+ category: security
2498
+ subcategory:
2499
+ - vuln
2500
+ technology:
2501
+ - python
2502
+ likelihood: MEDIUM
2503
+ impact: HIGH
2504
+ confidence: LOW
2505
+ vulnerability_class:
2506
+ - Hard-coded Secrets
2507
+ paths:
2508
+ exclude:
2509
+ - '**/test*/**'
2510
+ - '**/tests/**'
2511
+ - '**/*test*.py'
2512
+ - '**/*example*.py'
2513
+ - '**/docs_src/**'
2514
+ - '**/examples/**'
2515
+ - '**/conftest.py'
2516
+ - '**/fixtures/**'
2517
+ patterns:
2518
+ - pattern-either:
2519
+ - pattern: $VAR = "$LITERAL"
2520
+ - metavariable-regex:
2521
+ metavariable: $VAR
2522
+ regex: (?i)^([a-z0-9]+_)*(password|passwd|passwsd|pwd|passphrase|secret|secret_key|api_key|apikey|api_secret|access_token|auth_token|refresh_token|session_token|client_secret|private_key|token|credential)s?$
2523
+ - metavariable-regex:
2524
+ metavariable: $LITERAL
2525
+ regex: ^.{8,}$
2526
+ - metavariable-regex:
2527
+ metavariable: $LITERAL
2528
+ regex: ^(?!.*(\{\}|\{\d|\{[a-zA-Z_]|%s|%d|xxxx|XXXX|changeme|CHANGEME|your[-_ ]|placeholder|example|redacted|\*\*\*)).*$
2529
+ - pattern-not: $VAR = os.environ[$X]
2530
+ - pattern-not: $VAR = os.getenv($X)
2531
+ - pattern-not: $VAR = ""
2532
+ - id: narvy.python.secrets.logger-credential-leak.logger-credential-leak
2533
+ message: A logging call includes a variable named like a credential (password/token/secret/api_key)
2534
+ in its message. Logs routinely end up in less-restricted storage than the application itself (log
2535
+ aggregators, ticket attachments, `journalctl`, support bundles); writing a live credential there is
2536
+ an information-disclosure bug (CWE-532). Redact or omit the value before logging.
2537
+ severity: WARNING
2538
+ languages:
2539
+ - python
2540
+ metadata:
2541
+ cwe:
2542
+ - 'CWE-532: Insertion of Sensitive Information into Log File'
2543
+ owasp:
2544
+ - A09:2021 - Security Logging and Monitoring Failures
2545
+ category: security
2546
+ subcategory:
2547
+ - vuln
2548
+ technology:
2549
+ - python
2550
+ likelihood: MEDIUM
2551
+ impact: MEDIUM
2552
+ confidence: LOW
2553
+ vulnerability_class:
2554
+ - Mishandled Sensitive Information
2555
+ patterns:
2556
+ - pattern-either:
2557
+ - pattern: logging.$LEVEL(f"...{$VAR}...")
2558
+ - pattern: logger.$LEVEL(f"...{$VAR}...")
2559
+ - pattern: $LOG.$LEVEL(f"...{$VAR}...")
2560
+ - metavariable-regex:
2561
+ metavariable: $LEVEL
2562
+ regex: ^(debug|info|warning|warn|error|critical|exception)$
2563
+ - metavariable-regex:
2564
+ metavariable: $VAR
2565
+ regex: (?i)^(password|passwd|pwd|token|secret|api_key|apikey|access_token|auth_token|credential)s?$
2566
+ - id: narvy.python.session.django-csrf-exempt-view.django-csrf-exempt-view
2567
+ message: View is decorated with `@csrf_exempt`, opting it out of Django's CSRF protection entirely.
2568
+ If the view performs a state-changing action (POST/PUT/DELETE that writes data) and relies on cookie-based
2569
+ session auth, it is now forgeable from any other site the victim has open (CWE-352). Only use `@csrf_exempt`
2570
+ for endpoints authenticated by something other than cookies (e.g. a signed API token checked in the
2571
+ view), and say so in a comment.
2572
+ severity: WARNING
2573
+ languages:
2574
+ - python
2575
+ metadata:
2576
+ cwe:
2577
+ - 'CWE-352: Cross-Site Request Forgery (CSRF)'
2578
+ owasp:
2579
+ - A01:2021 - Broken Access Control
2580
+ category: security
2581
+ subcategory:
2582
+ - vuln
2583
+ technology:
2584
+ - django
2585
+ likelihood: MEDIUM
2586
+ impact: MEDIUM
2587
+ confidence: LOW
2588
+ vulnerability_class:
2589
+ - Cross-Site Request Forgery (CSRF)
2590
+ patterns:
2591
+ - pattern: "@csrf_exempt\ndef $VIEW(...):\n ...\n"
2592
+ - id: narvy.python.session.django-debug-true-in-settings.django-debug-true-in-settings
2593
+ message: '`DEBUG = True` in a Django settings file. In debug mode Django shows full stack traces with
2594
+ local variables, settings values (including secrets, unless explicitly redacted), and SQL queries
2595
+ to any visitor who triggers a 500 - a serious information disclosure if this file is ever the one
2596
+ loaded in production (CWE-489, CWE-215). Default to `DEBUG = False` and flip it on only via an environment
2597
+ variable in local dev.'
2598
+ severity: WARNING
2599
+ languages:
2600
+ - python
2601
+ metadata:
2602
+ cwe:
2603
+ - 'CWE-489: Active Debug Code'
2604
+ - 'CWE-215: Insertion of Sensitive Information Into Debugging Code'
2605
+ owasp:
2606
+ - A05:2021 - Security Misconfiguration
2607
+ category: security
2608
+ subcategory:
2609
+ - vuln
2610
+ technology:
2611
+ - django
2612
+ likelihood: LOW
2613
+ impact: MEDIUM
2614
+ confidence: LOW
2615
+ vulnerability_class:
2616
+ - Active Debug Code
2617
+ patterns:
2618
+ - pattern: DEBUG = True
2619
+ - id: narvy.python.session.django-samesite-none-or-disabled.django-samesite-none-or-disabled
2620
+ message: Session/CSRF cookie SameSite policy is set to "None" or disabled. SameSite=None allows the
2621
+ cookie to be sent on cross-site requests, which is the exact condition CSRF protection exists to prevent
2622
+ (CWE-352). Use `SESSION_COOKIE_SAMESITE = "Lax"` (or "Strict") unless you have a documented cross-site
2623
+ embedding need paired with other CSRF defenses.
2624
+ severity: WARNING
2625
+ languages:
2626
+ - python
2627
+ metadata:
2628
+ cwe:
2629
+ - 'CWE-352: Cross-Site Request Forgery (CSRF)'
2630
+ owasp:
2631
+ - A01:2021 - Broken Access Control
2632
+ category: security
2633
+ subcategory:
2634
+ - vuln
2635
+ technology:
2636
+ - django
2637
+ likelihood: LOW
2638
+ impact: MEDIUM
2639
+ confidence: MEDIUM
2640
+ vulnerability_class:
2641
+ - Cross-Site Request Forgery (CSRF)
2642
+ patterns:
2643
+ - pattern-either:
2644
+ - pattern: SESSION_COOKIE_SAMESITE = None
2645
+ - pattern: SESSION_COOKIE_SAMESITE = "None"
2646
+ - pattern: CSRF_COOKIE_SAMESITE = None
2647
+ - pattern: CSRF_COOKIE_SAMESITE = "None"
2648
+ - id: narvy.python.session.django-secret-key-hardcoded-literal.django-secret-key-hardcoded-literal
2649
+ message: '`SECRET_KEY` is assigned a plain string literal directly in settings, with no environment/secret-manager
2650
+ indirection. Django''s SECRET_KEY signs sessions, CSRF tokens, and password-reset links; a value committed
2651
+ to source control (and therefore to every clone, fork, and CI log) is a hardcoded cryptographic key
2652
+ (CWE-321, CWE-798). Load it from an environment variable and fail startup if unset.'
2653
+ severity: ERROR
2654
+ languages:
2655
+ - python
2656
+ metadata:
2657
+ cwe:
2658
+ - 'CWE-321: Use of Hard-coded Cryptographic Key'
2659
+ - 'CWE-798: Use of Hard-coded Credentials'
2660
+ owasp:
2661
+ - A02:2021 - Cryptographic Failures
2662
+ category: security
2663
+ subcategory:
2664
+ - vuln
2665
+ technology:
2666
+ - django
2667
+ likelihood: HIGH
2668
+ impact: HIGH
2669
+ confidence: MEDIUM
2670
+ vulnerability_class:
2671
+ - Cryptographic Issues
2672
+ - Hard-coded Credentials
2673
+ paths:
2674
+ exclude:
2675
+ - '**/test*/**'
2676
+ - '**/tests/**'
2677
+ - '**/*test*.py'
2678
+ - '**/*example*.py'
2679
+ - '**/docs_src/**'
2680
+ - '**/examples/**'
2681
+ patterns:
2682
+ - pattern: SECRET_KEY = "$LITERAL"
2683
+ - metavariable-regex:
2684
+ metavariable: $LITERAL
2685
+ regex: ^.{8,}$
2686
+ - id: narvy.python.session.django-session-cookie-insecure-settings.django-session-cookie-insecure-settings
2687
+ message: Django settings explicitly disable a session-cookie security flag (`SESSION_COOKIE_SECURE =
2688
+ False`, `SESSION_COOKIE_HTTPONLY = False`, or `CSRF_COOKIE_SECURE = False`). This allows the session/CSRF
2689
+ cookie to be sent over plain HTTP or read from JavaScript, undermining transport security and XSS-hardening
2690
+ respectively (CWE-614, CWE-1004).
2691
+ severity: WARNING
2692
+ languages:
2693
+ - python
2694
+ metadata:
2695
+ cwe:
2696
+ - 'CWE-614: Sensitive Cookie in HTTPS Session Without ''Secure'' Attribute'
2697
+ - 'CWE-1004: Sensitive Cookie Without ''HttpOnly'' Flag'
2698
+ owasp:
2699
+ - A05:2021 - Security Misconfiguration
2700
+ category: security
2701
+ subcategory:
2702
+ - vuln
2703
+ technology:
2704
+ - django
2705
+ likelihood: MEDIUM
2706
+ impact: MEDIUM
2707
+ confidence: HIGH
2708
+ vulnerability_class:
2709
+ - Cookie Security
2710
+ patterns:
2711
+ - pattern-either:
2712
+ - pattern: SESSION_COOKIE_SECURE = False
2713
+ - pattern: SESSION_COOKIE_HTTPONLY = False
2714
+ - pattern: CSRF_COOKIE_SECURE = False
2715
+ - id: narvy.python.session.flask-debug-mode-enabled.flask-debug-mode-enabled
2716
+ message: Flask app is run with `debug=True` (or `app.debug = True`). The Werkzeug debugger it enables
2717
+ exposes an interactive Python console on every unhandled exception - if this ever reaches a production
2718
+ deployment, any visitor who triggers an error gets a remote Python shell (CWE-489, active debug code).
2719
+ Gate `debug=` behind an environment-controlled flag that defaults to `False`, and never hardcode `True`.
2720
+ severity: ERROR
2721
+ languages:
2722
+ - python
2723
+ metadata:
2724
+ cwe:
2725
+ - 'CWE-489: Active Debug Code'
2726
+ owasp:
2727
+ - A05:2021 - Security Misconfiguration
2728
+ category: security
2729
+ subcategory:
2730
+ - vuln
2731
+ technology:
2732
+ - flask
2733
+ likelihood: MEDIUM
2734
+ impact: HIGH
2735
+ confidence: MEDIUM
2736
+ vulnerability_class:
2737
+ - Active Debug Code
2738
+ patterns:
2739
+ - pattern-either:
2740
+ - pattern: $APP.run(..., debug=True, ...)
2741
+ - pattern: $APP.debug = True
2742
+ - pattern: $APP.config["DEBUG"] = True
2743
+ - pattern: $APP.config['DEBUG'] = True
2744
+ - id: narvy.python.session.flask-set-cookie-missing-httponly.flask-set-cookie-missing-httponly
2745
+ message: '`set_cookie()` is called without `httponly=True`. A cookie without HttpOnly is readable from
2746
+ JavaScript via `document.cookie`, so any XSS on the page can exfiltrate it (session hijack). CWE-1004.
2747
+ Set `httponly=True`, or `SESSION_COOKIE_HTTPONLY = True` globally.'
2748
+ severity: WARNING
2749
+ languages:
2750
+ - python
2751
+ metadata:
2752
+ cwe:
2753
+ - 'CWE-1004: Sensitive Cookie Without ''HttpOnly'' Flag'
2754
+ owasp:
2755
+ - A05:2021 - Security Misconfiguration
2756
+ category: security
2757
+ subcategory:
2758
+ - vuln
2759
+ technology:
2760
+ - flask
2761
+ likelihood: MEDIUM
2762
+ impact: MEDIUM
2763
+ confidence: MEDIUM
2764
+ vulnerability_class:
2765
+ - Cookie Security
2766
+ patterns:
2767
+ - pattern: $RESP.set_cookie($NAME, $VALUE, ...)
2768
+ - pattern-not: $RESP.set_cookie($NAME, $VALUE, ..., httponly=True, ...)
2769
+ - pattern-not: $RESP.set_cookie($NAME, $VALUE, ..., httponly=$X, ...)
2770
+ - id: narvy.python.session.flask-set-cookie-missing-secure.flask-set-cookie-missing-secure
2771
+ message: '`set_cookie()` is called without `secure=True`. Without the Secure flag, the browser will
2772
+ also send this cookie over plain HTTP, exposing it to network eavesdroppers and active MITM (CWE-614).
2773
+ Set `secure=True` for any cookie carrying session/auth state, or set `SESSION_COOKIE_SECURE = True`
2774
+ globally in Flask config.'
2775
+ severity: WARNING
2776
+ languages:
2777
+ - python
2778
+ metadata:
2779
+ cwe:
2780
+ - 'CWE-614: Sensitive Cookie in HTTPS Session Without ''Secure'' Attribute'
2781
+ owasp:
2782
+ - A05:2021 - Security Misconfiguration
2783
+ category: security
2784
+ subcategory:
2785
+ - vuln
2786
+ technology:
2787
+ - flask
2788
+ likelihood: MEDIUM
2789
+ impact: MEDIUM
2790
+ confidence: MEDIUM
2791
+ vulnerability_class:
2792
+ - Cookie Security
2793
+ patterns:
2794
+ - pattern: $RESP.set_cookie($NAME, $VALUE, ...)
2795
+ - pattern-not: $RESP.set_cookie($NAME, $VALUE, ..., secure=True, ...)
2796
+ - pattern-not: $RESP.set_cookie($NAME, $VALUE, ..., secure=$X, ...)
2797
+ - id: narvy.python.session.password-compared-with-equality.password-compared-with-equality
2798
+ message: A value named like a password/token/secret is compared with `==` instead of a constant-time
2799
+ comparison. Python's `==` on strings short-circuits on the first differing byte, so the comparison
2800
+ time leaks how many leading characters were correct - a timing side channel an attacker can exploit
2801
+ to recover the secret byte by byte (CWE-208). Use `hmac.compare_digest(a, b)` instead.
2802
+ severity: WARNING
2803
+ languages:
2804
+ - python
2805
+ metadata:
2806
+ cwe:
2807
+ - 'CWE-208: Observable Timing Discrepancy'
2808
+ owasp:
2809
+ - A02:2021 - Cryptographic Failures
2810
+ category: security
2811
+ subcategory:
2812
+ - vuln
2813
+ technology:
2814
+ - python
2815
+ likelihood: LOW
2816
+ impact: MEDIUM
2817
+ confidence: LOW
2818
+ vulnerability_class:
2819
+ - Improper Authentication
2820
+ patterns:
2821
+ - pattern-either:
2822
+ - pattern: "if $STORED_SECRET == $PROVIDED_SECRET:\n ...\n"
2823
+ - pattern: "if $PROVIDED_SECRET == $STORED_SECRET:\n ...\n"
2824
+ - metavariable-regex:
2825
+ metavariable: $STORED_SECRET
2826
+ regex: (?i)\.(password|passwd|pwd|secret|token|api_key|signature|hmac|mac)$
2827
+ - pattern-not-inside: '$R = hmac.compare_digest(...)
2828
+
2829
+ ...
2830
+
2831
+ '
2832
+ - id: narvy.python.sqli.django-extra-tainted.django-extra-tainted
2833
+ message: QuerySet `.extra()` is called with a `where=`/`select=`/`tables=` clause built by string interpolation.
2834
+ `.extra()` SQL fragments are inserted into the query almost verbatim; any interpolated variable is
2835
+ SQL injection (CWE-89). `.extra()` is deprecated for this exact reason - use the ORM's expression
2836
+ API or `.raw()` with bound `params`.
2837
+ severity: ERROR
2838
+ languages:
2839
+ - python
2840
+ metadata:
2841
+ cwe:
2842
+ - 'CWE-89: Improper Neutralization of Special Elements used in an SQL Command (''SQL Injection'')'
2843
+ owasp:
2844
+ - A03:2021 - Injection
2845
+ category: security
2846
+ subcategory:
2847
+ - vuln
2848
+ technology:
2849
+ - django
2850
+ likelihood: HIGH
2851
+ impact: HIGH
2852
+ confidence: MEDIUM
2853
+ vulnerability_class:
2854
+ - SQL Injection
2855
+ patterns:
2856
+ - pattern-either:
2857
+ - pattern: $QS.extra(where=[f"...{$VAR}..."])
2858
+ - pattern: $QS.extra(where=["..." + $VAR])
2859
+ - pattern: '$QS.extra(select={$K: f"...{$VAR}..."})'
2860
+ - id: narvy.python.sqli.django-queryset-raw-tainted.django-queryset-raw-tainted
2861
+ message: '`Model.objects.raw()` is called with a SQL string built by concatenation or interpolation
2862
+ rather than passed as a fixed literal with the `params=` argument. Django''s `.raw()` does not auto-parameterize
2863
+ the SQL text itself - only the `params` list is safely bound. SQL injection (CWE-89). Use `Model.objects.raw("...
2864
+ WHERE id = %s", [user_id])`.'
2865
+ severity: ERROR
2866
+ languages:
2867
+ - python
2868
+ metadata:
2869
+ cwe:
2870
+ - 'CWE-89: Improper Neutralization of Special Elements used in an SQL Command (''SQL Injection'')'
2871
+ owasp:
2872
+ - A03:2021 - Injection
2873
+ category: security
2874
+ subcategory:
2875
+ - vuln
2876
+ technology:
2877
+ - django
2878
+ likelihood: HIGH
2879
+ impact: HIGH
2880
+ confidence: HIGH
2881
+ vulnerability_class:
2882
+ - SQL Injection
2883
+ patterns:
2884
+ - pattern-either:
2885
+ - pattern: $MODEL.objects.raw("..." + $VAR)
2886
+ - pattern: $MODEL.objects.raw($VAR + "...")
2887
+ - pattern: $MODEL.objects.raw(f"...{$VAR}...")
2888
+ - pattern: $MODEL.objects.raw("...".format(...))
2889
+ - pattern: $MODEL.objects.raw("..." % $VAR)
2890
+ - id: narvy.python.sqli.pymongo-eval-where-injection.pymongo-eval-where-injection
2891
+ message: MongoDB `$where` clause (or `db.eval()`) built with a tainted string. `$where` executes JavaScript
2892
+ server-side against every document; an attacker-controlled fragment here is server-side NoSQL code
2893
+ injection, not just a filter bypass (CWE-943, CWE-94). Use standard MongoDB query operators instead
2894
+ of `$where`.
2895
+ severity: ERROR
2896
+ languages:
2897
+ - python
2898
+ metadata:
2899
+ cwe:
2900
+ - 'CWE-943: Improper Neutralization of Special Elements in Data Query Logic'
2901
+ - 'CWE-94: Improper Control of Generation of Code (''Code Injection'')'
2902
+ owasp:
2903
+ - A03:2021 - Injection
2904
+ category: security
2905
+ subcategory:
2906
+ - vuln
2907
+ technology:
2908
+ - pymongo
2909
+ - mongodb
2910
+ likelihood: MEDIUM
2911
+ impact: HIGH
2912
+ confidence: MEDIUM
2913
+ vulnerability_class:
2914
+ - SQL Injection
2915
+ patterns:
2916
+ - pattern-either:
2917
+ - pattern: '{"$where": f"...{$VAR}..."}'
2918
+ - pattern: '{"$where": "..." + $VAR}'
2919
+ - pattern: $DB.eval(f"...{$VAR}...")
2920
+ - pattern: $DB.eval("..." + $VAR)
2921
+ - id: narvy.python.sqli.sqlalchemy-execute-string-concat.sqlalchemy-execute-string-concat
2922
+ message: '`.execute()` on a SQLAlchemy Connection/Session/Engine called with a raw string built by concatenation
2923
+ or f-string rather than a parameterized `text()` construct with bound params. SQL injection (CWE-89).'
2924
+ severity: ERROR
2925
+ languages:
2926
+ - python
2927
+ metadata:
2928
+ cwe:
2929
+ - 'CWE-89: Improper Neutralization of Special Elements used in an SQL Command (''SQL Injection'')'
2930
+ owasp:
2931
+ - A03:2021 - Injection
2932
+ category: security
2933
+ subcategory:
2934
+ - vuln
2935
+ technology:
2936
+ - sqlalchemy
2937
+ likelihood: HIGH
2938
+ impact: HIGH
2939
+ confidence: MEDIUM
2940
+ vulnerability_class:
2941
+ - SQL Injection
2942
+ patterns:
2943
+ - pattern-either:
2944
+ - pattern: $SESSION.execute("..." + $VAR)
2945
+ - pattern: $SESSION.execute(f"...{$VAR}...")
2946
+ - pattern: $ENGINE.connect().execute("..." + $VAR)
2947
+ - metavariable-regex:
2948
+ metavariable: $SESSION
2949
+ regex: (?i)(session|conn|connection|engine)
2950
+ - id: narvy.python.sqli.sqlalchemy-text-tainted.sqlalchemy-text-tainted
2951
+ message: 'SQLAlchemy `text()` construct built by string interpolation rather than using bound parameters
2952
+ (`text("... WHERE id = :id").bindparams(id=x)` or `conn.execute(text(...), {"id": x})`). Interpolating
2953
+ a variable directly into the SQL text defeats SQLAlchemy''s parameterization and is SQL injection
2954
+ (CWE-89).'
2955
+ severity: ERROR
2956
+ languages:
2957
+ - python
2958
+ metadata:
2959
+ cwe:
2960
+ - 'CWE-89: Improper Neutralization of Special Elements used in an SQL Command (''SQL Injection'')'
2961
+ owasp:
2962
+ - A03:2021 - Injection
2963
+ category: security
2964
+ subcategory:
2965
+ - vuln
2966
+ technology:
2967
+ - sqlalchemy
2968
+ likelihood: HIGH
2969
+ impact: HIGH
2970
+ confidence: HIGH
2971
+ vulnerability_class:
2972
+ - SQL Injection
2973
+ patterns:
2974
+ - pattern-either:
2975
+ - pattern: text(f"...{$VAR}...")
2976
+ - pattern: text("..." + $VAR)
2977
+ - pattern: text($VAR + "...")
2978
+ - pattern: text("...".format($VAR))
2979
+ - pattern: text("..." % $VAR)
2980
+ - pattern-not: text("...")
2981
+ - id: narvy.python.sqli.formatted-sql-string-to-execute.formatted-sql-string-to-execute
2982
+ message: >-
2983
+ A SQL query built with Python string formatting (%, .format(), an f-string
2984
+ interpolation, or + concatenation) reaches cursor.execute / cursor.executemany.
2985
+ If any interpolated value is attacker-controlled this is SQL injection (CWE-89).
2986
+ Build queries with bound parameters instead: execute(sql, (params,)) or use an
2987
+ ORM with bound parameters.
2988
+ severity: ERROR
2989
+ languages:
2990
+ - python
2991
+ mode: taint
2992
+ metadata:
2993
+ cwe:
2994
+ - 'CWE-89: Improper Neutralization of Special Elements used in an SQL Command (''SQL Injection'')'
2995
+ owasp:
2996
+ - 'A03:2021 - Injection'
2997
+ category: security
2998
+ subcategory:
2999
+ - audit
3000
+ technology:
3001
+ - python
3002
+ - sqlite3
3003
+ - psycopg2
3004
+ - mysql-connector
3005
+ - aiopg
3006
+ - asyncpg
3007
+ - pymysql
3008
+ likelihood: MEDIUM
3009
+ impact: HIGH
3010
+ confidence: MEDIUM
3011
+ vulnerability_class:
3012
+ - Improper Validation
3013
+ references:
3014
+ - https://cwe.mitre.org/data/definitions/89.html
3015
+ pattern-sources:
3016
+ - patterns:
3017
+ - pattern: '"$SQL" % $X'
3018
+ - metavariable-regex:
3019
+ metavariable: $SQL
3020
+ regex: (?i).*\b(select|insert\s+into|update|delete\s+from|drop\s+table|alter\s+table|create\s+table|merge|truncate|where|values|set|from)\b.*
3021
+ - patterns:
3022
+ - pattern: '"$SQL".format(...)'
3023
+ - metavariable-regex:
3024
+ metavariable: $SQL
3025
+ regex: (?i).*\b(select|insert\s+into|update|delete\s+from|drop\s+table|alter\s+table|create\s+table|merge|truncate|where|values|set|from)\b.*
3026
+ - pattern: 'f"...{$X}..."'
3027
+ - patterns:
3028
+ - pattern: '"$SQL" + $X'
3029
+ - metavariable-regex:
3030
+ metavariable: $SQL
3031
+ regex: (?i).*\b(select|insert\s+into|update|delete\s+from|drop\s+table|alter\s+table|create\s+table|merge|truncate|where|values|set|from)\b.*
3032
+ pattern-sinks:
3033
+ - pattern: $CURSOR.execute($QUERY, ...)
3034
+ - pattern: $CURSOR.execute($QUERY)
3035
+ - pattern: $CURSOR.executemany($QUERY, ...)
3036
+ - pattern: $CURSOR.executemany($QUERY)
3037
+ - id: narvy.python.ssrf.httpx-tainted-url.httpx-tainted-url
3038
+ message: '`httpx` client call with a URL built directly from request data. Same SSRF risk (CWE-918)
3039
+ as `requests`/`urllib` - httpx is increasingly the default HTTP client in async FastAPI codebases
3040
+ and is just as exploitable if the URL/host is attacker-controlled.'
3041
+ severity: ERROR
3042
+ languages:
3043
+ - python
3044
+ metadata:
3045
+ cwe:
3046
+ - 'CWE-918: Server-Side Request Forgery (SSRF)'
3047
+ owasp:
3048
+ - A10:2021 - Server-Side Request Forgery
3049
+ category: security
3050
+ subcategory:
3051
+ - vuln
3052
+ technology:
3053
+ - python
3054
+ - httpx
3055
+ - fastapi
3056
+ likelihood: HIGH
3057
+ impact: HIGH
3058
+ confidence: MEDIUM
3059
+ vulnerability_class:
3060
+ - Server-Side Request Forgery (SSRF)
3061
+ patterns:
3062
+ - pattern-either:
3063
+ - pattern: httpx.$METHOD($URL)
3064
+ - pattern: await $CLIENT.$METHOD($URL)
3065
+ - metavariable-regex:
3066
+ metavariable: $URL
3067
+ regex: (?i)(request\.(args|form|json|values|get)|query_params|path_params)
3068
+ - metavariable-regex:
3069
+ metavariable: $METHOD
3070
+ regex: ^(get|post|put|delete|head|patch|request)$
3071
+ - id: narvy.python.ssrf.requests-tainted-url.requests-tainted-url
3072
+ message: '`requests.$METHOD()` is called with a URL built directly from request data (`request.args`,
3073
+ `request.form`, `request.json`, a Django `request.GET`, or a FastAPI path/query parameter). Without
3074
+ validating the target host against an allowlist, an attacker can point this request at internal services
3075
+ (cloud metadata endpoints, admin panels, internal APIs) that are not otherwise reachable from the
3076
+ internet. SSRF (CWE-918).'
3077
+ severity: ERROR
3078
+ languages:
3079
+ - python
3080
+ metadata:
3081
+ cwe:
3082
+ - 'CWE-918: Server-Side Request Forgery (SSRF)'
3083
+ owasp:
3084
+ - A10:2021 - Server-Side Request Forgery
3085
+ category: security
3086
+ subcategory:
3087
+ - vuln
3088
+ technology:
3089
+ - python
3090
+ - requests
3091
+ - flask
3092
+ - django
3093
+ likelihood: HIGH
3094
+ impact: HIGH
3095
+ confidence: MEDIUM
3096
+ vulnerability_class:
3097
+ - Server-Side Request Forgery (SSRF)
3098
+ patterns:
3099
+ - pattern-either:
3100
+ - pattern: requests.$METHOD(request.$KIND.$ATTR(...))
3101
+ - pattern: requests.$METHOD(request.$KIND[$X])
3102
+ - pattern: requests.$METHOD(request.$KIND.get(...))
3103
+ - pattern: requests.$METHOD(f"...{request.$KIND.$ATTR(...)}...")
3104
+ - pattern: requests.$METHOD("..." + request.$KIND.$ATTR(...))
3105
+ - pattern: '$URL = request.$KIND.$ATTR(...)
3106
+
3107
+ ...
3108
+
3109
+ requests.$METHOD($URL)
3110
+
3111
+ '
3112
+ - pattern: '$URL = request.$KIND[$X]
3113
+
3114
+ ...
3115
+
3116
+ requests.$METHOD($URL)
3117
+
3118
+ '
3119
+ - metavariable-regex:
3120
+ metavariable: $METHOD
3121
+ regex: ^(get|post|put|delete|head|patch|request)$
3122
+ - metavariable-regex:
3123
+ metavariable: $KIND
3124
+ regex: ^(args|form|json|values|GET|POST|query_params|path_params)$
3125
+ - id: narvy.python.ssrf.tainted-host-only.tainted-host-only
3126
+ message: A request's target *host* (not the full URL) is built from request data and then f-string-interpolated
3127
+ into a `requests`/`urllib` call (`f"http://{host}/..."`). Restricting only the path while letting
3128
+ the host be attacker-controlled is still full SSRF (CWE-918) - validate the host against an allowlist
3129
+ before building the URL.
3130
+ severity: ERROR
3131
+ languages:
3132
+ - python
3133
+ metadata:
3134
+ cwe:
3135
+ - 'CWE-918: Server-Side Request Forgery (SSRF)'
3136
+ owasp:
3137
+ - A10:2021 - Server-Side Request Forgery
3138
+ category: security
3139
+ subcategory:
3140
+ - vuln
3141
+ technology:
3142
+ - python
3143
+ - requests
3144
+ likelihood: MEDIUM
3145
+ impact: HIGH
3146
+ confidence: MEDIUM
3147
+ vulnerability_class:
3148
+ - Server-Side Request Forgery (SSRF)
3149
+ patterns:
3150
+ - pattern-either:
3151
+ - pattern: '$HOST = request.$KIND.$ATTR(...)
3152
+
3153
+ ...
3154
+
3155
+ requests.$METHOD(f"http://{$HOST}$REST")
3156
+
3157
+ '
3158
+ - pattern: '$HOST = request.$KIND.$ATTR(...)
3159
+
3160
+ ...
3161
+
3162
+ requests.$METHOD(f"https://{$HOST}$REST")
3163
+
3164
+ '
3165
+ - metavariable-regex:
3166
+ metavariable: $KIND
3167
+ regex: ^(args|form|json|values|GET|POST)$
3168
+ - id: narvy.python.ssrf.urllib-tainted-url.urllib-tainted-url
3169
+ message: '`urllib.request.urlopen()` is called with a URL built directly from request data. Same SSRF
3170
+ risk as the `requests`-library case (CWE-918): the server will fetch whatever host/path the attacker
3171
+ supplies, including internal-only addresses. Validate the target against an allowlist of hosts, or
3172
+ resolve and check the IP is not in a private/link-local range before fetching.'
3173
+ severity: ERROR
3174
+ languages:
3175
+ - python
3176
+ metadata:
3177
+ cwe:
3178
+ - 'CWE-918: Server-Side Request Forgery (SSRF)'
3179
+ owasp:
3180
+ - A10:2021 - Server-Side Request Forgery
3181
+ category: security
3182
+ subcategory:
3183
+ - vuln
3184
+ technology:
3185
+ - python
3186
+ likelihood: HIGH
3187
+ impact: HIGH
3188
+ confidence: MEDIUM
3189
+ vulnerability_class:
3190
+ - Server-Side Request Forgery (SSRF)
3191
+ patterns:
3192
+ - pattern-either:
3193
+ - pattern: urllib.request.urlopen(request.$KIND.$ATTR(...))
3194
+ - pattern: urllib.request.urlopen(request.$KIND[$X])
3195
+ - pattern: urlopen(request.$KIND.$ATTR(...))
3196
+ - pattern: '$URL = request.$KIND.$ATTR(...)
3197
+
3198
+ ...
3199
+
3200
+ urllib.request.urlopen($URL)
3201
+
3202
+ '
3203
+ - metavariable-regex:
3204
+ metavariable: $KIND
3205
+ regex: ^(args|form|json|values|GET|POST)$
3206
+ - id: narvy.python.transport.bind-all-interfaces.bind-all-interfaces
3207
+ message: Server socket / dev-server binds to `0.0.0.0` (all interfaces) explicitly in application code.
3208
+ This exposes the service on every network interface, including ones only meant to be internal/loopback,
3209
+ bypassing whatever network-level restriction the deployment intended (CWE-668). Bind to a specific
3210
+ interface, or make the bind address configurable via environment and default it to `127.0.0.1` for
3211
+ anything not meant to be internet-facing.
3212
+ severity: WARNING
3213
+ languages:
3214
+ - python
3215
+ metadata:
3216
+ cwe:
3217
+ - 'CWE-668: Exposure of Resource to Wrong Sphere'
3218
+ owasp:
3219
+ - A05:2021 - Security Misconfiguration
3220
+ category: security
3221
+ subcategory:
3222
+ - vuln
3223
+ technology:
3224
+ - python
3225
+ - flask
3226
+ likelihood: LOW
3227
+ impact: MEDIUM
3228
+ confidence: LOW
3229
+ vulnerability_class:
3230
+ - Mishandled Sensitive Information
3231
+ patterns:
3232
+ - pattern-either:
3233
+ - pattern: $APP.run(host="0.0.0.0", ...)
3234
+ - pattern: socket.bind(("0.0.0.0", $PORT))
3235
+ - id: narvy.python.transport.cleartext-http-literal.cleartext-http-literal
3236
+ message: Cleartext HTTP URL detected. Traffic to this endpoint is unencrypted and vulnerable to MITM
3237
+ (eavesdropping, tampering). Use HTTPS. CWE-319.
3238
+ severity: WARNING
3239
+ languages:
3240
+ - python
3241
+ metadata:
3242
+ cwe:
3243
+ - 'CWE-319: Cleartext Transmission of Sensitive Information'
3244
+ owasp:
3245
+ - A02:2021 - Cryptographic Failures
3246
+ - A03:2017 - Sensitive Data Exposure
3247
+ category: security
3248
+ subcategory:
3249
+ - vuln
3250
+ technology:
3251
+ - python
3252
+ likelihood: MEDIUM
3253
+ impact: MEDIUM
3254
+ confidence: HIGH
3255
+ vulnerability_class:
3256
+ - Insecure Communication
3257
+ references:
3258
+ - https://cwe.mitre.org/data/definitions/319.html
3259
+ - https://owasp.org/www-community/vulnerabilities/Insecure_Transport
3260
+ paths:
3261
+ exclude:
3262
+ - '**/fuzzer/payloads.py'
3263
+ - '**/fuzzer/**/payloads*.py'
3264
+ - '**/oob/blind_payloads.py'
3265
+ - '**/*_analyzer.py'
3266
+ - '**/payloads/*.py'
3267
+ patterns:
3268
+ - pattern-either:
3269
+ - pattern: '"$URL"'
3270
+ - pattern: '''$URL'''
3271
+ - metavariable-regex:
3272
+ metavariable: $URL
3273
+ regex: ^http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0|169\.254\.|10\.\d+\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|\[::1\]|[a-zA-Z0-9.-]+\.(local|internal|test|example)(/|$|:)|schemas\.|www\.w3\.org|xmlns\.com|purl\.org|ns\.adobe\.com|java\.sun\.com|jabber\.org|x\.org|evil\.com)[a-zA-Z0-9._~:/?#\[\]@!$&'()*+,;=%-]+$
3274
+ - id: narvy.python.xpath-injection.lxml-xpath-tainted.lxml-xpath-tainted
3275
+ message: An XPath expression evaluated by `lxml`/`xml.etree`'s `.xpath()` is built by string interpolation
3276
+ with request data. Like SQL, XPath has no built-in parameter binding for string-built queries; an
3277
+ attacker can inject expression syntax (`' or '1'='1`) to select unintended nodes or bypass an authentication
3278
+ check implemented as an XPath query. CWE-643 (XPath Injection). Use `.xpath(expr, var=$VAR)` with
3279
+ XPath variable binding instead of interpolating the value into the expression string.
3280
+ severity: ERROR
3281
+ languages:
3282
+ - python
3283
+ metadata:
3284
+ cwe:
3285
+ - 'CWE-643: Improper Neutralization of Data within XPath Expressions (''XPath Injection'')'
3286
+ owasp:
3287
+ - A03:2021 - Injection
3288
+ category: security
3289
+ subcategory:
3290
+ - vuln
3291
+ technology:
3292
+ - lxml
3293
+ - python
3294
+ likelihood: MEDIUM
3295
+ impact: MEDIUM
3296
+ confidence: MEDIUM
3297
+ vulnerability_class:
3298
+ - Improper Validation
3299
+ patterns:
3300
+ - pattern-either:
3301
+ - pattern: $TREE.xpath(f"...{$VAR}...")
3302
+ - pattern: $TREE.xpath("..." + $VAR)
3303
+ - pattern: $TREE.xpath("...".format($VAR))
3304
+ - pattern: $TREE.xpath("..." % $VAR)
3305
+ - pattern-not: $TREE.xpath("...")
3306
+ - id: narvy.python.xss.django-context-autoescape-disabled.django-context-autoescape-disabled
3307
+ message: A Django template `Context` is constructed with `autoescape=False`. Every variable rendered
3308
+ through this context skips Django's automatic HTML escaping (CWE-79). If any of the rendered data
3309
+ originates from a request, a database row a user can edit, or any other attacker-influenced source,
3310
+ this is XSS. Remove the `autoescape=False` argument, or escape the specific values that need to stay
3311
+ unescaped with `mark_safe()` after running them through `bleach.clean()` or similar.
3312
+ severity: WARNING
3313
+ languages:
3314
+ - python
3315
+ metadata:
3316
+ cwe:
3317
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
3318
+ owasp:
3319
+ - A03:2021 - Injection
3320
+ category: security
3321
+ subcategory:
3322
+ - vuln
3323
+ technology:
3324
+ - django
3325
+ likelihood: LOW
3326
+ impact: HIGH
3327
+ confidence: MEDIUM
3328
+ vulnerability_class:
3329
+ - Cross-Site-Scripting (XSS)
3330
+ patterns:
3331
+ - pattern-either:
3332
+ - pattern: Context(..., autoescape=False, ...)
3333
+ - pattern: RequestContext(..., autoescape=False, ...)
3334
+ - id: narvy.python.xss.django-html-email-body-tainted.django-html-email-body-tainted
3335
+ message: An `EmailMessage` whose `content_subtype` is set to `"html"` is built from request data. Mail
3336
+ clients render this body as raw HTML with no equivalent of Django's template auto-escaping applied
3337
+ - if an attacker controls the value, they control the HTML/script that renders in whoever's inbox
3338
+ receives it (CWE-79, email-delivered XSS). Escape the value with `django.utils.html.escape()` before
3339
+ interpolating it into the HTML body, or build the email through a Django template (auto-escaped) instead
3340
+ of raw string concatenation/formatting.
3341
+ severity: WARNING
3342
+ languages:
3343
+ - python
3344
+ metadata:
3345
+ cwe:
3346
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
3347
+ owasp:
3348
+ - A03:2021 - Injection
3349
+ category: security
3350
+ subcategory:
3351
+ - vuln
3352
+ technology:
3353
+ - django
3354
+ likelihood: MEDIUM
3355
+ impact: MEDIUM
3356
+ confidence: MEDIUM
3357
+ vulnerability_class:
3358
+ - Cross-Site-Scripting (XSS)
3359
+ patterns:
3360
+ - pattern-inside: "def $FUNC(...):\n ...\n $EMAIL.content_subtype = \"html\"\n ...\n"
3361
+ - pattern-either:
3362
+ - pattern: django.core.mail.EmailMessage($SUBJ, request.$KIND.$ATTR(...), ...)
3363
+ - pattern: EmailMessage($SUBJ, request.$KIND.$ATTR(...), ...)
3364
+ - pattern: '$BODY = request.$KIND.$ATTR(...)
3365
+
3366
+ ...
3367
+
3368
+ django.core.mail.EmailMessage($SUBJ, $BODY, ...)
3369
+
3370
+ '
3371
+ - pattern: '$BODY = request.$KIND.$ATTR(...)
3372
+
3373
+ ...
3374
+
3375
+ EmailMessage($SUBJ, $BODY, ...)
3376
+
3377
+ '
3378
+ - metavariable-regex:
3379
+ metavariable: $KIND
3380
+ regex: ^(GET|POST|data|query_params)$
3381
+ - id: narvy.python.xss.django-httpresponse-echoes-request-data.django-httpresponse-echoes-request-data
3382
+ message: '`HttpResponse()` built by interpolating `request.GET`/`request.POST` directly into the response
3383
+ body instead of going through a Django template (which auto-escapes) or `django.utils.html.escape()`.
3384
+ Reflected XSS (CWE-79).'
3385
+ severity: ERROR
3386
+ languages:
3387
+ - python
3388
+ metadata:
3389
+ cwe:
3390
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
3391
+ owasp:
3392
+ - A03:2021 - Injection
3393
+ category: security
3394
+ subcategory:
3395
+ - vuln
3396
+ technology:
3397
+ - django
3398
+ likelihood: HIGH
3399
+ impact: MEDIUM
3400
+ confidence: MEDIUM
3401
+ vulnerability_class:
3402
+ - Cross-Site-Scripting (XSS)
3403
+ patterns:
3404
+ - pattern-either:
3405
+ - pattern: HttpResponse(f"...{request.$KIND.$ATTR(...)}...")
3406
+ - pattern: HttpResponse("..." + request.$KIND.$ATTR(...))
3407
+ - pattern: HttpResponse("...".format(request.$KIND.$ATTR(...)))
3408
+ - pattern: HttpResponse("..." % request.$KIND.$ATTR(...))
3409
+ - metavariable-regex:
3410
+ metavariable: $KIND
3411
+ regex: ^(GET|POST|COOKIES|META)$
3412
+ - id: narvy.python.xss.django-mark-safe-html-email-body.django-mark-safe-html-email-body
3413
+ message: '`mark_safe()` output is passed directly as an HTML email body (`EmailMessage` with `content_subtype
3414
+ = "html"`, or `send_mail(..., html_message=...)`). `mark_safe()` skips Django''s escaping, so whatever
3415
+ went into it reaches the recipient''s mail client verbatim (CWE-79). Only wrap already-sanitized HTML
3416
+ (e.g. from `bleach.clean()`) with `mark_safe()`, never a raw concatenation/format string built from
3417
+ request or database data.'
3418
+ severity: WARNING
3419
+ languages:
3420
+ - python
3421
+ metadata:
3422
+ cwe:
3423
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
3424
+ owasp:
3425
+ - A03:2021 - Injection
3426
+ category: security
3427
+ subcategory:
3428
+ - audit
3429
+ technology:
3430
+ - django
3431
+ likelihood: LOW
3432
+ impact: MEDIUM
3433
+ confidence: LOW
3434
+ vulnerability_class:
3435
+ - Cross-Site-Scripting (XSS)
3436
+ patterns:
3437
+ - pattern-either:
3438
+ - patterns:
3439
+ - pattern-inside: "def $FUNC(...):\n ...\n $EMAIL.content_subtype = \"html\"\n ...\n"
3440
+ - pattern: '$BODY = mark_safe(...)
3441
+
3442
+ ...
3443
+
3444
+ django.core.mail.EmailMessage($SUBJ, $BODY, ...)
3445
+
3446
+ '
3447
+ - patterns:
3448
+ - pattern-inside: "def $FUNC(...):\n ...\n $EMAIL.content_subtype = \"html\"\n ...\n"
3449
+ - pattern: '$BODY = mark_safe(...)
3450
+
3451
+ ...
3452
+
3453
+ EmailMessage($SUBJ, $BODY, ...)
3454
+
3455
+ '
3456
+ - pattern: '$BODY = mark_safe(...)
3457
+
3458
+ ...
3459
+
3460
+ send_mail(..., html_message=$BODY, ...)
3461
+
3462
+ '
3463
+ - id: narvy.python.xss.django-mark-safe-tainted.django-mark-safe-tainted
3464
+ message: '`mark_safe()` is applied to a string built by interpolating a variable that is not passed
3465
+ through `escape()`/`conditional_escape()` first. `mark_safe()` tells Django''s template engine to
3466
+ skip auto-escaping entirely - any HTML/JS in the unescaped interpolated value reaches the browser
3467
+ verbatim (CWE-79). If the variable is fully attacker-controlled this is XSS; if it is only developer/admin-controlled,
3468
+ escaping it anyway costs nothing and keeps the call reviewable without having to trace where every
3469
+ value came from. Wrap the interpolated part in `escape(...)`, or build the HTML through a template
3470
+ instead of `mark_safe()`.'
3471
+ severity: WARNING
3472
+ languages:
3473
+ - python
3474
+ metadata:
3475
+ cwe:
3476
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
3477
+ owasp:
3478
+ - A03:2021 - Injection
3479
+ category: security
3480
+ subcategory:
3481
+ - audit
3482
+ technology:
3483
+ - django
3484
+ likelihood: MEDIUM
3485
+ impact: MEDIUM
3486
+ confidence: LOW
3487
+ vulnerability_class:
3488
+ - Cross-Site-Scripting (XSS)
3489
+ patterns:
3490
+ - pattern-either:
3491
+ - pattern: mark_safe(f"...{$VAR}...")
3492
+ - pattern: mark_safe("..." + $VAR)
3493
+ - pattern: mark_safe("...".format($VAR))
3494
+ - pattern: mark_safe("..." % $VAR)
3495
+ - pattern-not: mark_safe("...")
3496
+ - pattern-not: mark_safe(f"...{escape($VAR)}...")
3497
+ - pattern-not: mark_safe(f"...{conditional_escape($VAR)}...")
3498
+ - pattern-not: mark_safe("..." + escape($VAR))
3499
+ - pattern-not: mark_safe("...".format(escape($VAR)))
3500
+ - id: narvy.python.xss.django-send-mail-html-message-tainted.django-send-mail-html-message-tainted
3501
+ message: Django's `send_mail(...)` is called with `html_message=` built from request data. `html_message`
3502
+ is rendered as raw HTML by the recipient's mail client with no auto-escaping applied (CWE-79, email-delivered
3503
+ XSS). Escape the value with `django.utils.html.escape()` first, or render it through a Django template.
3504
+ severity: WARNING
3505
+ languages:
3506
+ - python
3507
+ metadata:
3508
+ cwe:
3509
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
3510
+ owasp:
3511
+ - A03:2021 - Injection
3512
+ category: security
3513
+ subcategory:
3514
+ - vuln
3515
+ technology:
3516
+ - django
3517
+ likelihood: MEDIUM
3518
+ impact: MEDIUM
3519
+ confidence: MEDIUM
3520
+ vulnerability_class:
3521
+ - Cross-Site-Scripting (XSS)
3522
+ patterns:
3523
+ - pattern-either:
3524
+ - pattern: send_mail(..., html_message=request.$KIND.$ATTR(...), ...)
3525
+ - pattern: django.core.mail.send_mail(..., html_message=request.$KIND.$ATTR(...), ...)
3526
+ - pattern: '$BODY = request.$KIND.$ATTR(...)
3527
+
3528
+ ...
3529
+
3530
+ send_mail(..., html_message=$BODY, ...)
3531
+
3532
+ '
3533
+ - metavariable-regex:
3534
+ metavariable: $KIND
3535
+ regex: ^(GET|POST|data|query_params)$
3536
+ - id: narvy.python.xss.django-template-tag-autoescape-off.django-template-tag-autoescape-off
3537
+ message: A Django template file contains `{% autoescape off %}`. Every variable rendered inside this
3538
+ block skips Django's automatic HTML escaping until the matching `{% endautoescape %}` (CWE-79). If
3539
+ any variable rendered in that block can contain attacker-controlled text, this is XSS. Remove the
3540
+ block, or wrap only the specific value that is genuinely pre-sanitized HTML with `|safe` / `mark_safe()`
3541
+ in Python instead of disabling escaping for the whole region.
3542
+ severity: WARNING
3543
+ languages:
3544
+ - generic
3545
+ paths:
3546
+ include:
3547
+ - '*.html'
3548
+ metadata:
3549
+ cwe:
3550
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
3551
+ owasp:
3552
+ - A03:2021 - Injection
3553
+ category: security
3554
+ subcategory:
3555
+ - vuln
3556
+ technology:
3557
+ - django
3558
+ likelihood: LOW
3559
+ impact: MEDIUM
3560
+ confidence: LOW
3561
+ vulnerability_class:
3562
+ - Cross-Site-Scripting (XSS)
3563
+ pattern-regex: \{%\s*autoescape\s+off\s*%\}
3564
+ - id: narvy.python.xss.django-templates-setting-autoescape-disabled.django-templates-setting-autoescape-disabled
3565
+ message: 'Django''s `TEMPLATES` setting sets `''autoescape'': False` in a backend''s `OPTIONS`. This
3566
+ turns off automatic HTML escaping for every template rendered through this backend project-wide -
3567
+ any template variable that is not manually wrapped in `escape()` becomes an XSS sink (CWE-79). Remove
3568
+ the `autoescape` override (it defaults to `True`), or escape every interpolated value by hand if you
3569
+ truly need it off.'
3570
+ severity: WARNING
3571
+ languages:
3572
+ - python
3573
+ metadata:
3574
+ cwe:
3575
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
3576
+ owasp:
3577
+ - A03:2021 - Injection
3578
+ category: security
3579
+ subcategory:
3580
+ - vuln
3581
+ technology:
3582
+ - django
3583
+ likelihood: LOW
3584
+ impact: HIGH
3585
+ confidence: HIGH
3586
+ vulnerability_class:
3587
+ - Cross-Site-Scripting (XSS)
3588
+ patterns:
3589
+ - pattern-either:
3590
+ - pattern: '{..., ''OPTIONS'': {..., ''autoescape'': False, ...}, ...}
3591
+
3592
+ '
3593
+ - pattern: '{..., "OPTIONS": {..., "autoescape": False, ...}, ...}
3594
+
3595
+ '
3596
+ - id: narvy.python.xss.flask-raw-html-response.flask-raw-html-response
3597
+ message: A Flask response is built from a non-literal, non-template-rendered string with an explicit
3598
+ `text/html` mimetype/content_type. Nothing escapes this body before it reaches the browser. If the
3599
+ string contains attacker-controlled data this is reflected XSS (CWE-79). Render through `render_template`
3600
+ (Jinja2 auto-escapes by default) or explicitly escape with `markupsafe.escape()` before concatenation.
3601
+ severity: WARNING
3602
+ languages:
3603
+ - python
3604
+ metadata:
3605
+ cwe:
3606
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
3607
+ owasp:
3608
+ - A03:2021 - Injection
3609
+ category: security
3610
+ subcategory:
3611
+ - audit
3612
+ technology:
3613
+ - python
3614
+ - flask
3615
+ likelihood: LOW
3616
+ impact: HIGH
3617
+ confidence: LOW
3618
+ vulnerability_class:
3619
+ - Cross-Site-Scripting (XSS)
3620
+ patterns:
3621
+ - pattern-either:
3622
+ - pattern: Response($BODY, mimetype="text/html")
3623
+ - pattern: Response($BODY, content_type="text/html")
3624
+ - pattern-not: Response("...", mimetype="text/html")
3625
+ - pattern-not: Response("...", content_type="text/html")
3626
+ - pattern-not: Response(render_template(...), mimetype="text/html")
3627
+ - id: narvy.python.xss.flask-response-echoes-request-data.flask-response-echoes-request-data
3628
+ message: A Flask view returns an HTML/text response built by directly interpolating request data (`request.args`,
3629
+ `request.form`, `request.values`, ...) instead of rendering it through Jinja2's auto-escaping `render_template()`.
3630
+ A browser reflecting this response executes any HTML/JS the attacker put in the request value. Reflected
3631
+ XSS (CWE-79). Use `render_template()` with the value passed as a context variable, or `markupsafe.escape()`
3632
+ if you must build the string manually.
3633
+ severity: ERROR
3634
+ languages:
3635
+ - python
3636
+ metadata:
3637
+ cwe:
3638
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
3639
+ owasp:
3640
+ - A03:2021 - Injection
3641
+ category: security
3642
+ subcategory:
3643
+ - vuln
3644
+ technology:
3645
+ - flask
3646
+ likelihood: HIGH
3647
+ impact: MEDIUM
3648
+ confidence: MEDIUM
3649
+ vulnerability_class:
3650
+ - Cross-Site-Scripting (XSS)
3651
+ patterns:
3652
+ - pattern-either:
3653
+ - pattern: return f"...{request.$KIND.$ATTR(...)}..."
3654
+ - pattern: return f"...{request.$KIND.$ATTR}..."
3655
+ - pattern: return f"...{request.$KIND[$X]}..."
3656
+ - pattern: return "..." + request.$KIND.$ATTR(...)
3657
+ - pattern: return "...".format(request.$KIND.$ATTR(...))
3658
+ - pattern: return Response(f"...{request.$KIND.$ATTR(...)}...")
3659
+ - pattern: return make_response(f"...{request.$KIND.$ATTR(...)}...")
3660
+ - metavariable-regex:
3661
+ metavariable: $KIND
3662
+ regex: ^(args|form|values|GET|POST|json|cookies|headers)$
3663
+ - id: narvy.python.xss.jinja2-autoescape-disabled.jinja2-autoescape-disabled
3664
+ message: Jinja2 `Environment(...)` is constructed with `autoescape=False`, or omits `autoescape` entirely
3665
+ - which defaults to False for a plain `jinja2.Environment` (unlike Flask's `app.jinja_env`, which
3666
+ turns it on for you). Every variable rendered through templates in this environment is XSS-vulnerable
3667
+ by default (CWE-79). Pass `autoescape=True`, or `select_autoescape([...])` to scope it to HTML/XML
3668
+ template extensions.
3669
+ severity: WARNING
3670
+ languages:
3671
+ - python
3672
+ metadata:
3673
+ cwe:
3674
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
3675
+ owasp:
3676
+ - A03:2021 - Injection
3677
+ category: security
3678
+ subcategory:
3679
+ - vuln
3680
+ technology:
3681
+ - jinja2
3682
+ likelihood: MEDIUM
3683
+ impact: MEDIUM
3684
+ confidence: MEDIUM
3685
+ vulnerability_class:
3686
+ - Cross-Site-Scripting (XSS)
3687
+ patterns:
3688
+ - pattern-either:
3689
+ - pattern: jinja2.Environment(..., autoescape=False, ...)
3690
+ - pattern: Environment(..., autoescape=False, ...)
3691
+ - patterns:
3692
+ - pattern-either:
3693
+ - pattern: jinja2.Environment(loader=..., ...)
3694
+ - pattern: Environment(loader=..., ...)
3695
+ - pattern-not: jinja2.Environment(..., autoescape=$AUTOESCAPE, ...)
3696
+ - pattern-not: Environment(..., autoescape=$AUTOESCAPE, ...)
3697
+ - id: narvy.python.xss.markup-wrap-non-literal.markup-wrap-non-literal
3698
+ message: '`Markup()` wraps a non-literal value, marking it as pre-escaped HTML. Jinja2 will render it
3699
+ unescaped. If the value contains attacker-controlled data this is XSS (CWE-79). Only wrap already-sanitized
3700
+ output (e.g. from `bleach.clean()`) with `Markup`, never raw user input.'
3701
+ severity: ERROR
3702
+ languages:
3703
+ - python
3704
+ metadata:
3705
+ cwe:
3706
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
3707
+ owasp:
3708
+ - A03:2021 - Injection
3709
+ category: security
3710
+ subcategory:
3711
+ - vuln
3712
+ technology:
3713
+ - python
3714
+ - flask
3715
+ - jinja2
3716
+ - markupsafe
3717
+ likelihood: MEDIUM
3718
+ impact: HIGH
3719
+ confidence: MEDIUM
3720
+ vulnerability_class:
3721
+ - Cross-Site-Scripting (XSS)
3722
+ patterns:
3723
+ - pattern-either:
3724
+ - pattern: Markup($X)
3725
+ - pattern: markupsafe.Markup($X)
3726
+ - pattern: flask.Markup($X)
3727
+ - pattern-not: Markup("...")
3728
+ - pattern-not: markupsafe.Markup("...")
3729
+ - pattern-not: flask.Markup("...")
3730
+ - pattern-not: Markup(bleach.clean(...))
3731
+ - pattern-not: Markup($SANITIZER.clean(...))
3732
+ - id: narvy.python.xss.raw-html-fragment-built-from-request-data.raw-html-fragment-built-from-request-data
3733
+ message: A value derived from the HTTP request flows into a string that is built to look like an HTML
3734
+ fragment (an f-string/%-format/.format() call/concatenation containing a literal `<tag ...>`), without
3735
+ going through an escaping function first. Whoever eventually renders this fragment - a template `{%
3736
+ autoescape off %}` block, a hand-rolled response, a non-Django rendering layer (widgets, table columns,
3737
+ admin actions) - gets the attacker's HTML/JS verbatim. CWE-79. Escape the interpolated value with
3738
+ `django.utils.html.escape()` / `markupsafe.escape()` / `html.escape()` before it goes into the fragment,
3739
+ or build the fragment through `format_html()` (Django) / Jinja2 auto-escaping instead of a raw string
3740
+ op.
3741
+ severity: WARNING
3742
+ languages:
3743
+ - python
3744
+ metadata:
3745
+ cwe:
3746
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
3747
+ owasp:
3748
+ - A03:2021 - Injection
3749
+ category: security
3750
+ subcategory:
3751
+ - vuln
3752
+ technology:
3753
+ - python
3754
+ - django
3755
+ - flask
3756
+ likelihood: MEDIUM
3757
+ impact: MEDIUM
3758
+ confidence: LOW
3759
+ vulnerability_class:
3760
+ - Cross-Site-Scripting (XSS)
3761
+ references:
3762
+ - https://cwe.mitre.org/data/definitions/79.html
3763
+ mode: taint
3764
+ pattern-sources:
3765
+ - patterns:
3766
+ - pattern-either:
3767
+ - pattern: request.$KIND.get(...)
3768
+ - pattern: request.$KIND[...]
3769
+ - metavariable-regex:
3770
+ metavariable: $KIND
3771
+ regex: ^(GET|POST|args|form|values|json|COOKIES|META|data)$
3772
+ pattern-sinks:
3773
+ - patterns:
3774
+ - pattern: f"...{$SINK}..."
3775
+ - pattern-not-regex: ^((?!<[a-zA-Z]).)*$
3776
+ - patterns:
3777
+ - pattern: '"...%s..." % $SINK'
3778
+ - pattern-not-regex: ^((?!<[a-zA-Z]).)*$
3779
+ - patterns:
3780
+ - pattern: '"...%s..." % ($SINK, ...)'
3781
+ - pattern-not-regex: ^((?!<[a-zA-Z]).)*$
3782
+ - patterns:
3783
+ - pattern: '"...{}...".format($SINK)'
3784
+ - pattern-not-regex: ^((?!<[a-zA-Z]).)*$
3785
+ - patterns:
3786
+ - pattern: '"...{}...".format(..., $SINK, ...)'
3787
+ - pattern-not-regex: ^((?!<[a-zA-Z]).)*$
3788
+ - patterns:
3789
+ - pattern: '"..." + $SINK'
3790
+ - pattern-not-regex: ^((?!<[a-zA-Z]).)*$
3791
+ - patterns:
3792
+ - pattern: $SINK + "..."
3793
+ - pattern-not-regex: ^((?!<[a-zA-Z]).)*$
3794
+ pattern-sanitizers:
3795
+ - patterns:
3796
+ - pattern: escape(...)
3797
+ - patterns:
3798
+ - pattern: django.utils.html.escape(...)
3799
+ - patterns:
3800
+ - pattern: markupsafe.escape(...)
3801
+ - patterns:
3802
+ - pattern: html.escape(...)
3803
+ - patterns:
3804
+ - pattern: conditional_escape(...)
3805
+ - patterns:
3806
+ - pattern: format_html(...)
3807
+ - id: narvy.python.xss.template-string-render-tainted.render-template-string-tainted
3808
+ message: '`render_template_string()` is called with a template *text* built from request data, rather
3809
+ than a fixed template file. Since the first argument is compiled as a Jinja2 template, an attacker
3810
+ who controls it can inject template syntax (`{{ 7*7 }}`, or worse, `{{ self.__init__.__globals__ ...
3811
+ }}`), which is Server-Side Template Injection - typically full RCE, not just XSS (CWE-94, CWE-79).
3812
+ Never build the template text itself from user input; pass user data only as context variables to
3813
+ a fixed template.'
3814
+ severity: ERROR
3815
+ languages:
3816
+ - python
3817
+ metadata:
3818
+ cwe:
3819
+ - 'CWE-94: Improper Control of Generation of Code (''Code Injection'')'
3820
+ - 'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')'
3821
+ owasp:
3822
+ - A03:2021 - Injection
3823
+ category: security
3824
+ subcategory:
3825
+ - vuln
3826
+ technology:
3827
+ - flask
3828
+ - jinja2
3829
+ likelihood: HIGH
3830
+ impact: HIGH
3831
+ confidence: HIGH
3832
+ vulnerability_class:
3833
+ - Cross-Site-Scripting (XSS)
3834
+ - Code Injection
3835
+ patterns:
3836
+ - pattern-either:
3837
+ - pattern: render_template_string(request.$KIND.$ATTR(...))
3838
+ - pattern: render_template_string(f"...{request.$KIND.$ATTR(...)}...")
3839
+ - pattern: render_template_string("..." + request.$KIND.$ATTR(...))
3840
+ - pattern: '$T = request.$KIND.$ATTR(...)
3841
+
3842
+ ...
3843
+
3844
+ render_template_string($T)
3845
+
3846
+ '
3847
+ - metavariable-regex:
3848
+ metavariable: $KIND
3849
+ regex: ^(args|form|values|GET|POST|json)$
3850
+ - id: narvy.python.deserialization.jsonpickle-decode-untrusted.jsonpickle-decode-untrusted
3851
+ message: '`jsonpickle.decode()` deserializes data that is not a fixed local trusted source. Unlike plain
3852
+ `json.loads()`, jsonpickle can reconstruct arbitrary Python objects (including calling constructors)
3853
+ from the `py/object` markers in its payload - applying the same object-instantiation RCE risk as `pickle`
3854
+ to data that looks like harmless JSON (CWE-502). Use `json.loads()` for untrusted input unless you
3855
+ specifically need full object round-tripping from a trusted source only.'
3856
+ severity: ERROR
3857
+ languages:
3858
+ - python
3859
+ metadata:
3860
+ cwe:
3861
+ - 'CWE-502: Deserialization of Untrusted Data'
3862
+ owasp:
3863
+ - A08:2021 - Software and Data Integrity Failures
3864
+ category: security
3865
+ subcategory:
3866
+ - vuln
3867
+ technology:
3868
+ - python
3869
+ - jsonpickle
3870
+ likelihood: LOW
3871
+ impact: HIGH
3872
+ confidence: MEDIUM
3873
+ vulnerability_class:
3874
+ - Insecure Deserialization
3875
+ patterns:
3876
+ - pattern: jsonpickle.decode($DATA)
3877
+ - metavariable-regex:
3878
+ metavariable: $DATA
3879
+ regex: (?i)(request|upload|response|body|payload)
3880
+ - id: narvy.python.deserialization.marshal-loads-untrusted.marshal-loads-untrusted
3881
+ message: '`marshal.loads()` deserializes data that is not a fixed local trusted source. `marshal` is
3882
+ the format used for compiled Python bytecode - loading crafted marshal data from an untrusted source
3883
+ is a code-execution vector, and unlike `pickle`, its format is explicitly documented as unstable/unsafe
3884
+ by CPython itself (CWE-502).'
3885
+ severity: ERROR
3886
+ languages:
3887
+ - python
3888
+ metadata:
3889
+ cwe:
3890
+ - 'CWE-502: Deserialization of Untrusted Data'
3891
+ owasp:
3892
+ - A08:2021 - Software and Data Integrity Failures
3893
+ category: security
3894
+ subcategory:
3895
+ - vuln
3896
+ technology:
3897
+ - python
3898
+ likelihood: LOW
3899
+ impact: HIGH
3900
+ confidence: MEDIUM
3901
+ vulnerability_class:
3902
+ - Insecure Deserialization
3903
+ patterns:
3904
+ - pattern: marshal.loads($DATA)
3905
+ - metavariable-regex:
3906
+ metavariable: $DATA
3907
+ regex: (?i)(request|upload|response|body|payload|socket|message|msg)
3908
+ - id: narvy.python.deserialization.pickle-loads-untrusted.pickle-loads-untrusted
3909
+ message: '`pickle.load()`/`pickle.loads()` deserializes data that is not a fixed local trusted file
3910
+ - a request body, an uploaded file, a cache/queue payload, or a network socket read. Unpickling is
3911
+ equivalent to code execution: a crafted pickle stream can call arbitrary callables via `__reduce__`.
3912
+ This is remote code execution (CWE-502), not just a data-integrity issue. Use JSON (or another data-only
3913
+ format) for anything that crosses a trust boundary; if you must use pickle, sign the payload (HMAC)
3914
+ and verify before unpickling.'
3915
+ severity: ERROR
3916
+ languages:
3917
+ - python
3918
+ metadata:
3919
+ cwe:
3920
+ - 'CWE-502: Deserialization of Untrusted Data'
3921
+ owasp:
3922
+ - A08:2021 - Software and Data Integrity Failures
3923
+ category: security
3924
+ subcategory:
3925
+ - vuln
3926
+ technology:
3927
+ - python
3928
+ likelihood: HIGH
3929
+ impact: HIGH
3930
+ confidence: MEDIUM
3931
+ vulnerability_class:
3932
+ - Insecure Deserialization
3933
+ patterns:
3934
+ - pattern-either:
3935
+ - pattern: pickle.load($F)
3936
+ - pattern: pickle.loads($DATA)
3937
+ - pattern: cPickle.loads($DATA)
3938
+ - metavariable-regex:
3939
+ metavariable: $DATA
3940
+ regex: (?i)(request|upload|response|body|payload|socket|redis|celery|queue|message|msg)
3941
+ - id: narvy.python.deserialization.shelve-open-untrusted-path.shelve-open-untrusted-path
3942
+ message: '`shelve.open()` is backed by `pickle` under the hood - reading a shelve database an attacker
3943
+ can influence the path or contents of is the same RCE risk as calling `pickle.load()` directly (CWE-502).
3944
+ Only open shelve files from a fixed, trusted, non-user-writable path.'
3945
+ severity: WARNING
3946
+ languages:
3947
+ - python
3948
+ metadata:
3949
+ cwe:
3950
+ - 'CWE-502: Deserialization of Untrusted Data'
3951
+ owasp:
3952
+ - A08:2021 - Software and Data Integrity Failures
3953
+ category: security
3954
+ subcategory:
3955
+ - vuln
3956
+ technology:
3957
+ - python
3958
+ likelihood: LOW
3959
+ impact: HIGH
3960
+ confidence: LOW
3961
+ vulnerability_class:
3962
+ - Insecure Deserialization
3963
+ patterns:
3964
+ - pattern: shelve.open($PATH)
3965
+ - metavariable-regex:
3966
+ metavariable: $PATH
3967
+ regex: (?i)(request|upload|user|param)