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,3683 @@
1
+ # Narvy JavaScript / TypeScript security rules (.js/.jsx/.ts/.tsx).
2
+ # Injection rules run in taint mode and pin the vulnerable arg with focus-metavariable.
3
+
4
+ rules:
5
+
6
+ # CLEARTEXT HTTP
7
+
8
+ - id: narvy.javascript.transport.cleartext-http-literal.cleartext-http-literal
9
+ message: >-
10
+ Cleartext HTTP URL detected. Traffic to this endpoint is unencrypted
11
+ and vulnerable to MITM (eavesdropping, tampering, session hijacking).
12
+ Use HTTPS. CWE-319.
13
+ severity: WARNING
14
+ languages: [javascript, typescript]
15
+ metadata:
16
+ cwe:
17
+ - "CWE-319: Cleartext Transmission of Sensitive Information"
18
+ owasp:
19
+ - "A02:2021 - Cryptographic Failures"
20
+ - "A03:2017 - Sensitive Data Exposure"
21
+ category: security
22
+ subcategory: [vuln]
23
+ technology: [javascript, typescript]
24
+ likelihood: MEDIUM
25
+ impact: MEDIUM
26
+ confidence: HIGH
27
+ vulnerability_class:
28
+ - Insecure Communication
29
+ references:
30
+ - https://cwe.mitre.org/data/definitions/319.html
31
+ - https://owasp.org/www-community/vulnerabilities/Insecure_Transport
32
+ paths:
33
+ exclude:
34
+ - "**/fuzzer/payloads.js"
35
+ - "**/fuzzer/payloads.ts"
36
+ - "**/payloads/*.js"
37
+ - "**/payloads/*.ts"
38
+ - "**/test/**"
39
+ - "**/tests/**"
40
+ - "**/__tests__/**"
41
+ - "**/*.test.js"
42
+ - "**/*.test.ts"
43
+ - "**/*.spec.js"
44
+ - "**/*.spec.ts"
45
+ patterns:
46
+ - pattern-either:
47
+ - pattern: "\"$URL\""
48
+ - pattern: "'$URL'"
49
+ - pattern: "`$URL`"
50
+ - metavariable-regex:
51
+ metavariable: $URL
52
+ 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._~:/?#\[\]@!$&'()*+,;=%-]+$
53
+
54
+ # CORS ORIGIN REFLECTION
55
+
56
+ - id: narvy.javascript.cors.origin-callback-always-allows.origin-callback-always-allows
57
+ message: >-
58
+ The CORS origin callback calls back with `true` unconditionally,
59
+ without checking the incoming origin against an allow-list. This
60
+ accepts requests from every origin, same as `origin: "*"`, but
61
+ combined with `credentials: true` this is worse: unlike a literal
62
+ wildcard, per the CORS spec a dynamic origin callback CAN be
63
+ combined with credentialed requests, letting any site make
64
+ authenticated cross-origin requests on behalf of a logged-in user
65
+ (CWE-942). Check the `origin` argument against an explicit
66
+ allow-list before calling back with true.
67
+ severity: WARNING
68
+ languages: [javascript, typescript]
69
+ metadata:
70
+ cwe:
71
+ - "CWE-942: Permissive Cross-domain Policy with Untrusted Domains"
72
+ owasp:
73
+ - "A05:2021 - Security Misconfiguration"
74
+ category: security
75
+ subcategory: [vuln]
76
+ technology: [express, cors, nodejs, javascript, typescript]
77
+ likelihood: LOW
78
+ impact: HIGH
79
+ confidence: MEDIUM
80
+ vulnerability_class:
81
+ - Other
82
+ references:
83
+ - https://cwe.mitre.org/data/definitions/942.html
84
+ patterns:
85
+ - pattern-either:
86
+ - pattern: |
87
+ cors({..., origin: function ($ORIGIN, $CB) { $CB(null, true) }, ...})
88
+ - pattern: |
89
+ cors({..., origin: ($ORIGIN, $CB) => { $CB(null, true) }, ...})
90
+ - pattern: |
91
+ cors({..., origin: ($ORIGIN, $CB) => $CB(null, true), ...})
92
+
93
+ - id: narvy.javascript.cors.header-reflects-request-origin.header-reflects-request-origin
94
+ message: >-
95
+ Access-Control-Allow-Origin is set directly from the incoming
96
+ request's Origin header, with no allow-list check in between. This
97
+ lets any origin pass CORS, functionally identical to a wildcard,
98
+ and if paired with Access-Control-Allow-Credentials: true it
99
+ allows any site to make authenticated cross-origin requests
100
+ (CWE-942). Look the incoming origin up in an explicit allow-list
101
+ and only echo it back if it matches.
102
+ severity: WARNING
103
+ languages: [javascript, typescript]
104
+ metadata:
105
+ cwe:
106
+ - "CWE-942: Permissive Cross-domain Policy with Untrusted Domains"
107
+ owasp:
108
+ - "A05:2021 - Security Misconfiguration"
109
+ category: security
110
+ subcategory: [vuln]
111
+ technology: [express, nodejs, javascript, typescript]
112
+ likelihood: LOW
113
+ impact: HIGH
114
+ confidence: MEDIUM
115
+ vulnerability_class:
116
+ - Other
117
+ patterns:
118
+ - pattern-either:
119
+ - pattern: $RES.header("Access-Control-Allow-Origin", $REQ.headers.origin)
120
+ - pattern: $RES.header('Access-Control-Allow-Origin', $REQ.headers.origin)
121
+ - pattern: $RES.setHeader("Access-Control-Allow-Origin", $REQ.headers.origin)
122
+ - pattern: $RES.setHeader('Access-Control-Allow-Origin', $REQ.headers.origin)
123
+ - pattern: $RES.set("Access-Control-Allow-Origin", $REQ.headers.origin)
124
+
125
+ # EXPRESS SECURITY HARDENING
126
+
127
+ # 1. Helmet with Content-Security-Policy explicitly disabled.
128
+ - id: narvy.javascript.express.helmet-csp-disabled.helmet-csp-disabled
129
+ message: >-
130
+ Helmet is initialised with `contentSecurityPolicy: false`, which turns
131
+ OFF the Content-Security-Policy header. Without a CSP the app has no
132
+ defence-in-depth against XSS, clickjacking via script/style injection,
133
+ or malicious resource loads. Configure an explicit CSP directive set
134
+ instead of disabling it. CWE-1021 / CWE-693.
135
+ severity: WARNING
136
+ languages: [javascript, typescript]
137
+ metadata:
138
+ cwe:
139
+ - "CWE-1021: Improper Restriction of Rendered UI Layers or Frames"
140
+ - "CWE-693: Protection Mechanism Failure"
141
+ owasp:
142
+ - "A05:2021 - Security Misconfiguration"
143
+ category: security
144
+ subcategory: [vuln]
145
+ technology: [express, nodejs, helmet, javascript, typescript]
146
+ likelihood: MEDIUM
147
+ impact: MEDIUM
148
+ confidence: HIGH
149
+ vulnerability_class:
150
+ - Security Misconfiguration
151
+ references:
152
+ - https://helmetjs.github.io/
153
+ - https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
154
+ paths:
155
+ exclude:
156
+ - "**/test/**"
157
+ - "**/tests/**"
158
+ - "**/__tests__/**"
159
+ - "**/*.test.js"
160
+ - "**/*.test.ts"
161
+ - "**/*.spec.js"
162
+ - "**/*.spec.ts"
163
+ patterns:
164
+ - pattern-either:
165
+ - pattern: |
166
+ helmet({..., contentSecurityPolicy: false, ...})
167
+ - pattern: |
168
+ helmet.contentSecurityPolicy(false)
169
+
170
+ # 2. CORS configured to allow any origin.
171
+ - id: narvy.javascript.express.cors-wildcard-origin.cors-wildcard-origin
172
+ message: >-
173
+ CORS is configured to accept requests from ANY origin
174
+ (`origin: "*"` or `origin: true`). Combined with credentialed requests
175
+ this lets any website read authenticated responses on behalf of a
176
+ logged-in user. Restrict `origin` to an explicit allow-list. CWE-942.
177
+ severity: WARNING
178
+ languages: [javascript, typescript]
179
+ metadata:
180
+ cwe:
181
+ - "CWE-942: Permissive Cross-domain Policy with Untrusted Domains"
182
+ owasp:
183
+ - "A05:2021 - Security Misconfiguration"
184
+ category: security
185
+ subcategory: [vuln]
186
+ technology: [express, nodejs, cors, javascript, typescript]
187
+ likelihood: MEDIUM
188
+ impact: MEDIUM
189
+ confidence: MEDIUM
190
+ vulnerability_class:
191
+ - Security Misconfiguration
192
+ references:
193
+ - https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
194
+ paths:
195
+ exclude:
196
+ - "**/test/**"
197
+ - "**/tests/**"
198
+ - "**/__tests__/**"
199
+ - "**/*.test.js"
200
+ - "**/*.test.ts"
201
+ - "**/*.spec.js"
202
+ - "**/*.spec.ts"
203
+ patterns:
204
+ - pattern-either:
205
+ - pattern: |
206
+ cors({..., origin: "*", ...})
207
+ - pattern: |
208
+ cors({..., origin: true, ...})
209
+
210
+ # 3. MongoDB connection string with no credentials.
211
+ - id: narvy.javascript.database.mongodb-uri-without-auth.mongodb-uri-without-auth
212
+ message: >-
213
+ MongoDB connection string has no user:password credentials, so it
214
+ connects to an UNAUTHENTICATED database. If this URI is ever used in
215
+ production (e.g. an unset env var falling back to this literal), any
216
+ network-adjacent actor can read/write the entire database. Require
217
+ authentication and never fall back to a credential-less URI. CWE-306.
218
+ severity: INFO
219
+ languages: [javascript, typescript]
220
+ metadata:
221
+ cwe:
222
+ - "CWE-306: Missing Authentication for Critical Function"
223
+ owasp:
224
+ - "A05:2021 - Security Misconfiguration"
225
+ - "A07:2021 - Identification and Authentication Failures"
226
+ category: security
227
+ subcategory: [audit]
228
+ technology: [mongodb, nodejs, javascript, typescript]
229
+ likelihood: LOW
230
+ impact: HIGH
231
+ confidence: MEDIUM
232
+ vulnerability_class:
233
+ - Security Misconfiguration
234
+ references:
235
+ - https://www.mongodb.com/docs/manual/administration/security-checklist/
236
+ paths:
237
+ exclude:
238
+ - "**/test/**"
239
+ - "**/tests/**"
240
+ - "**/__tests__/**"
241
+ - "**/*.test.js"
242
+ - "**/*.test.ts"
243
+ - "**/*.spec.js"
244
+ - "**/*.spec.ts"
245
+ patterns:
246
+ - pattern-either:
247
+ - pattern: "\"$URI\""
248
+ - pattern: "'$URI'"
249
+ - metavariable-regex:
250
+ metavariable: $URI
251
+ # mongodb:// or mongodb+srv:// with a host but NO "@" (credentials).
252
+ regex: ^mongodb(\+srv)?://(?!.*@)[a-zA-Z0-9._-]+(:[0-9]+)?(/[a-zA-Z0-9._-]*)?$
253
+
254
+ # HARDCODED KDF FALLBACK
255
+
256
+ # Rule 1: env-fallback fed into `crypto.createHash(...).update(...)`
257
+ - id: narvy.javascript.crypto.hardcoded-env-fallback-to-hash.hardcoded-env-fallback-to-hash
258
+ message: >-
259
+ Hardcoded fallback secret '$LITERAL' is used when the environment
260
+ variable is unset, and is then fed into a cryptographic hash. If the
261
+ env var is missing in any deployment (e.g. local dev, CI, misconfigured
262
+ prod), the encryption key reduces to a public constant. Furthermore,
263
+ a single hash round is not a key-derivation function - use HKDF
264
+ (crypto.hkdfSync), PBKDF2 (crypto.pbkdf2Sync), scrypt, or Argon2 with
265
+ a per-tenant salt. Fix the env loading (fail fast if absent) AND
266
+ switch to a real KDF.
267
+ severity: ERROR
268
+ languages: [javascript, typescript]
269
+ metadata:
270
+ cwe:
271
+ - "CWE-321: Use of Hard-coded Cryptographic Key"
272
+ - "CWE-798: Use of Hard-coded Credentials"
273
+ - "CWE-916: Use of Password Hash With Insufficient Computational Effort"
274
+ owasp:
275
+ - "A02:2021 - Cryptographic Failures"
276
+ - "A07:2021 - Identification and Authentication Failures"
277
+ category: security
278
+ subcategory: [vuln]
279
+ technology: [node-crypto, javascript]
280
+ likelihood: HIGH
281
+ impact: HIGH
282
+ confidence: HIGH
283
+ vulnerability_class:
284
+ - Cryptographic Issues
285
+ - Hard-coded Credentials
286
+ references:
287
+ - https://cwe.mitre.org/data/definitions/321.html
288
+ - https://cwe.mitre.org/data/definitions/916.html
289
+ - https://datatracker.ietf.org/doc/html/rfc5869
290
+ patterns:
291
+ - pattern-either:
292
+ # Variant 1: `||` operator
293
+ - pattern: |
294
+ $CRYPTO.createHash($ALGO).update(process.env.$ENV || "$LITERAL")
295
+ - pattern: |
296
+ $CRYPTO.createHash($ALGO).update(process.env.$ENV || '$LITERAL')
297
+ - pattern: |
298
+ $CRYPTO.createHash($ALGO).update(process.env[$X] || "$LITERAL")
299
+ # Variant 2: `??` nullish coalescing
300
+ - pattern: |
301
+ $CRYPTO.createHash($ALGO).update(process.env.$ENV ?? "$LITERAL")
302
+ - pattern: |
303
+ $CRYPTO.createHash($ALGO).update(process.env.$ENV ?? '$LITERAL')
304
+ - pattern: |
305
+ $CRYPTO.createHash($ALGO).update(process.env[$X] ?? "$LITERAL")
306
+ # Variant 3: ternary
307
+ - pattern: |
308
+ $CRYPTO.createHash($ALGO).update(process.env.$ENV ? process.env.$ENV : "$LITERAL")
309
+ - pattern: |
310
+ $CRYPTO.createHash($ALGO).update(process.env.$ENV ? process.env.$ENV : '$LITERAL')
311
+ # Variant 4: two-statement (split hash chain)
312
+ - pattern: |
313
+ $HASH = $CRYPTO.createHash($ALGO)
314
+ ...
315
+ $HASH.update(process.env.$ENV || "$LITERAL")
316
+ - pattern: |
317
+ $HASH = $CRYPTO.createHash($ALGO)
318
+ ...
319
+ $HASH.update(process.env.$ENV ?? "$LITERAL")
320
+ # Variant 5: separate-assign (let K=env.X; if (!K) K="lit"; ...update(K))
321
+ - pattern: |
322
+ $K = process.env.$ENV
323
+ ...
324
+ if (!$K) { $K = "$LITERAL" }
325
+ ...
326
+ $CRYPTO.createHash($ALGO).update($K)
327
+ - pattern: |
328
+ let $K = process.env.$ENV
329
+ ...
330
+ if (!$K) { $K = "$LITERAL" }
331
+ ...
332
+ $CRYPTO.createHash($ALGO).update($K)
333
+ # Variant 6: chained-var (const K = env.X; const F = K || "lit"; update(F))
334
+ - pattern: |
335
+ $K = process.env.$ENV
336
+ ...
337
+ $F = $K || "$LITERAL"
338
+ ...
339
+ $CRYPTO.createHash($ALGO).update($F)
340
+ - pattern: |
341
+ $K = process.env.$ENV
342
+ ...
343
+ $F = $K ?? "$LITERAL"
344
+ ...
345
+ $CRYPTO.createHash($ALGO).update($F)
346
+ # Variant 7: ternary in assignment chained to hash
347
+ - pattern: |
348
+ $K = process.env.$ENV ? process.env.$ENV : "$LITERAL"
349
+ ...
350
+ $CRYPTO.createHash($ALGO).update($K)
351
+ - metavariable-regex:
352
+ metavariable: $LITERAL
353
+ regex: ^[A-Za-z0-9 _!@#$%^&*()+=,.:?<>\[\]{}|/\\-]{1,64}$
354
+
355
+ # Rule 2: env-fallback as symmetric-cipher key / HMAC key
356
+ - id: narvy.javascript.crypto.hardcoded-env-fallback-to-cipher-key.hardcoded-env-fallback-to-cipher-key
357
+ message: >-
358
+ Hardcoded fallback secret '$LITERAL' from `process.env.$ENV` is passed
359
+ directly to a symmetric cipher or HMAC. If the env var is unset the
360
+ key becomes a public constant and all ciphertexts can be decrypted by
361
+ anyone reading this source. Fail fast on missing env at startup, and
362
+ derive the cipher key via HKDF / PBKDF2 with a per-record salt.
363
+ severity: ERROR
364
+ languages: [javascript, typescript]
365
+ metadata:
366
+ cwe:
367
+ - "CWE-321: Use of Hard-coded Cryptographic Key"
368
+ - "CWE-798: Use of Hard-coded Credentials"
369
+ owasp:
370
+ - "A02:2021 - Cryptographic Failures"
371
+ category: security
372
+ subcategory: [vuln]
373
+ technology: [node-crypto, javascript]
374
+ likelihood: HIGH
375
+ impact: HIGH
376
+ confidence: HIGH
377
+ vulnerability_class:
378
+ - Cryptographic Issues
379
+ - Hard-coded Credentials
380
+ patterns:
381
+ - pattern-either:
382
+ # `||` operator
383
+ - pattern: |
384
+ $CRYPTO.createCipheriv($ALGO, process.env.$ENV || "$LITERAL", ...)
385
+ - pattern: |
386
+ $CRYPTO.createDecipheriv($ALGO, process.env.$ENV || "$LITERAL", ...)
387
+ - pattern: |
388
+ $CRYPTO.createHmac($ALGO, process.env.$ENV || "$LITERAL")
389
+ - pattern: |
390
+ $CRYPTO.createCipher($ALGO, process.env.$ENV || "$LITERAL")
391
+ # `??` nullish
392
+ - pattern: |
393
+ $CRYPTO.createCipheriv($ALGO, process.env.$ENV ?? "$LITERAL", ...)
394
+ - pattern: |
395
+ $CRYPTO.createDecipheriv($ALGO, process.env.$ENV ?? "$LITERAL", ...)
396
+ - pattern: |
397
+ $CRYPTO.createHmac($ALGO, process.env.$ENV ?? "$LITERAL")
398
+ # Ternary
399
+ - pattern: |
400
+ $CRYPTO.createHmac($ALGO, process.env.$ENV ? process.env.$ENV : "$LITERAL")
401
+ - pattern: |
402
+ $CRYPTO.createCipheriv($ALGO, process.env.$ENV ? process.env.$ENV : "$LITERAL", ...)
403
+ # Two-statement: separate-assign
404
+ - pattern: |
405
+ $K = process.env.$ENV
406
+ ...
407
+ if (!$K) { $K = "$LITERAL" }
408
+ ...
409
+ $CRYPTO.createHmac($ALGO, $K)
410
+ - pattern: |
411
+ let $K = process.env.$ENV
412
+ ...
413
+ if (!$K) { $K = "$LITERAL" }
414
+ ...
415
+ $CRYPTO.createHmac($ALGO, $K)
416
+ # Chained-var
417
+ - pattern: |
418
+ $K = process.env.$ENV
419
+ ...
420
+ $F = $K || "$LITERAL"
421
+ ...
422
+ $CRYPTO.createHmac($ALGO, $F)
423
+ - pattern: |
424
+ $K = process.env.$ENV
425
+ ...
426
+ $F = $K ?? "$LITERAL"
427
+ ...
428
+ $CRYPTO.createHmac($ALGO, $F)
429
+ - pattern: |
430
+ $K = process.env.$ENV
431
+ ...
432
+ $F = $K || "$LITERAL"
433
+ ...
434
+ $CRYPTO.createCipheriv($ALGO, $F, ...)
435
+ - metavariable-regex:
436
+ metavariable: $LITERAL
437
+ regex: ^[A-Za-z0-9 _!@#$%^&*()+=,.:?<>\[\]{}|/\\-]{1,64}$
438
+
439
+ # Rule 3: env-fallback fed into KDF (HKDF / PBKDF2 / scrypt)
440
+ - id: narvy.javascript.crypto.hardcoded-env-fallback-to-kdf.hardcoded-env-fallback-to-kdf
441
+ message: >-
442
+ Hardcoded fallback secret '$LITERAL' is used as input keying material
443
+ (IKM) to a KDF. When the env var is unset the IKM reduces to a public
444
+ constant - defeats the entire KDF. Fail fast on missing env at app
445
+ startup (`assert process.env.X, 'X must be set'`).
446
+ severity: ERROR
447
+ languages: [javascript, typescript]
448
+ metadata:
449
+ cwe:
450
+ - "CWE-321: Use of Hard-coded Cryptographic Key"
451
+ - "CWE-798: Use of Hard-coded Credentials"
452
+ owasp:
453
+ - "A02:2021 - Cryptographic Failures"
454
+ category: security
455
+ subcategory: [vuln]
456
+ technology: [node-crypto, javascript]
457
+ likelihood: HIGH
458
+ impact: HIGH
459
+ confidence: HIGH
460
+ vulnerability_class:
461
+ - Cryptographic Issues
462
+ - Hard-coded Credentials
463
+ patterns:
464
+ - pattern-either:
465
+ # Direct call with `||`
466
+ - pattern: |
467
+ $CRYPTO.hkdfSync($ALGO, Buffer.from(process.env.$ENV || "$LITERAL", ...), ...)
468
+ - pattern: |
469
+ $CRYPTO.hkdfSync($ALGO, Buffer.from(process.env.$ENV || '$LITERAL', ...), ...)
470
+ - pattern: |
471
+ $CRYPTO.hkdfSync($ALGO, process.env.$ENV || "$LITERAL", ...)
472
+ - pattern: |
473
+ $CRYPTO.pbkdf2Sync(process.env.$ENV || "$LITERAL", ...)
474
+ - pattern: |
475
+ $CRYPTO.scryptSync(process.env.$ENV || "$LITERAL", ...)
476
+ # Direct call with `??`
477
+ - pattern: |
478
+ $CRYPTO.hkdfSync($ALGO, Buffer.from(process.env.$ENV ?? "$LITERAL", ...), ...)
479
+ - pattern: |
480
+ $CRYPTO.hkdfSync($ALGO, process.env.$ENV ?? "$LITERAL", ...)
481
+ - pattern: |
482
+ $CRYPTO.pbkdf2Sync(process.env.$ENV ?? "$LITERAL", ...)
483
+ - pattern: |
484
+ $CRYPTO.scryptSync(process.env.$ENV ?? "$LITERAL", ...)
485
+ # Ternary
486
+ - pattern: |
487
+ $CRYPTO.pbkdf2Sync(process.env.$ENV ? process.env.$ENV : "$LITERAL", ...)
488
+ - pattern: |
489
+ $CRYPTO.scryptSync(process.env.$ENV ? process.env.$ENV : "$LITERAL", ...)
490
+ # Two-step pattern: assign IKM from env-fallback, then use in KDF
491
+ - pattern: |
492
+ $IKM = Buffer.from(process.env.$ENV || "$LITERAL", ...)
493
+ ...
494
+ $CRYPTO.hkdfSync($ALGO, $IKM, ...)
495
+ - pattern: |
496
+ $IKM = Buffer.from(process.env.$ENV ?? "$LITERAL", ...)
497
+ ...
498
+ $CRYPTO.hkdfSync($ALGO, $IKM, ...)
499
+ # Separate-assign feeding KDF
500
+ - pattern: |
501
+ $K = process.env.$ENV
502
+ ...
503
+ if (!$K) { $K = "$LITERAL" }
504
+ ...
505
+ $CRYPTO.pbkdf2Sync($K, ...)
506
+ - pattern: |
507
+ $K = process.env.$ENV
508
+ ...
509
+ if (!$K) { $K = "$LITERAL" }
510
+ ...
511
+ $CRYPTO.hkdfSync($ALGO, $K, ...)
512
+ # Chained-var feeding KDF
513
+ - pattern: |
514
+ $K = process.env.$ENV
515
+ ...
516
+ $F = $K || "$LITERAL"
517
+ ...
518
+ $CRYPTO.pbkdf2Sync($F, ...)
519
+ - pattern: |
520
+ $K = process.env.$ENV
521
+ ...
522
+ $F = $K || "$LITERAL"
523
+ ...
524
+ $CRYPTO.hkdfSync($ALGO, $F, ...)
525
+ - metavariable-regex:
526
+ metavariable: $LITERAL
527
+ regex: ^[A-Za-z0-9 _!@#$%^&*()+=,.:?<>\[\]{}|/\\-]{1,64}$
528
+
529
+ # Rule 4: single-hash-as-KDF (weak KDF - CWE-916)
530
+ - id: narvy.javascript.crypto.single-hash-as-kdf.single-hash-as-kdf
531
+ message: >-
532
+ A single SHA-1 / SHA-256 / MD5 round is used to derive an encryption
533
+ key from `process.env.$ENV`. This is NOT a key-derivation function:
534
+ brute-force attacks can recompute billions of candidates per second on
535
+ modern GPUs. Use HKDF (crypto.hkdfSync, RFC 5869) for high-entropy
536
+ inputs, PBKDF2 / scrypt / Argon2 for low-entropy passwords.
537
+ Per-tenant salt is mandatory.
538
+ severity: WARNING
539
+ languages: [javascript, typescript]
540
+ metadata:
541
+ cwe:
542
+ - "CWE-916: Use of Password Hash With Insufficient Computational Effort"
543
+ - "CWE-326: Inadequate Encryption Strength"
544
+ owasp:
545
+ - "A02:2021 - Cryptographic Failures"
546
+ category: security
547
+ subcategory: [vuln]
548
+ technology: [node-crypto, javascript]
549
+ likelihood: MEDIUM
550
+ impact: HIGH
551
+ confidence: MEDIUM
552
+ vulnerability_class:
553
+ - Cryptographic Issues
554
+ patterns:
555
+ - pattern-either:
556
+ - pattern: |
557
+ $CRYPTO.createCipheriv($ALGO, $CRYPTO.createHash($H).update(process.env.$ENV).digest(), ...)
558
+ - pattern: |
559
+ $CRYPTO.createCipheriv($ALGO, $CRYPTO.createHash($H).update(process.env.$ENV || $X).digest(), ...)
560
+ - pattern: |
561
+ $CRYPTO.createCipheriv($ALGO, $CRYPTO.createHash($H).update(process.env.$ENV ?? $X).digest(), ...)
562
+ - pattern: |
563
+ $CRYPTO.createDecipheriv($ALGO, $CRYPTO.createHash($H).update(process.env.$ENV).digest(), ...)
564
+ - pattern: |
565
+ $CRYPTO.createDecipheriv($ALGO, $CRYPTO.createHash($H).update(process.env.$ENV || $X).digest(), ...)
566
+ - pattern: |
567
+ $CRYPTO.createDecipheriv($ALGO, $CRYPTO.createHash($H).update(process.env.$ENV ?? $X).digest(), ...)
568
+ - pattern: |
569
+ function $F() {
570
+ return $CRYPTO.createHash($H)
571
+ .update(process.env.$ENV || $X)
572
+ .digest()
573
+ }
574
+ - pattern: |
575
+ function $F() {
576
+ return $CRYPTO.createHash($H).update(process.env.$ENV || $X).digest()
577
+ }
578
+ - pattern: |
579
+ function $F() {
580
+ return $CRYPTO.createHash($H)
581
+ .update(process.env.$ENV ?? $X)
582
+ .digest()
583
+ }
584
+ - metavariable-regex:
585
+ metavariable: $H
586
+ regex: ^['"](md5|sha1|sha-?1|sha256|sha-?256)['"]$
587
+
588
+ # Rule 5 (NEW): env-fallback fed into jwt.sign / jwt.verify
589
+ - id: narvy.javascript.crypto.hardcoded-env-fallback-to-jwt-sign.hardcoded-env-fallback-to-jwt-sign
590
+ message: >-
591
+ Hardcoded fallback secret '$LITERAL' is used as the JWT signing key
592
+ when `process.env.$ENV` is unset. If the env var is missing in any
593
+ deployment (local dev, CI, misconfigured prod), tokens are signed
594
+ with a public constant - anyone reading this source can mint valid
595
+ tokens. THE canonical real-world `jwt.sign(...)` bug. Fix: fail fast
596
+ at app startup if the secret env var is absent
597
+ (`assert process.env.$ENV, 'JWT_SECRET must be set'`).
598
+ severity: ERROR
599
+ languages: [javascript, typescript]
600
+ metadata:
601
+ cwe:
602
+ - "CWE-321: Use of Hard-coded Cryptographic Key"
603
+ - "CWE-798: Use of Hard-coded Credentials"
604
+ - "CWE-547: Use of Hard-coded, Security-relevant Constants"
605
+ owasp:
606
+ - "A02:2021 - Cryptographic Failures"
607
+ - "A07:2021 - Identification and Authentication Failures"
608
+ category: security
609
+ subcategory: [vuln]
610
+ technology: [jsonwebtoken, jwt, javascript]
611
+ likelihood: HIGH
612
+ impact: HIGH
613
+ confidence: HIGH
614
+ vulnerability_class:
615
+ - Cryptographic Issues
616
+ - Hard-coded Credentials
617
+ patterns:
618
+ - pattern-either:
619
+ # jwt.sign / jsonwebtoken.sign - `||` operator
620
+ - pattern: |
621
+ $JWT.sign($PAYLOAD, process.env.$ENV || "$LITERAL", ...)
622
+ - pattern: |
623
+ $JWT.sign($PAYLOAD, process.env.$ENV || "$LITERAL")
624
+ - pattern: |
625
+ $JWT.sign($PAYLOAD, process.env.$ENV || '$LITERAL', ...)
626
+ - pattern: |
627
+ $JWT.sign($PAYLOAD, process.env.$ENV || '$LITERAL')
628
+ # jwt.sign - `??` nullish
629
+ - pattern: |
630
+ $JWT.sign($PAYLOAD, process.env.$ENV ?? "$LITERAL", ...)
631
+ - pattern: |
632
+ $JWT.sign($PAYLOAD, process.env.$ENV ?? "$LITERAL")
633
+ # jwt.verify - `||`
634
+ - pattern: |
635
+ $JWT.verify($TOKEN, process.env.$ENV || "$LITERAL", ...)
636
+ - pattern: |
637
+ $JWT.verify($TOKEN, process.env.$ENV || "$LITERAL")
638
+ - pattern: |
639
+ $JWT.verify($TOKEN, process.env.$ENV || '$LITERAL')
640
+ # jwt.verify - `??`
641
+ - pattern: |
642
+ $JWT.verify($TOKEN, process.env.$ENV ?? "$LITERAL", ...)
643
+ - pattern: |
644
+ $JWT.verify($TOKEN, process.env.$ENV ?? "$LITERAL")
645
+ # Ternary
646
+ - pattern: |
647
+ $JWT.sign($PAYLOAD, process.env.$ENV ? process.env.$ENV : "$LITERAL", ...)
648
+ - pattern: |
649
+ $JWT.verify($TOKEN, process.env.$ENV ? process.env.$ENV : "$LITERAL", ...)
650
+ # Separate-assign feeding jwt.sign
651
+ - pattern: |
652
+ $K = process.env.$ENV
653
+ ...
654
+ if (!$K) { $K = "$LITERAL" }
655
+ ...
656
+ $JWT.sign($PAYLOAD, $K, ...)
657
+ # Chained-var feeding jwt.sign
658
+ - pattern: |
659
+ $K = process.env.$ENV
660
+ ...
661
+ $F = $K || "$LITERAL"
662
+ ...
663
+ $JWT.sign($PAYLOAD, $F, ...)
664
+ - pattern: |
665
+ $K = process.env.$ENV
666
+ ...
667
+ $F = $K ?? "$LITERAL"
668
+ ...
669
+ $JWT.sign($PAYLOAD, $F, ...)
670
+ - metavariable-regex:
671
+ metavariable: $LITERAL
672
+ regex: ^[A-Za-z0-9 _!@#$%^&*()+=,.:?<>\[\]{}|/\\-]{1,64}$
673
+
674
+ # Rule 6 (NEW): env-fallback fed into Web Crypto API (crypto.subtle)
675
+ - id: narvy.javascript.crypto.hardcoded-env-fallback-to-web-crypto.hardcoded-env-fallback-to-web-crypto
676
+ message: >-
677
+ Hardcoded fallback secret '$LITERAL' is fed into the Web Crypto API
678
+ (crypto.subtle.*). Common in Cloudflare Workers, Deno, and modern
679
+ Node 18+ runtimes. If the env var is unset the key material reduces
680
+ to a public constant - defeats the entire crypto operation. Fail
681
+ fast on missing env at startup.
682
+ severity: ERROR
683
+ languages: [javascript, typescript]
684
+ metadata:
685
+ cwe:
686
+ - "CWE-321: Use of Hard-coded Cryptographic Key"
687
+ - "CWE-798: Use of Hard-coded Credentials"
688
+ owasp:
689
+ - "A02:2021 - Cryptographic Failures"
690
+ category: security
691
+ subcategory: [vuln]
692
+ technology: [web-crypto, cloudflare-workers, deno, javascript]
693
+ likelihood: HIGH
694
+ impact: HIGH
695
+ confidence: HIGH
696
+ vulnerability_class:
697
+ - Cryptographic Issues
698
+ - Hard-coded Credentials
699
+ patterns:
700
+ - pattern-either:
701
+ # crypto.subtle.importKey called directly with env-fallback string
702
+ - pattern: |
703
+ $SUBTLE.importKey($FORMAT, process.env.$ENV || "$LITERAL", ...)
704
+ - pattern: |
705
+ $SUBTLE.importKey($FORMAT, process.env.$ENV ?? "$LITERAL", ...)
706
+ # Wrapped in TextEncoder().encode(...) (the canonical browser shape)
707
+ - pattern: |
708
+ $SUBTLE.importKey($FORMAT, new TextEncoder().encode(process.env.$ENV || "$LITERAL"), ...)
709
+ - pattern: |
710
+ $SUBTLE.importKey($FORMAT, new TextEncoder().encode(process.env.$ENV ?? "$LITERAL"), ...)
711
+ - pattern: |
712
+ $SUBTLE.importKey($FORMAT, new TextEncoder().encode(process.env.$ENV || '$LITERAL'), ...)
713
+ - pattern: |
714
+ $SUBTLE.importKey($FORMAT, new TextEncoder().encode(process.env.$ENV ?? '$LITERAL'), ...)
715
+ # Two-statement: encode into local, then importKey
716
+ - pattern: |
717
+ $RAW = new TextEncoder().encode(process.env.$ENV || "$LITERAL")
718
+ ...
719
+ $SUBTLE.importKey($FORMAT, $RAW, ...)
720
+ - pattern: |
721
+ $RAW = new TextEncoder().encode(process.env.$ENV ?? "$LITERAL")
722
+ ...
723
+ $SUBTLE.importKey($FORMAT, $RAW, ...)
724
+ # Separate-assign feeding Web Crypto
725
+ - pattern: |
726
+ $K = process.env.$ENV
727
+ ...
728
+ if (!$K) { $K = "$LITERAL" }
729
+ ...
730
+ $SUBTLE.importKey($FORMAT, new TextEncoder().encode($K), ...)
731
+ - metavariable-regex:
732
+ metavariable: $LITERAL
733
+ regex: ^[A-Za-z0-9 _!@#$%^&*()+=,.:?<>\[\]{}|/\\-]{1,64}$
734
+
735
+ # Rule 7: bare-literal secret reaching a JWT sign/verify call (no env var
736
+ # at all). Covers jsonwebtoken, jose and passport-jwt in one taint rule.
737
+ - id: narvy.javascript.crypto.hardcoded-jwt-sign-secret.hardcoded-jwt-sign-secret
738
+ message: >-
739
+ A hardcoded string literal secret reaches a JWT signing or
740
+ verification call (jsonwebtoken, jose, or passport-jwt). The literal
741
+ is a public constant baked into the source - anyone who reads it can
742
+ forge valid tokens (CWE-321 / CWE-798). Load the secret from an
743
+ environment variable or a secrets manager, and fail startup if it is
744
+ missing.
745
+ severity: ERROR
746
+ languages: [javascript, typescript]
747
+ mode: taint
748
+ metadata:
749
+ cwe:
750
+ - "CWE-321: Use of Hard-coded Cryptographic Key"
751
+ - "CWE-798: Use of Hard-coded Credentials"
752
+ owasp:
753
+ - "A02:2021 - Cryptographic Failures"
754
+ - "A07:2021 - Identification and Authentication Failures"
755
+ category: security
756
+ subcategory: [vuln]
757
+ technology: [jsonwebtoken, jose, passport-jwt, jwt, javascript, typescript]
758
+ likelihood: HIGH
759
+ impact: HIGH
760
+ confidence: HIGH
761
+ vulnerability_class:
762
+ - Cryptographic Issues
763
+ - Hard-coded Credentials
764
+ pattern-sources:
765
+ # Bare literal reaching a JWT secret argument; narrow sinks keep it off other strings.
766
+ - pattern: '"..."'
767
+ pattern-sinks:
768
+ # jsonwebtoken sign/verify; $JWT name-constrained to jwt/jsonwebtoken.
769
+ - patterns:
770
+ - pattern-either:
771
+ - pattern: $JWT.sign($PAYLOAD, $SECRET, ...)
772
+ - pattern: $JWT.sign($PAYLOAD, $SECRET)
773
+ - pattern: $JWT.verify($TOKEN, $SECRET, ...)
774
+ - pattern: $JWT.verify($TOKEN, $SECRET)
775
+ - focus-metavariable: $SECRET
776
+ - metavariable-regex:
777
+ metavariable: $JWT
778
+ regex: (?i)^(jwt|jsonwebtoken|jsonWebToken)$
779
+ # jose: new SignJWT(payload)....sign(secretBytes), secret wrapped
780
+ # as a Uint8Array via TextEncoder/Buffer (jose's documented shape).
781
+ - patterns:
782
+ - pattern-either:
783
+ - pattern: $CHAIN.sign(new TextEncoder().encode($SECRET))
784
+ - pattern: $CHAIN.sign(Buffer.from($SECRET))
785
+ - focus-metavariable: $SECRET
786
+ # passport-jwt: new JwtStrategy({ secretOrKey: secret, ... }, cb).
787
+ - patterns:
788
+ - pattern: |
789
+ new JwtStrategy({..., secretOrKey: $SECRET, ...}, ...)
790
+ - focus-metavariable: $SECRET
791
+
792
+ # JWT SESSION
793
+
794
+ - id: narvy.javascript.jwt.sign-alg-none.sign-alg-none
795
+ message: >-
796
+ A JWT is signed with algorithm 'none'. A 'none'-algorithm token has
797
+ no signature at all - anyone can craft a token with arbitrary
798
+ claims and it will pass verification on a server that also
799
+ accepts 'none' (CWE-347). Never use algorithm 'none'; sign with
800
+ HS256/RS256 and a properly managed secret or key pair.
801
+ severity: ERROR
802
+ languages: [javascript, typescript]
803
+ metadata:
804
+ cwe:
805
+ - "CWE-347: Improper Verification of Cryptographic Signature"
806
+ owasp:
807
+ - "A02:2021 - Cryptographic Failures"
808
+ category: security
809
+ subcategory: [vuln]
810
+ technology: [jsonwebtoken, javascript, typescript]
811
+ likelihood: LOW
812
+ impact: HIGH
813
+ confidence: HIGH
814
+ vulnerability_class:
815
+ - Improper Authentication
816
+ references:
817
+ - https://cwe.mitre.org/data/definitions/347.html
818
+ patterns:
819
+ - pattern-either:
820
+ - pattern: 'jwt.sign($PAYLOAD, ..., {..., algorithm: "none", ...})'
821
+ - pattern: jwt.sign($PAYLOAD, null, ...)
822
+ - pattern: jwt.sign($PAYLOAD, "", ...)
823
+
824
+ - id: narvy.javascript.jwt.verify-missing-algorithms-allowlist.verify-missing-algorithms-allowlist
825
+ message: >-
826
+ jwt.verify is called without an `algorithms` allow-list in its
827
+ options. Without pinning the accepted algorithm(s), a library that
828
+ supports both symmetric (HS256) and asymmetric (RS256) signing can
829
+ be tricked into verifying an HS256 token that was HMAC-signed
830
+ using the server's own RSA public key as the secret - a well-known
831
+ JWT algorithm-confusion forgery (CWE-347). Always pass an explicit
832
+ `algorithms: ['HS256']` (or whichever single algorithm is
833
+ expected) to jwt.verify.
834
+ severity: ERROR
835
+ languages: [javascript, typescript]
836
+ metadata:
837
+ cwe:
838
+ - "CWE-347: Improper Verification of Cryptographic Signature"
839
+ owasp:
840
+ - "A02:2021 - Cryptographic Failures"
841
+ category: security
842
+ subcategory: [vuln]
843
+ technology: [jsonwebtoken, javascript, typescript]
844
+ likelihood: LOW
845
+ impact: HIGH
846
+ confidence: MEDIUM
847
+ vulnerability_class:
848
+ - Improper Authentication
849
+ patterns:
850
+ - pattern-either:
851
+ - pattern: jwt.verify($TOKEN, $SECRET)
852
+ - pattern: jwt.verify($TOKEN, $SECRET, {...})
853
+ - pattern-not: 'jwt.verify($TOKEN, $SECRET, {..., algorithms: [...], ...})'
854
+
855
+ - id: narvy.javascript.session.cookie-secure-explicitly-disabled.cookie-secure-explicitly-disabled
856
+ message: >-
857
+ A session cookie is configured with `secure: false`. This allows
858
+ the session cookie to be sent over plain HTTP, exposing it to
859
+ network eavesdroppers who can then hijack the session (CWE-614).
860
+ Set `secure: true` (typically gated on NODE_ENV === 'production'
861
+ for local HTTP development) so the cookie is only ever sent over
862
+ HTTPS.
863
+ severity: WARNING
864
+ languages: [javascript, typescript]
865
+ metadata:
866
+ cwe:
867
+ - "CWE-614: Sensitive Cookie Without 'Secure' Attribute"
868
+ owasp:
869
+ - "A02:2021 - Cryptographic Failures"
870
+ category: security
871
+ subcategory: [vuln]
872
+ technology: [express, nodejs, javascript, typescript]
873
+ likelihood: MEDIUM
874
+ impact: MEDIUM
875
+ confidence: HIGH
876
+ vulnerability_class:
877
+ - Improper Authentication
878
+ patterns:
879
+ - pattern-either:
880
+ - pattern: |
881
+ session({..., cookie: {..., secure: false, ...}, ...})
882
+ - pattern: |
883
+ {..., cookie: {..., secure: false, ...}, ...}
884
+
885
+ # Rule 1b: cookie security options omitted, not just false; scoped to session() calls.
886
+ - id: narvy.javascript.session.cookie-security-options-omitted.cookie-security-options-omitted
887
+ message: >-
888
+ express-session is configured with the cookie security options
889
+ (secure / httpOnly / sameSite) either entirely absent from the
890
+ `cookie` block or the `cookie` block itself absent from the
891
+ session() config. Express defaults these to insecure values
892
+ (secure defaults off, sameSite defaults off), so silence here is
893
+ silently insecure-by-default, not a neutral choice (CWE-614 /
894
+ CWE-1004). Set an explicit `cookie: { secure: true, httpOnly:
895
+ true, sameSite: 'strict' }` block (gate `secure` on
896
+ NODE_ENV === 'production' for local HTTP dev).
897
+ severity: WARNING
898
+ languages: [javascript, typescript]
899
+ metadata:
900
+ cwe:
901
+ - "CWE-614: Sensitive Cookie Without 'Secure' Attribute"
902
+ - "CWE-1004: Sensitive Cookie Without 'HttpOnly' Flag"
903
+ owasp:
904
+ - "A02:2021 - Cryptographic Failures"
905
+ category: security
906
+ subcategory: [vuln]
907
+ technology: [express, nodejs, javascript, typescript]
908
+ likelihood: MEDIUM
909
+ impact: MEDIUM
910
+ confidence: MEDIUM
911
+ vulnerability_class:
912
+ - Improper Authentication
913
+ patterns:
914
+ - pattern: session($OPTS)
915
+ - pattern-either:
916
+ # secure never mentioned in the cookie block (block itself may
917
+ # be missing entirely - that also satisfies "not mentioned").
918
+ - patterns:
919
+ - pattern: session($OPTS)
920
+ - pattern-not: |
921
+ session({..., cookie: {..., secure: $S, ...}, ...})
922
+ # httpOnly never mentioned.
923
+ - patterns:
924
+ - pattern: session($OPTS)
925
+ - pattern-not: |
926
+ session({..., cookie: {..., httpOnly: $H, ...}, ...})
927
+ # sameSite never mentioned.
928
+ - patterns:
929
+ - pattern: session($OPTS)
930
+ - pattern-not: |
931
+ session({..., cookie: {..., sameSite: $SS, ...}, ...})
932
+
933
+ # MISC SECURITY
934
+
935
+ # Rule 1: prototype pollution via merge of request body
936
+ - id: narvy.javascript.misc.prototype-pollution-merge-reqbody.prototype-pollution-merge-reqbody
937
+ message: >-
938
+ Request-derived data (`req.body` / `req.query` / `req.params`) is
939
+ merged into an object via a deep-merge/extend helper or
940
+ `Object.assign`. If the merge implementation does not block
941
+ `__proto__` / `constructor.prototype` keys, an attacker can pollute
942
+ `Object.prototype` and change behavior application-wide (CWE-1321).
943
+ Use a merge library with prototype-pollution protection (lodash
944
+ >=4.17.21 with a key filter, or a schema-validated merge) and never
945
+ merge raw request bodies directly.
946
+ severity: WARNING
947
+ languages: [javascript, typescript]
948
+ metadata:
949
+ cwe:
950
+ - "CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')"
951
+ owasp:
952
+ - "A08:2021 - Software and Data Integrity Failures"
953
+ category: security
954
+ subcategory: [audit]
955
+ technology: [javascript, typescript, express, node]
956
+ likelihood: LOW
957
+ impact: HIGH
958
+ confidence: LOW
959
+ vulnerability_class:
960
+ - Improper Input Validation
961
+ patterns:
962
+ - pattern-either:
963
+ - pattern: $_.merge($TARGET, $REQ.body)
964
+ - pattern: $_.merge($TARGET, $REQ.query)
965
+ - pattern: $_.extend($TARGET, $REQ.body)
966
+ - pattern: $_.extend(true, $TARGET, $REQ.body)
967
+ - pattern: Object.assign($TARGET, $REQ.body)
968
+ - pattern: merge($TARGET, $REQ.body)
969
+ - pattern: deepmerge($TARGET, $REQ.body)
970
+
971
+ # Rule 2: ReDoS - RegExp from unsanitized input; focus pins the PATTERN arg.
972
+ - id: narvy.javascript.misc.redos-dynamic-regexp.redos-dynamic-regexp
973
+ message: >-
974
+ A request-derived value (`req.body` / `req.query` / `req.params`)
975
+ flows into the pattern argument of `new RegExp()`. An attacker who
976
+ controls the pattern can supply a catastrophically-backtracking regex,
977
+ causing a denial of service when it is later evaluated (ReDoS,
978
+ CWE-1333). Validate/allow-list the pattern, cap its length, escape it
979
+ with a regex-escape helper, or run matching with a timeout / a
980
+ linear-time engine (e.g. re2).
981
+ severity: WARNING
982
+ languages: [javascript, typescript]
983
+ mode: taint
984
+ metadata:
985
+ cwe:
986
+ - "CWE-1333: Inefficient Regular Expression Complexity"
987
+ - "CWE-400: Uncontrolled Resource Consumption"
988
+ owasp:
989
+ - "A05:2021 - Security Misconfiguration"
990
+ category: security
991
+ subcategory: [audit]
992
+ technology: [javascript, typescript]
993
+ likelihood: LOW
994
+ impact: MEDIUM
995
+ confidence: MEDIUM
996
+ vulnerability_class:
997
+ - Denial-of-Service (DoS)
998
+ pattern-sources:
999
+ - patterns:
1000
+ - pattern-either:
1001
+ - pattern: $REQ.body
1002
+ - pattern: $REQ.query
1003
+ - pattern: $REQ.params
1004
+ - metavariable-regex:
1005
+ metavariable: $REQ
1006
+ regex: ^(req|request|_req|ctx|httpRequest|httpReq)$
1007
+ - patterns:
1008
+ - pattern: $REQ.$KIND.$X
1009
+ - metavariable-regex:
1010
+ metavariable: $KIND
1011
+ regex: ^(body|params|query)$
1012
+ pattern-sanitizers:
1013
+ # Regex-escaping the value makes every metacharacter literal, which
1014
+ # removes the attacker's ability to introduce backtracking.
1015
+ - pattern: escapeRegExp(...)
1016
+ - pattern: escapeRegex(...)
1017
+ - pattern: escapeStringRegexp(...)
1018
+ - pattern: $M.escapeRegExp(...)
1019
+ - pattern: $M.escapeRegex(...)
1020
+ - pattern: RegExp.escape(...)
1021
+ - pattern: parseInt(...)
1022
+ - pattern: Number(...)
1023
+ - patterns:
1024
+ - pattern: $V
1025
+ - pattern-inside: |
1026
+ if (<... $ALLOW.includes(...) ...>) { ... }
1027
+ - patterns:
1028
+ - pattern: $V
1029
+ - pattern-inside: |
1030
+ if (!$ALLOW.includes($V)) return ...;
1031
+ ...
1032
+ pattern-sinks:
1033
+ # First argument only - see the conversion note above.
1034
+ - patterns:
1035
+ - pattern: new RegExp($SINK, ...)
1036
+ - focus-metavariable: $SINK
1037
+
1038
+ # Rule 3: mass assignment via ORM create/update of raw request body
1039
+ - id: narvy.javascript.misc.mass-assignment-orm-reqbody.mass-assignment-orm-reqbody
1040
+ message: >-
1041
+ An ORM `.create()` / `.update()` call is passed the raw request body
1042
+ directly, with no field allow-list. If the model has
1043
+ security-sensitive fields (e.g. `role`, `isAdmin`, `verified`,
1044
+ `balance`), an attacker can set them by adding extra JSON keys to
1045
+ the request (mass assignment, CWE-915). Explicitly whitelist
1046
+ permitted fields before passing them to the ORM.
1047
+ severity: WARNING
1048
+ languages: [javascript, typescript]
1049
+ metadata:
1050
+ cwe:
1051
+ - "CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes"
1052
+ owasp:
1053
+ - "A08:2021 - Software and Data Integrity Failures"
1054
+ category: security
1055
+ subcategory: [audit]
1056
+ technology: [javascript, typescript, mongoose, sequelize, express]
1057
+ likelihood: MEDIUM
1058
+ impact: MEDIUM
1059
+ confidence: LOW
1060
+ vulnerability_class:
1061
+ - Improper Access Control
1062
+ patterns:
1063
+ - pattern-either:
1064
+ - pattern: $MODEL.create($REQ.body)
1065
+ - pattern: $MODEL.update($REQ.body, ...)
1066
+ - pattern: $MODEL.findByIdAndUpdate($ID, $REQ.body, ...)
1067
+ - pattern: $MODEL.findOneAndUpdate($COND, $REQ.body, ...)
1068
+
1069
+ # Rule 4: world-writable file permissions
1070
+ - id: narvy.javascript.misc.insecure-file-permission.insecure-file-permission
1071
+ message: >-
1072
+ `fs.chmod` / `fs.chmodSync` sets world-writable (and often
1073
+ world-readable) permissions (0o777 / 0o666 / 0777 / 0666). Any local
1074
+ user or process can then modify or read this file, which is
1075
+ dangerous for config files, key material, or scripts that get
1076
+ executed later (CWE-732). Use the narrowest mode that works, e.g.
1077
+ 0o600 for secrets or 0o644 for regular files.
1078
+ severity: WARNING
1079
+ languages: [javascript, typescript]
1080
+ metadata:
1081
+ cwe:
1082
+ - "CWE-732: Incorrect Permission Assignment for Critical Resource"
1083
+ owasp:
1084
+ - "A01:2021 - Broken Access Control"
1085
+ category: security
1086
+ subcategory: [audit]
1087
+ technology: [javascript, typescript, node]
1088
+ likelihood: LOW
1089
+ impact: MEDIUM
1090
+ confidence: HIGH
1091
+ vulnerability_class:
1092
+ - Improper Access Control
1093
+ patterns:
1094
+ - pattern-either:
1095
+ - pattern: fs.chmod($PATH, 0o777, ...)
1096
+ - pattern: fs.chmod($PATH, 0777, ...)
1097
+ - pattern: fs.chmod($PATH, 0o666, ...)
1098
+ - pattern: fs.chmod($PATH, 0666, ...)
1099
+ - pattern: fs.chmodSync($PATH, 0o777)
1100
+ - pattern: fs.chmodSync($PATH, 0777)
1101
+ - pattern: fs.chmodSync($PATH, 0o666)
1102
+ - pattern: fs.chmodSync($PATH, 0666)
1103
+ - pattern: fsPromises.chmod($PATH, 0o777)
1104
+ - pattern: fsPromises.chmod($PATH, 0o666)
1105
+
1106
+ # OPEN REDIRECT
1107
+
1108
+ # 1. Server-side: res.redirect with an unvalidated request value.
1109
+ - id: narvy.javascript.openredirect.res-redirect-request-value.res-redirect-request-value
1110
+ message: >-
1111
+ res.redirect is called with a request-derived value used directly
1112
+ as the redirect target, with no allow-list or same-origin check.
1113
+ An attacker can craft a link that redirects victims to an
1114
+ attacker-controlled domain, useful for phishing after a trusted
1115
+ click (CWE-601). Validate that the target is a relative path or
1116
+ matches an explicit allow-list of hosts before redirecting.
1117
+ severity: WARNING
1118
+ languages: [javascript, typescript]
1119
+ metadata:
1120
+ cwe:
1121
+ - "CWE-601: URL Redirection to Untrusted Site ('Open Redirect')"
1122
+ owasp:
1123
+ - "A01:2021 - Broken Access Control"
1124
+ category: security
1125
+ subcategory: [vuln]
1126
+ technology: [express, nodejs, javascript, typescript]
1127
+ likelihood: MEDIUM
1128
+ impact: MEDIUM
1129
+ confidence: MEDIUM
1130
+ vulnerability_class:
1131
+ - Open Redirect
1132
+ references:
1133
+ - https://cwe.mitre.org/data/definitions/601.html
1134
+ patterns:
1135
+ - pattern-either:
1136
+ - pattern: $RES.redirect($REQ.$KIND.$X)
1137
+ - pattern: $RES.redirect($CODE, $REQ.$KIND.$X)
1138
+ - pattern: |
1139
+ $V = $REQ.$KIND.$X
1140
+ ...
1141
+ $RES.redirect($V)
1142
+ - pattern: |
1143
+ $V = $REQ.$KIND.$X
1144
+ ...
1145
+ $RES.redirect($CODE, $V)
1146
+ - metavariable-regex:
1147
+ metavariable: $KIND
1148
+ regex: ^(query|params|body)$
1149
+ - pattern-not-inside: |
1150
+ if (<... $ALLOWLIST.includes(...) ...>) { ... }
1151
+ - pattern-not-inside: |
1152
+ if (<... $V.startsWith("/") ...>) { ... }
1153
+
1154
+ # 2. Client-side: location assigned from a URL parameter (DOM open
1155
+ # redirect).
1156
+ - id: narvy.javascript.openredirect.dom-location-from-url-param.dom-location-from-url-param
1157
+ message: >-
1158
+ window.location (or .href) is assigned a value read straight from
1159
+ a URL query/hash parameter. A crafted link can point this at an
1160
+ attacker-controlled domain, i.e. DOM-based open redirect
1161
+ (CWE-601), and depending on how the value reaches the sink this
1162
+ can also enable javascript: URI execution. Validate the target is
1163
+ a relative, same-origin path before assigning it.
1164
+ severity: WARNING
1165
+ languages: [javascript, typescript]
1166
+ mode: taint
1167
+ metadata:
1168
+ cwe:
1169
+ - "CWE-601: URL Redirection to Untrusted Site ('Open Redirect')"
1170
+ owasp:
1171
+ - "A01:2021 - Broken Access Control"
1172
+ category: security
1173
+ subcategory: [vuln]
1174
+ technology: [javascript, typescript, dom]
1175
+ likelihood: LOW
1176
+ impact: MEDIUM
1177
+ confidence: LOW
1178
+ vulnerability_class:
1179
+ - Open Redirect
1180
+ pattern-sources:
1181
+ - pattern: new URLSearchParams(location.search).get(...)
1182
+ - pattern: new URLSearchParams(window.location.search).get(...)
1183
+ - pattern: location.hash
1184
+ - pattern: window.location.hash
1185
+ pattern-sinks:
1186
+ - pattern: window.location = $SINK
1187
+ - pattern: window.location.href = $SINK
1188
+ - pattern: location.href = $SINK
1189
+ - pattern: window.location.assign($SINK)
1190
+ - pattern: window.location.replace($SINK)
1191
+
1192
+ # PATH TRAVERSAL
1193
+
1194
+ # 1. fs.* sink fed by a path built from request data.
1195
+ - id: narvy.javascript.pathtraversal.fs-read-write-tainted-path.fs-read-write-tainted-path
1196
+ message: >-
1197
+ A filesystem call (readFile/writeFile/unlink/createReadStream) is
1198
+ given a path built from request data (path.join with a req.*
1199
+ segment, or direct string concatenation). Without validation an
1200
+ attacker can supply `../../` sequences to read or write files
1201
+ outside the intended directory (CWE-22). Resolve the path with
1202
+ path.resolve, then verify it still starts with the intended base
1203
+ directory before using it, or reject any segment containing `..`.
1204
+ severity: ERROR
1205
+ languages: [javascript, typescript]
1206
+ metadata:
1207
+ cwe:
1208
+ - "CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')"
1209
+ owasp:
1210
+ - "A01:2021 - Broken Access Control"
1211
+ category: security
1212
+ subcategory: [vuln]
1213
+ technology: [nodejs, javascript, typescript]
1214
+ likelihood: MEDIUM
1215
+ impact: HIGH
1216
+ confidence: MEDIUM
1217
+ vulnerability_class:
1218
+ - Path Traversal
1219
+ references:
1220
+ - https://cwe.mitre.org/data/definitions/22.html
1221
+ patterns:
1222
+ - pattern-either:
1223
+ - pattern: $FS.readFile(path.join($BASE, $REQ.$KIND.$X), ...)
1224
+ - pattern: $FS.readFileSync(path.join($BASE, $REQ.$KIND.$X), ...)
1225
+ - pattern: $FS.writeFile(path.join($BASE, $REQ.$KIND.$X), ...)
1226
+ - pattern: $FS.writeFileSync(path.join($BASE, $REQ.$KIND.$X), ...)
1227
+ - pattern: $FS.unlink(path.join($BASE, $REQ.$KIND.$X), ...)
1228
+ - pattern: $FS.unlinkSync(path.join($BASE, $REQ.$KIND.$X))
1229
+ - pattern: $FS.createReadStream(path.join($BASE, $REQ.$KIND.$X))
1230
+ - pattern: $FS.readFile("$DIR" + $REQ.$KIND.$X, ...)
1231
+ - pattern: $FS.readFileSync("$DIR" + $REQ.$KIND.$X)
1232
+ - pattern: $FS.readFile(`$DIR${$REQ.$KIND.$X}`, ...)
1233
+ - pattern: $FS.readFileSync(`$DIR${$REQ.$KIND.$X}`)
1234
+ - metavariable-regex:
1235
+ metavariable: $KIND
1236
+ regex: ^(query|params|body|headers|cookies)$
1237
+ - pattern-not-inside: |
1238
+ if (<... $REQ.$KIND.$X.includes("..") ...>) { ... }
1239
+ - pattern-not-inside: |
1240
+ if (<... !$RESOLVED.startsWith($BASE2) ...>) { ... }
1241
+ paths:
1242
+ exclude:
1243
+ - "**/test/**"
1244
+ - "**/tests/**"
1245
+ - "**/__tests__/**"
1246
+
1247
+ # 2. res.sendFile with a path built from request data.
1248
+ - id: narvy.javascript.pathtraversal.sendfile-tainted-path.sendfile-tainted-path
1249
+ message: >-
1250
+ res.sendFile is called with a path built from a request field via
1251
+ path.join, with no visible containment check. An attacker can
1252
+ submit `..` segments in the request field to read files outside
1253
+ the served directory (CWE-22). Use `root` option combined with a
1254
+ validated, normalized relative path, and reject values containing
1255
+ `..`.
1256
+ severity: ERROR
1257
+ languages: [javascript, typescript]
1258
+ metadata:
1259
+ cwe:
1260
+ - "CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')"
1261
+ owasp:
1262
+ - "A01:2021 - Broken Access Control"
1263
+ category: security
1264
+ subcategory: [vuln]
1265
+ technology: [express, nodejs, javascript, typescript]
1266
+ likelihood: MEDIUM
1267
+ impact: HIGH
1268
+ confidence: MEDIUM
1269
+ vulnerability_class:
1270
+ - Path Traversal
1271
+ patterns:
1272
+ - pattern: $RES.sendFile(path.join($BASE, $REQ.$KIND.$X), ...)
1273
+ - metavariable-regex:
1274
+ metavariable: $KIND
1275
+ regex: ^(query|params|body)$
1276
+ - pattern-not-inside: |
1277
+ if (<... $REQ.$KIND.$X.includes("..") ...>) { ... }
1278
+
1279
+ # POSTMESSAGE
1280
+
1281
+ - id: narvy.javascript.postmessage.wildcard-target-origin.wildcard-target-origin
1282
+ message: >-
1283
+ postMessage is called with "*" as the target origin. The message
1284
+ is delivered to whatever origin the target window currently has,
1285
+ which an attacker can control by first navigating the window/frame
1286
+ to their own page. If the message carries a token, session data,
1287
+ or any sensitive value, this leaks it to an attacker-controlled
1288
+ origin (CWE-346). Pass the exact expected origin instead of "*".
1289
+ severity: WARNING
1290
+ languages: [javascript, typescript]
1291
+ metadata:
1292
+ cwe:
1293
+ - "CWE-346: Origin Validation Error"
1294
+ owasp:
1295
+ - "A05:2021 - Security Misconfiguration"
1296
+ category: security
1297
+ subcategory: [vuln]
1298
+ technology: [javascript, typescript, dom]
1299
+ likelihood: LOW
1300
+ impact: MEDIUM
1301
+ confidence: MEDIUM
1302
+ vulnerability_class:
1303
+ - Other
1304
+ references:
1305
+ - https://cwe.mitre.org/data/definitions/346.html
1306
+ - https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#security_concerns
1307
+ patterns:
1308
+ - pattern-either:
1309
+ - pattern: $TARGET.postMessage($DATA, "*")
1310
+ - pattern: $TARGET.postMessage($DATA, '*')
1311
+
1312
+ - id: narvy.javascript.postmessage.handler-missing-origin-check.handler-missing-origin-check
1313
+ message: >-
1314
+ A "message" event handler is registered but never checks
1315
+ event.origin. Any page that can obtain a reference to this window
1316
+ (an iframe it embeds, a window it opened) can send it a message,
1317
+ and without an origin check the handler will treat that message as
1318
+ trusted (CWE-346). Validate event.origin against an exact expected
1319
+ value before acting on event.data.
1320
+ severity: WARNING
1321
+ languages: [javascript, typescript]
1322
+ metadata:
1323
+ cwe:
1324
+ - "CWE-346: Origin Validation Error"
1325
+ owasp:
1326
+ - "A05:2021 - Security Misconfiguration"
1327
+ category: security
1328
+ subcategory: [vuln]
1329
+ technology: [javascript, typescript, dom]
1330
+ likelihood: LOW
1331
+ impact: MEDIUM
1332
+ confidence: LOW
1333
+ vulnerability_class:
1334
+ - Other
1335
+ patterns:
1336
+ - pattern-either:
1337
+ - pattern: |
1338
+ window.addEventListener("message", function ($EVT) { ... })
1339
+ - pattern: |
1340
+ window.addEventListener("message", ($EVT) => { ... })
1341
+ - pattern: |
1342
+ addEventListener("message", function ($EVT) { ... })
1343
+ - pattern: |
1344
+ addEventListener("message", ($EVT) => { ... })
1345
+ - pattern-not: |
1346
+ window.addEventListener("message", function ($EVT) { <... $EVT.origin ...> })
1347
+ - pattern-not: |
1348
+ window.addEventListener("message", ($EVT) => { <... $EVT.origin ...> })
1349
+ - pattern-not: |
1350
+ addEventListener("message", function ($EVT) { <... $EVT.origin ...> })
1351
+ - pattern-not: |
1352
+ addEventListener("message", ($EVT) => { <... $EVT.origin ...> })
1353
+
1354
+ # SQL CONCAT
1355
+
1356
+ # Rule 2a: string-concat user input into .query/.execute/.raw (taint); focus $SINK, SQL-looking regex, receiver-scoped.
1357
+ - id: narvy.javascript.sqli.user-input-concat-query.user-input-concat-query
1358
+ message: >-
1359
+ User input from the request flows into a SQL string built by
1360
+ CONCATENATION and passed to a query/execute/raw sink. Classic SQL
1361
+ injection (CWE-89). Use a parameterized query: pass user input as the
1362
+ 2nd arg of .query(sql, [params]) or use a query builder / ORM with
1363
+ bound parameters.
1364
+ severity: ERROR
1365
+ languages: [javascript, typescript]
1366
+ mode: taint
1367
+ metadata:
1368
+ cwe:
1369
+ - "CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')"
1370
+ owasp:
1371
+ - "A03:2021 - Injection"
1372
+ - "A01:2017 - Injection"
1373
+ category: security
1374
+ subcategory: [vuln]
1375
+ technology: [pg, mysql, mysql2, sqlite3, knex, sequelize, typeorm, javascript, typescript]
1376
+ likelihood: HIGH
1377
+ impact: HIGH
1378
+ confidence: HIGH
1379
+ vulnerability_class:
1380
+ - Improper Validation
1381
+ references:
1382
+ - https://cwe.mitre.org/data/definitions/89.html
1383
+ - https://owasp.org/www-community/attacks/SQL_Injection
1384
+ pattern-sources:
1385
+ # Whole request container, so destructuring propagates taint.
1386
+ # Receiver name constrained - see PRECISION GUARD 4 above.
1387
+ - patterns:
1388
+ - pattern-either:
1389
+ - pattern: $REQ.body
1390
+ - pattern: $REQ.query
1391
+ - pattern: $REQ.params
1392
+ - pattern: $REQ.headers
1393
+ - pattern: $REQ.cookies
1394
+ - metavariable-regex:
1395
+ metavariable: $REQ
1396
+ regex: ^(req|request|_req|ctx|httpRequest|httpReq)$
1397
+ # Two-level access keeps the pre-taint rule's unconstrained receiver.
1398
+ - patterns:
1399
+ - pattern: $REQ.$KIND.$X
1400
+ - metavariable-regex:
1401
+ metavariable: $KIND
1402
+ regex: ^(body|params|query|headers|cookies)$
1403
+ # AWS Lambda proxy-integration handler event.
1404
+ - patterns:
1405
+ - pattern: $EVENT.$EKIND
1406
+ - metavariable-regex:
1407
+ metavariable: $EKIND
1408
+ regex: ^(queryStringParameters|pathParameters|multiValueQueryStringParameters|multiValueHeaders)$
1409
+ - pattern: JSON.parse($EVENT.body)
1410
+ # Raw (unparsed) Lambda body. Receiver pinned to event-shaped names,
1411
+ # because a bare `$X.body` also matches an OUTBOUND response body.
1412
+ - patterns:
1413
+ - pattern: $EVENT.body
1414
+ - metavariable-regex:
1415
+ metavariable: $EVENT
1416
+ regex: ^(event|evt|_event|lambdaEvent|apiEvent)$
1417
+ pattern-sanitizers:
1418
+ # Numeric coercion makes SQL metacharacters unrepresentable.
1419
+ - pattern: parseInt(...)
1420
+ - pattern: parseFloat(...)
1421
+ - pattern: Number(...)
1422
+ - pattern: BigInt(...)
1423
+ # Driver-provided escaping (mysql/mysql2 escape+escapeId, pg
1424
+ # escapeLiteral/escapeIdentifier, SqlString.escape, knex .escape).
1425
+ - pattern: escape(...)
1426
+ - pattern: $M.escape(...)
1427
+ - pattern: $M.escapeId(...)
1428
+ - pattern: $M.escapeLiteral(...)
1429
+ - pattern: $M.escapeIdentifier(...)
1430
+ # Allow-list / validator guards (wrapping-if and negated early-return).
1431
+ - patterns:
1432
+ - pattern: $V
1433
+ - pattern-inside: |
1434
+ if (<... $ALLOW.includes(...) ...>) { ... }
1435
+ - patterns:
1436
+ - pattern: $V
1437
+ - pattern-inside: |
1438
+ if (!$ALLOW.includes($V)) { ... }
1439
+ ...
1440
+ - patterns:
1441
+ - pattern: $V
1442
+ - pattern-inside: |
1443
+ if (!$ALLOW.includes($V)) return ...;
1444
+ ...
1445
+ - patterns:
1446
+ - pattern: $V
1447
+ - pattern-inside: |
1448
+ if (!$RE.test($V)) { ... }
1449
+ ...
1450
+ - patterns:
1451
+ - pattern: $V
1452
+ - pattern-inside: |
1453
+ if (!$RE.test($V)) return ...;
1454
+ ...
1455
+ pattern-sinks:
1456
+ - patterns:
1457
+ - pattern-either:
1458
+ - pattern: $CONN.query($SINK, ...)
1459
+ - pattern: $CONN.execute($SINK, ...)
1460
+ - pattern: $CONN.raw($SINK, ...)
1461
+ - focus-metavariable: $SINK
1462
+ # Concatenation only; the template-literal shape is the sibling
1463
+ # rule user-input-template-query.
1464
+ - metavariable-pattern:
1465
+ metavariable: $SINK
1466
+ pattern: $A + $B
1467
+ - metavariable-regex:
1468
+ metavariable: $SINK
1469
+ regex: '(?is).*\b(select|insert|update|delete|drop|alter|create|merge|truncate)\b.*'
1470
+
1471
+ # Rule 2b: template-literal user input into .query/.execute/.raw; tagged templates not matched.
1472
+ - id: narvy.javascript.sqli.user-input-template-query.user-input-template-query
1473
+ message: >-
1474
+ User input from the request is interpolated into a template-literal
1475
+ SQL string passed to a query/execute/raw sink. SQL injection (CWE-89).
1476
+ Use parameterized queries with positional placeholders ($1, ?).
1477
+ severity: ERROR
1478
+ languages: [javascript, typescript]
1479
+ mode: taint
1480
+ metadata:
1481
+ cwe:
1482
+ - "CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')"
1483
+ owasp:
1484
+ - "A03:2021 - Injection"
1485
+ category: security
1486
+ subcategory: [vuln]
1487
+ technology: [pg, mysql, mysql2, sqlite3, knex, sequelize, typeorm, javascript, typescript]
1488
+ likelihood: HIGH
1489
+ impact: HIGH
1490
+ confidence: HIGH
1491
+ vulnerability_class:
1492
+ - Improper Validation
1493
+ pattern-sources:
1494
+ - patterns:
1495
+ - pattern-either:
1496
+ - pattern: $REQ.body
1497
+ - pattern: $REQ.query
1498
+ - pattern: $REQ.params
1499
+ - pattern: $REQ.headers
1500
+ - pattern: $REQ.cookies
1501
+ - metavariable-regex:
1502
+ metavariable: $REQ
1503
+ regex: ^(req|request|_req|ctx|httpRequest|httpReq)$
1504
+ - patterns:
1505
+ - pattern: $REQ.$KIND.$X
1506
+ - metavariable-regex:
1507
+ metavariable: $KIND
1508
+ regex: ^(body|params|query|headers|cookies)$
1509
+ - patterns:
1510
+ - pattern: $EVENT.$EKIND
1511
+ - metavariable-regex:
1512
+ metavariable: $EKIND
1513
+ regex: ^(queryStringParameters|pathParameters|multiValueQueryStringParameters|multiValueHeaders)$
1514
+ - pattern: JSON.parse($EVENT.body)
1515
+ # Raw (unparsed) Lambda body. Receiver pinned to event-shaped names,
1516
+ # because a bare `$X.body` also matches an OUTBOUND response body.
1517
+ - patterns:
1518
+ - pattern: $EVENT.body
1519
+ - metavariable-regex:
1520
+ metavariable: $EVENT
1521
+ regex: ^(event|evt|_event|lambdaEvent|apiEvent)$
1522
+ pattern-sanitizers:
1523
+ - pattern: parseInt(...)
1524
+ - pattern: parseFloat(...)
1525
+ - pattern: Number(...)
1526
+ - pattern: BigInt(...)
1527
+ - pattern: escape(...)
1528
+ - pattern: $M.escape(...)
1529
+ - pattern: $M.escapeId(...)
1530
+ - pattern: $M.escapeLiteral(...)
1531
+ - pattern: $M.escapeIdentifier(...)
1532
+ - patterns:
1533
+ - pattern: $V
1534
+ - pattern-inside: |
1535
+ if (<... $ALLOW.includes(...) ...>) { ... }
1536
+ - patterns:
1537
+ - pattern: $V
1538
+ - pattern-inside: |
1539
+ if (!$ALLOW.includes($V)) { ... }
1540
+ ...
1541
+ - patterns:
1542
+ - pattern: $V
1543
+ - pattern-inside: |
1544
+ if (!$ALLOW.includes($V)) return ...;
1545
+ ...
1546
+ - patterns:
1547
+ - pattern: $V
1548
+ - pattern-inside: |
1549
+ if (!$RE.test($V)) { ... }
1550
+ ...
1551
+ - patterns:
1552
+ - pattern: $V
1553
+ - pattern-inside: |
1554
+ if (!$RE.test($V)) return ...;
1555
+ ...
1556
+ pattern-sinks:
1557
+ - patterns:
1558
+ - pattern-either:
1559
+ - pattern: $CONN.query($SINK, ...)
1560
+ - pattern: $CONN.execute($SINK, ...)
1561
+ - pattern: $CONN.raw($SINK, ...)
1562
+ - focus-metavariable: $SINK
1563
+ # Template-literal shape and the SQL-looking check in one regex.
1564
+ - metavariable-regex:
1565
+ metavariable: $SINK
1566
+ regex: '(?is)^`.*\b(select|insert|update|delete|drop|alter|create|merge|truncate)\b.*'
1567
+
1568
+ # Rule 2c: SQL built into a variable then passed to sink; syntactic, requires request data + a SQL keyword in one RHS.
1569
+ - id: narvy.javascript.sqli.user-input-concat-query-var-indirect.user-input-concat-query-var-indirect
1570
+ message: >-
1571
+ A SQL string that concatenates / interpolates request data
1572
+ (query/params/body/headers/cookies) is built into a variable and then
1573
+ passed to a query/execute/raw sink. SQL injection (CWE-89), with one
1574
+ level of variable indirection. Use a parameterized query: pass user
1575
+ input as bound parameters, e.g. .query(sql, [params]).
1576
+ severity: ERROR
1577
+ languages: [javascript, typescript]
1578
+ metadata:
1579
+ cwe:
1580
+ - "CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')"
1581
+ owasp:
1582
+ - "A03:2021 - Injection"
1583
+ category: security
1584
+ subcategory: [vuln]
1585
+ technology: [pg, mysql, mysql2, sqlite3, knex, sequelize, typeorm, javascript, typescript]
1586
+ likelihood: HIGH
1587
+ impact: HIGH
1588
+ confidence: HIGH
1589
+ vulnerability_class:
1590
+ - Improper Validation
1591
+ references:
1592
+ - https://cwe.mitre.org/data/definitions/89.html
1593
+ - https://owasp.org/www-community/attacks/SQL_Injection
1594
+ patterns:
1595
+ - pattern-either:
1596
+ # concatenation carrying request data + a SQL-keyword string
1597
+ - patterns:
1598
+ - pattern: |
1599
+ $Q = <... $REQ.$KIND.$X ...>
1600
+ ...
1601
+ $CONN.$M($Q, ...)
1602
+ - pattern: |
1603
+ $Q = <... "$SQL" ...>
1604
+ ...
1605
+ $CONN.$M($Q, ...)
1606
+ - metavariable-regex:
1607
+ metavariable: $SQL
1608
+ regex: '(?is).*\b(select|insert|update|delete|drop|alter|create|merge|truncate)\b.*'
1609
+ # template literal carrying request data
1610
+ - patterns:
1611
+ - pattern: |
1612
+ $Q = <... $REQ.$KIND.$X ...>
1613
+ ...
1614
+ $CONN.$M($Q, ...)
1615
+ - pattern: |
1616
+ $Q = `...${...}...`
1617
+ ...
1618
+ $CONN.$M($Q, ...)
1619
+ - metavariable-regex:
1620
+ metavariable: $KIND
1621
+ regex: ^(query|params|body|headers|cookies)$
1622
+ - metavariable-regex:
1623
+ metavariable: $REQ
1624
+ regex: ^(req|request|_req|ctx|httpRequest|httpReq)$
1625
+ - metavariable-regex:
1626
+ metavariable: $M
1627
+ regex: ^(query|execute|raw)$
1628
+
1629
+ # SQLI ORM
1630
+
1631
+ - id: narvy.javascript.sqli.sequelize-literal-tainted.sequelize-literal-tainted
1632
+ message: >-
1633
+ sequelize.literal is called with a SQL fragment built from request
1634
+ data. literal() inserts its argument into the query completely
1635
+ unescaped - it exists specifically to bypass Sequelize's
1636
+ parameterization, so request data reaching it is direct SQL
1637
+ injection (CWE-89). Use a parameterized where-clause or bind
1638
+ variables instead of sequelize.literal for anything derived from
1639
+ user input.
1640
+ severity: ERROR
1641
+ languages: [javascript, typescript]
1642
+ metadata:
1643
+ cwe:
1644
+ - "CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')"
1645
+ owasp:
1646
+ - "A03:2021 - Injection"
1647
+ category: security
1648
+ subcategory: [vuln]
1649
+ technology: [sequelize, nodejs, javascript, typescript]
1650
+ likelihood: HIGH
1651
+ impact: HIGH
1652
+ confidence: HIGH
1653
+ vulnerability_class:
1654
+ - SQL Injection
1655
+ references:
1656
+ - https://cwe.mitre.org/data/definitions/89.html
1657
+ patterns:
1658
+ - pattern-either:
1659
+ - patterns:
1660
+ - pattern-either:
1661
+ - pattern: sequelize.literal("$SQL" + $REQ.$KIND.$X + ...)
1662
+ - pattern: sequelize.literal(`...${$REQ.$KIND.$X}...`)
1663
+ - pattern: |
1664
+ $V = $REQ.$KIND.$X
1665
+ ...
1666
+ sequelize.literal("$SQL" + $V + ...)
1667
+ - pattern: |
1668
+ $V = $REQ.$KIND.$X
1669
+ ...
1670
+ sequelize.literal(`...${$V}...`)
1671
+ - metavariable-regex:
1672
+ metavariable: $KIND
1673
+ regex: ^(query|params|body|headers|cookies)$
1674
+ # AWS Lambda handler taint source (see user-input-concat-query for
1675
+ # the nesting rationale).
1676
+ - patterns:
1677
+ - pattern-either:
1678
+ - pattern: sequelize.literal("$SQL" + $EVENT.$EKIND.$X + ...)
1679
+ - pattern: sequelize.literal(`...${$EVENT.$EKIND.$X}...`)
1680
+ - pattern: |
1681
+ $V = $EVENT.$EKIND.$X
1682
+ ...
1683
+ sequelize.literal("$SQL" + $V + ...)
1684
+ - pattern: |
1685
+ $V = $EVENT.$EKIND.$X
1686
+ ...
1687
+ sequelize.literal(`...${$V}...`)
1688
+ - metavariable-regex:
1689
+ metavariable: $EKIND
1690
+ regex: ^(queryStringParameters|pathParameters|headers|multiValueQueryStringParameters|multiValueHeaders)$
1691
+ - pattern: sequelize.literal("$SQL" + JSON.parse($EVENT.body).$X + ...)
1692
+
1693
+ - id: narvy.javascript.sqli.mongoose-where-tainted.mongoose-where-tainted
1694
+ message: >-
1695
+ A Mongoose query's `$where` clause is built from request data.
1696
+ $where runs its string as server-side JavaScript against every
1697
+ document in the collection, so request-controlled input here is
1698
+ effectively server-side code injection through the database layer
1699
+ (CWE-943 / CWE-94), not just NoSQL injection. Remove $where and
1700
+ express the same condition with regular MongoDB query operators.
1701
+ severity: ERROR
1702
+ languages: [javascript, typescript]
1703
+ metadata:
1704
+ cwe:
1705
+ - "CWE-943: Improper Neutralization of Special Elements in Data Query Logic"
1706
+ owasp:
1707
+ - "A03:2021 - Injection"
1708
+ category: security
1709
+ subcategory: [vuln]
1710
+ technology: [mongoose, mongodb, nodejs, javascript, typescript]
1711
+ likelihood: HIGH
1712
+ impact: HIGH
1713
+ confidence: HIGH
1714
+ vulnerability_class:
1715
+ - SQL Injection
1716
+ patterns:
1717
+ - pattern-either:
1718
+ - patterns:
1719
+ - pattern-either:
1720
+ - pattern: |
1721
+ {..., $where: "$JS" + $REQ.$KIND.$X + ..., ...}
1722
+ - pattern: |
1723
+ {..., $where: `...${$REQ.$KIND.$X}...`, ...}
1724
+ - metavariable-regex:
1725
+ metavariable: $KIND
1726
+ regex: ^(query|params|body|headers|cookies)$
1727
+ # AWS Lambda handler taint source (see user-input-concat-query for
1728
+ # the nesting rationale).
1729
+ - patterns:
1730
+ - pattern-either:
1731
+ - pattern: |
1732
+ {..., $where: "$JS" + $EVENT.$EKIND.$X + ..., ...}
1733
+ - pattern: |
1734
+ {..., $where: `...${$EVENT.$EKIND.$X}...`, ...}
1735
+ - metavariable-regex:
1736
+ metavariable: $EKIND
1737
+ regex: ^(queryStringParameters|pathParameters|headers|multiValueQueryStringParameters|multiValueHeaders)$
1738
+
1739
+ # SSRF REQUEST
1740
+
1741
+ - id: narvy.javascript.ssrf.request-url-from-user.request-url-from-user
1742
+ message: >-
1743
+ A request-derived value flows into an outbound HTTP client (http/https
1744
+ get/request, axios, fetch, got, node-fetch, request) as the target URL.
1745
+ An attacker controlling this value can force the server to make requests
1746
+ to internal services (cloud metadata, localhost admin ports), i.e.
1747
+ Server-Side Request Forgery (CWE-918). Validate the URL against an
1748
+ allow-list of hosts/schemes before the request.
1749
+ severity: ERROR
1750
+ languages:
1751
+ - javascript
1752
+ - typescript
1753
+ mode: taint
1754
+ metadata:
1755
+ cwe:
1756
+ - "CWE-918: Server-Side Request Forgery (SSRF)"
1757
+ owasp:
1758
+ - "A10:2021 - Server-Side Request Forgery (SSRF)"
1759
+ category: security
1760
+ confidence: MEDIUM
1761
+ references:
1762
+ - https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/
1763
+ pattern-sources:
1764
+ - pattern: req.query.$Q
1765
+ - pattern: req.params.$P
1766
+ - pattern: req.body.$B
1767
+ - pattern: req.headers.$H
1768
+ - pattern: req.query
1769
+ - pattern: req.params
1770
+ - pattern: request.query.$Q
1771
+ - pattern: request.params.$P
1772
+ - pattern: $CTX.request.query.$Q
1773
+ pattern-sanitizers:
1774
+ # An allow-list / URL-validator guard (safeFetch wrappers, host/scheme
1775
+ # validators) is the canonical SSRF mitigation and kills the taint.
1776
+ - pattern: safeFetch(...)
1777
+ - pattern: isAllowedUrl(...)
1778
+ - pattern: isAllowedHost(...)
1779
+ - pattern: isSafeUrl(...)
1780
+ - pattern: validateUrl(...)
1781
+ - pattern: validateHost(...)
1782
+ - patterns:
1783
+ - pattern: $V
1784
+ - pattern-inside: |
1785
+ if (<... $ALLOW.includes(...) ...>) { ... }
1786
+ pattern-sinks:
1787
+ # Every sink pins focus $SINK to the URL arg; $HTTP scoped to http/https.
1788
+ - patterns:
1789
+ - pattern: $HTTP.get($SINK, ...)
1790
+ - focus-metavariable: $SINK
1791
+ - metavariable-regex:
1792
+ metavariable: $HTTP
1793
+ regex: ^(http|https)$
1794
+ - patterns:
1795
+ - pattern: $HTTP.request($SINK, ...)
1796
+ - focus-metavariable: $SINK
1797
+ - metavariable-regex:
1798
+ metavariable: $HTTP
1799
+ regex: ^(http|https)$
1800
+ - patterns:
1801
+ - pattern: axios($SINK, ...)
1802
+ - focus-metavariable: $SINK
1803
+ - patterns:
1804
+ - pattern: axios.get($SINK, ...)
1805
+ - focus-metavariable: $SINK
1806
+ - patterns:
1807
+ - pattern: axios.request($SINK, ...)
1808
+ - focus-metavariable: $SINK
1809
+ - patterns:
1810
+ - pattern: fetch($SINK, ...)
1811
+ - focus-metavariable: $SINK
1812
+ - patterns:
1813
+ - pattern: got($SINK, ...)
1814
+ - focus-metavariable: $SINK
1815
+ - patterns:
1816
+ - pattern: got.get($SINK, ...)
1817
+ - focus-metavariable: $SINK
1818
+ # Scope request($SINK, ...) to a request/needle/node-fetch client assigned to a local.
1819
+ - patterns:
1820
+ - pattern: $REQ($SINK, ...)
1821
+ - focus-metavariable: $SINK
1822
+ - metavariable-pattern:
1823
+ metavariable: $REQ
1824
+ patterns:
1825
+ - pattern-either:
1826
+ - pattern: request
1827
+ - pattern: needle
1828
+ - pattern-inside: |
1829
+ $REQ = require("...");
1830
+ ...
1831
+ - patterns:
1832
+ - pattern: needle($M, $SINK, ...)
1833
+ - focus-metavariable: $SINK
1834
+ # needle method-style helpers; identifier must be a local from require("needle").
1835
+ - patterns:
1836
+ - pattern: $NEEDLE.$METHOD($SINK, ...)
1837
+ - focus-metavariable: $SINK
1838
+ - metavariable-regex:
1839
+ metavariable: $METHOD
1840
+ regex: ^(get|post|put|patch|delete|head)$
1841
+ - metavariable-pattern:
1842
+ metavariable: $NEEDLE
1843
+ patterns:
1844
+ - pattern: needle
1845
+ - pattern-inside: |
1846
+ $NEEDLE = require("needle");
1847
+ ...
1848
+
1849
+ # SSTI
1850
+
1851
+ - id: narvy.javascript.ssti.template-engine-compiles-request-data.template-engine-compiles-request-data
1852
+ message: >-
1853
+ A request-derived value is passed as the TEMPLATE STRING to a
1854
+ template engine's render/compile function (ejs, handlebars, pug,
1855
+ lodash template, Mustache). These engines evaluate JS expressions
1856
+ found inside the template source, so an attacker-controlled
1857
+ template string means arbitrary server-side code execution
1858
+ (CWE-1336 / effectively CWE-94). Never compile a template from
1859
+ request data - only bind request data as template variables into a
1860
+ fixed, trusted template.
1861
+ severity: ERROR
1862
+ languages: [javascript, typescript]
1863
+ mode: taint
1864
+ metadata:
1865
+ cwe:
1866
+ - "CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine"
1867
+ owasp:
1868
+ - "A03:2021 - Injection"
1869
+ category: security
1870
+ subcategory: [vuln]
1871
+ technology: [ejs, handlebars, pug, lodash, mustache, javascript, typescript]
1872
+ likelihood: LOW
1873
+ impact: HIGH
1874
+ confidence: MEDIUM
1875
+ vulnerability_class:
1876
+ - Code Injection
1877
+ references:
1878
+ - https://cwe.mitre.org/data/definitions/1336.html
1879
+ - https://portswigger.net/web-security/server-side-template-injection
1880
+ pattern-sources:
1881
+ - pattern: req.query.$Q
1882
+ - pattern: req.params.$P
1883
+ - pattern: req.body.$B
1884
+ pattern-sinks:
1885
+ - pattern: ejs.render($SINK, ...)
1886
+ - pattern: ejs.compile($SINK, ...)
1887
+ - pattern: handlebars.compile($SINK, ...)
1888
+ - pattern: Handlebars.compile($SINK, ...)
1889
+ - pattern: pug.render($SINK, ...)
1890
+ - pattern: pug.compile($SINK, ...)
1891
+ - pattern: _.template($SINK)
1892
+ - pattern: Mustache.render($SINK, ...)
1893
+
1894
+ - id: narvy.javascript.ssti.res-render-view-name-from-request.res-render-view-name-from-request
1895
+ message: >-
1896
+ The view name passed to res.render is taken directly from a
1897
+ request field. An attacker who controls the view name can render
1898
+ views not meant to be reachable directly, and on engines/setups
1899
+ that resolve the name to a filesystem path this can be combined
1900
+ with path traversal to render arbitrary files as a template
1901
+ (CWE-73). Restrict the view name to a fixed allow-list instead of
1902
+ passing the request value through.
1903
+ severity: WARNING
1904
+ languages: [javascript, typescript]
1905
+ metadata:
1906
+ cwe:
1907
+ - "CWE-73: External Control of File Name or Path"
1908
+ owasp:
1909
+ - "A03:2021 - Injection"
1910
+ category: security
1911
+ subcategory: [vuln]
1912
+ technology: [express, nodejs, javascript, typescript]
1913
+ likelihood: LOW
1914
+ impact: MEDIUM
1915
+ confidence: MEDIUM
1916
+ vulnerability_class:
1917
+ - Code Injection
1918
+ patterns:
1919
+ - pattern-either:
1920
+ - pattern: $RES.render($REQ.$KIND.$X, ...)
1921
+ - pattern: |
1922
+ $V = $REQ.$KIND.$X
1923
+ ...
1924
+ $RES.render($V, ...)
1925
+ - metavariable-regex:
1926
+ metavariable: $KIND
1927
+ regex: ^(query|params|body)$
1928
+
1929
+ # WEAK CRYPTO CIPHER
1930
+
1931
+ # Rule 1: weak cipher algorithm / ECB mode
1932
+ - id: narvy.javascript.crypto.weak-cipher-createcipheriv.weak-cipher-createcipheriv
1933
+ message: >-
1934
+ Weak or broken cipher algorithm passed to crypto.createCipheriv /
1935
+ createCipher. DES and RC4 are cryptographically broken; ECB mode
1936
+ leaks plaintext structure regardless of the underlying block cipher.
1937
+ Use AES-256-GCM (authenticated encryption) instead. CWE-327.
1938
+ severity: ERROR
1939
+ languages: [javascript, typescript]
1940
+ metadata:
1941
+ cwe:
1942
+ - "CWE-327: Use of a Broken or Risky Cryptographic Algorithm"
1943
+ - "CWE-329: Generation of Predictable IV with CBC Mode"
1944
+ owasp:
1945
+ - "A02:2021 - Cryptographic Failures"
1946
+ category: security
1947
+ subcategory: [vuln]
1948
+ technology: [node-crypto, javascript, typescript]
1949
+ likelihood: MEDIUM
1950
+ impact: HIGH
1951
+ confidence: HIGH
1952
+ vulnerability_class:
1953
+ - Cryptographic Issues
1954
+ patterns:
1955
+ - pattern-either:
1956
+ - pattern: crypto.createCipheriv("$ALGO", ...)
1957
+ - pattern: crypto.createCipheriv('$ALGO', ...)
1958
+ - pattern: crypto.createCipher("$ALGO", ...)
1959
+ - pattern: crypto.createCipher('$ALGO', ...)
1960
+ - pattern: $CRYPTO.createCipheriv("$ALGO", ...)
1961
+ - pattern: $CRYPTO.createCipheriv('$ALGO', ...)
1962
+ - metavariable-regex:
1963
+ metavariable: $ALGO
1964
+ regex: ^(?i).*(des|rc4|arcfour|-ecb|^ecb).*$
1965
+
1966
+ # Rule 1b: GCM decipher never finalized; without .final() tampered ciphertext decrypts.
1967
+ - id: narvy.javascript.crypto.gcm-decrypt-missing-final.gcm-decrypt-missing-final
1968
+ message: >-
1969
+ A GCM-mode decipher is created and fed ciphertext via .update(), but
1970
+ .final() is never called on it. In Node's crypto module the GCM
1971
+ authentication tag is only checked when .final() runs - it throws if
1972
+ the tag doesn't match the ciphertext. Skipping .final() means
1973
+ tampered ciphertext is silently accepted as if it were genuine
1974
+ (CWE-354). Always call .final() on a GCM decipher and let it throw.
1975
+ severity: ERROR
1976
+ languages: [javascript, typescript]
1977
+ metadata:
1978
+ cwe:
1979
+ - "CWE-354: Improper Validation of Integrity Check Value"
1980
+ owasp:
1981
+ - "A02:2021 - Cryptographic Failures"
1982
+ category: security
1983
+ subcategory: [vuln]
1984
+ technology: [node-crypto, javascript, typescript]
1985
+ likelihood: MEDIUM
1986
+ impact: HIGH
1987
+ confidence: MEDIUM
1988
+ vulnerability_class:
1989
+ - Cryptographic Issues
1990
+ patterns:
1991
+ - pattern: $CRYPTO.createDecipheriv("$ALGO", ...)
1992
+ - metavariable-regex:
1993
+ metavariable: $ALGO
1994
+ regex: (?i).*gcm.*
1995
+ - pattern-not-inside: |
1996
+ $DECIPHER = $CRYPTO.createDecipheriv("$ALGO", ...)
1997
+ ...
1998
+ $DECIPHER.final(...)
1999
+
2000
+ # Rule 1c: deprecated createCipher/createDecipher derives a static IV from the key.
2001
+ - id: narvy.javascript.crypto.deprecated-cipher-static-iv.deprecated-cipher-static-iv
2002
+ message: >-
2003
+ crypto.createCipher()/createDecipher() is deprecated and derives the
2004
+ IV internally from the key material via an undocumented KDF instead
2005
+ of accepting an explicit, random IV. The same key therefore always
2006
+ produces the same derived IV, so encrypting the same plaintext twice
2007
+ with the same key yields the same ciphertext, and CBC-mode chosen-
2008
+ plaintext attacks become possible (CWE-329). Use createCipheriv() /
2009
+ createDecipheriv() with a fresh, cryptographically random IV per
2010
+ encryption instead.
2011
+ severity: ERROR
2012
+ languages: [javascript, typescript]
2013
+ metadata:
2014
+ cwe:
2015
+ - "CWE-329: Generation of Predictable IV with CBC Mode"
2016
+ - "CWE-330: Use of Insufficiently Random Values"
2017
+ owasp:
2018
+ - "A02:2021 - Cryptographic Failures"
2019
+ category: security
2020
+ subcategory: [vuln]
2021
+ technology: [node-crypto, javascript, typescript]
2022
+ likelihood: MEDIUM
2023
+ impact: HIGH
2024
+ confidence: HIGH
2025
+ vulnerability_class:
2026
+ - Cryptographic Issues
2027
+ patterns:
2028
+ - pattern-either:
2029
+ - pattern: crypto.createCipher($ALGO, ...)
2030
+ - pattern: crypto.createDecipher($ALGO, ...)
2031
+ - pattern: $CRYPTO.createCipher($ALGO, ...)
2032
+ - pattern: $CRYPTO.createDecipher($ALGO, ...)
2033
+
2034
+ # Rule 1d: GCM auth tag from request data used without a length check.
2035
+ - id: narvy.javascript.crypto.gcm-tag-unvalidated-length.gcm-tag-unvalidated-length
2036
+ message: >-
2037
+ A GCM auth tag taken directly from request data is passed to
2038
+ setAuthTag() with no length check beforehand. GCM's forgery
2039
+ resistance depends on the full-length tag (16 bytes); an
2040
+ attacker-supplied truncated tag has a much higher chance of
2041
+ passing verification by luck, and some deployments accept
2042
+ whatever length is given (CWE-327). Reject the request unless the
2043
+ tag is exactly the expected length (normally 16 bytes) before
2044
+ calling setAuthTag().
2045
+ severity: WARNING
2046
+ languages: [javascript, typescript]
2047
+ metadata:
2048
+ cwe:
2049
+ - "CWE-327: Use of a Broken or Risky Cryptographic Algorithm"
2050
+ owasp:
2051
+ - "A02:2021 - Cryptographic Failures"
2052
+ category: security
2053
+ subcategory: [vuln]
2054
+ technology: [node-crypto, javascript, typescript]
2055
+ likelihood: LOW
2056
+ impact: HIGH
2057
+ confidence: MEDIUM
2058
+ vulnerability_class:
2059
+ - Cryptographic Issues
2060
+ patterns:
2061
+ - pattern-either:
2062
+ - pattern: $DECIPHER.setAuthTag(Buffer.from($REQ.$KIND.$X, ...))
2063
+ - pattern: $DECIPHER.setAuthTag($REQ.$KIND.$X)
2064
+ - metavariable-regex:
2065
+ metavariable: $KIND
2066
+ regex: ^(query|params|body|headers|cookies)$
2067
+ - pattern-not-inside: |
2068
+ if (<... $LEN.length ...>) { ... }
2069
+
2070
+ # Rule 2: Math.random() feeding a token / secret / password variable
2071
+ - id: narvy.javascript.crypto.math-random-security-token.math-random-security-token
2072
+ message: >-
2073
+ `Math.random()` is assigned to a variable whose name indicates
2074
+ security-sensitive use (token, secret, password, otp, session, nonce,
2075
+ apikey, csrf). Math.random() is not cryptographically secure - its
2076
+ output is predictable from a handful of samples. Use
2077
+ `crypto.randomBytes()` / `crypto.randomUUID()` for anything
2078
+ security-relevant. CWE-338.
2079
+ severity: WARNING
2080
+ languages: [javascript, typescript]
2081
+ metadata:
2082
+ cwe:
2083
+ - "CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)"
2084
+ - "CWE-330: Use of Insufficiently Random Values"
2085
+ owasp:
2086
+ - "A02:2021 - Cryptographic Failures"
2087
+ category: security
2088
+ subcategory: [audit]
2089
+ technology: [javascript, typescript]
2090
+ likelihood: MEDIUM
2091
+ impact: HIGH
2092
+ confidence: MEDIUM
2093
+ vulnerability_class:
2094
+ - Insufficient Entropy
2095
+ patterns:
2096
+ - pattern-either:
2097
+ - pattern: const $TOKEN = Math.random()
2098
+ - pattern: let $TOKEN = Math.random()
2099
+ - pattern: var $TOKEN = Math.random()
2100
+ - pattern: $TOKEN = Math.random()
2101
+ - metavariable-regex:
2102
+ metavariable: $TOKEN
2103
+ regex: (?i)(token|secret|password|passwd|pwd|otp|session|nonce|apikey|api_key|csrf|auth)
2104
+
2105
+ # Rule 3: undersized RSA key generation
2106
+ - id: narvy.javascript.crypto.weak-rsa-keysize.weak-rsa-keysize
2107
+ message: >-
2108
+ RSA key pair generated with `modulusLength` below 2048 bits. Keys
2109
+ under 2048 bits are considered breakable with modern compute and are
2110
+ rejected by NIST guidance for new key generation. Use at least 2048
2111
+ (prefer 3072/4096) for RSA, or switch to an elliptic-curve algorithm.
2112
+ CWE-326.
2113
+ severity: WARNING
2114
+ languages: [javascript, typescript]
2115
+ metadata:
2116
+ cwe:
2117
+ - "CWE-326: Inadequate Encryption Strength"
2118
+ owasp:
2119
+ - "A02:2021 - Cryptographic Failures"
2120
+ category: security
2121
+ subcategory: [audit]
2122
+ technology: [node-crypto, javascript, typescript]
2123
+ likelihood: LOW
2124
+ impact: HIGH
2125
+ confidence: HIGH
2126
+ vulnerability_class:
2127
+ - Cryptographic Issues
2128
+ patterns:
2129
+ - pattern-either:
2130
+ - pattern: 'crypto.generateKeyPair("rsa", { modulusLength: $N, ... }, ...)'
2131
+ - pattern: 'crypto.generateKeyPairSync("rsa", { modulusLength: $N, ... }, ...)'
2132
+ - metavariable-comparison:
2133
+ metavariable: $N
2134
+ comparison: $N < 2048
2135
+
2136
+ # WEAK CRYPTO
2137
+
2138
+ # Rule 1a: Node crypto.createHash('md5' | 'sha1')
2139
+ - id: narvy.javascript.crypto.weak-hash-create-hash.weak-hash-create-hash
2140
+ message: >-
2141
+ Weak hash MD5/SHA1 used via crypto.createHash. Both algorithms are
2142
+ cryptographically broken (MD5 collision, SHA1 collision). Use SHA-256
2143
+ or stronger for integrity. For password hashing use scrypt, Argon2,
2144
+ or PBKDF2 with high iteration count. CWE-327.
2145
+ severity: ERROR
2146
+ languages: [javascript, typescript]
2147
+ metadata:
2148
+ cwe:
2149
+ - "CWE-327: Use of a Broken or Risky Cryptographic Algorithm"
2150
+ owasp:
2151
+ - "A02:2021 - Cryptographic Failures"
2152
+ - "A03:2017 - Sensitive Data Exposure"
2153
+ category: security
2154
+ subcategory: [vuln]
2155
+ technology: [node-crypto, javascript, typescript]
2156
+ likelihood: HIGH
2157
+ impact: HIGH
2158
+ confidence: HIGH
2159
+ vulnerability_class:
2160
+ - Cryptographic Issues
2161
+ references:
2162
+ - https://cwe.mitre.org/data/definitions/327.html
2163
+ - https://tools.ietf.org/html/rfc6151
2164
+ patterns:
2165
+ - pattern-either:
2166
+ - pattern: crypto.createHash("$ALGO")
2167
+ - pattern: crypto.createHash('$ALGO')
2168
+ - pattern: $CRYPTO.createHash("$ALGO")
2169
+ - pattern: $CRYPTO.createHash('$ALGO')
2170
+ - metavariable-regex:
2171
+ metavariable: $ALGO
2172
+ regex: ^(?i)(md5|md4|md2|sha-?1)$
2173
+
2174
+ # Rule 1b: crypto-js MD5 / SHA1 module method
2175
+ - id: narvy.javascript.crypto.weak-hash-crypto-js.weak-hash-crypto-js
2176
+ message: >-
2177
+ Weak hash MD5/SHA1 used via crypto-js. Both algorithms are
2178
+ cryptographically broken. Use crypto-js SHA-256 / SHA-512 or
2179
+ stronger. CWE-327.
2180
+ severity: ERROR
2181
+ languages: [javascript, typescript]
2182
+ metadata:
2183
+ cwe:
2184
+ - "CWE-327: Use of a Broken or Risky Cryptographic Algorithm"
2185
+ owasp:
2186
+ - "A02:2021 - Cryptographic Failures"
2187
+ category: security
2188
+ subcategory: [vuln]
2189
+ technology: [crypto-js, javascript, typescript]
2190
+ likelihood: HIGH
2191
+ impact: HIGH
2192
+ confidence: HIGH
2193
+ vulnerability_class:
2194
+ - Cryptographic Issues
2195
+ patterns:
2196
+ - pattern-either:
2197
+ - pattern: $CRYPTOJS.MD5(...)
2198
+ - pattern: $CRYPTOJS.SHA1(...)
2199
+ - pattern: $CRYPTOJS.HmacMD5(...)
2200
+ - pattern: $CRYPTOJS.HmacSHA1(...)
2201
+
2202
+ # Rule 1c: node-forge md5 / sha1
2203
+ - id: narvy.javascript.crypto.weak-hash-forge.weak-hash-forge
2204
+ message: >-
2205
+ Weak hash MD5/SHA1 used via node-forge. Both algorithms are
2206
+ cryptographically broken. Use forge.md.sha256.create() or stronger.
2207
+ CWE-327.
2208
+ severity: ERROR
2209
+ languages: [javascript, typescript]
2210
+ metadata:
2211
+ cwe:
2212
+ - "CWE-327: Use of a Broken or Risky Cryptographic Algorithm"
2213
+ owasp:
2214
+ - "A02:2021 - Cryptographic Failures"
2215
+ category: security
2216
+ subcategory: [vuln]
2217
+ technology: [node-forge, javascript, typescript]
2218
+ likelihood: HIGH
2219
+ impact: HIGH
2220
+ confidence: HIGH
2221
+ vulnerability_class:
2222
+ - Cryptographic Issues
2223
+ patterns:
2224
+ - pattern-either:
2225
+ - pattern: $FORGE.md.md5.create()
2226
+ - pattern: $FORGE.md.sha1.create()
2227
+ - pattern: forge.md.md5.create()
2228
+ - pattern: forge.md.sha1.create()
2229
+
2230
+ # Rule 1d: Web Crypto subtle.digest with SHA-1
2231
+ - id: narvy.javascript.crypto.weak-hash-web-crypto.weak-hash-web-crypto
2232
+ message: >-
2233
+ Weak hash SHA-1 used via Web Crypto API (crypto.subtle.digest).
2234
+ SHA-1 is collision-broken. Use 'SHA-256' or 'SHA-512' instead.
2235
+ CWE-327.
2236
+ severity: ERROR
2237
+ languages: [javascript, typescript]
2238
+ metadata:
2239
+ cwe:
2240
+ - "CWE-327: Use of a Broken or Risky Cryptographic Algorithm"
2241
+ owasp:
2242
+ - "A02:2021 - Cryptographic Failures"
2243
+ category: security
2244
+ subcategory: [vuln]
2245
+ technology: [web-crypto, javascript, typescript]
2246
+ likelihood: HIGH
2247
+ impact: HIGH
2248
+ confidence: HIGH
2249
+ vulnerability_class:
2250
+ - Cryptographic Issues
2251
+ patterns:
2252
+ - pattern-either:
2253
+ - pattern: $SUBTLE.digest("$ALGO", ...)
2254
+ - pattern: $SUBTLE.digest('$ALGO', ...)
2255
+ - metavariable-regex:
2256
+ metavariable: $ALGO
2257
+ regex: ^(?i)(sha-?1|md5)$
2258
+
2259
+ # WEAK TLS
2260
+
2261
+ - id: narvy.javascript.tls.reject-unauthorized-disabled.reject-unauthorized-disabled
2262
+ message: >-
2263
+ TLS certificate validation is disabled via `rejectUnauthorized:
2264
+ false`. This accepts ANY certificate, including a self-signed one
2265
+ presented by an attacker performing a man-in-the-middle attack,
2266
+ defeating the entire purpose of TLS (CWE-295). Remove this option;
2267
+ if a private/internal CA is in use, add its certificate to the
2268
+ trust store (the `ca` option) instead of disabling verification.
2269
+ severity: ERROR
2270
+ languages: [javascript, typescript]
2271
+ metadata:
2272
+ cwe:
2273
+ - "CWE-295: Improper Certificate Validation"
2274
+ owasp:
2275
+ - "A02:2021 - Cryptographic Failures"
2276
+ category: security
2277
+ subcategory: [vuln]
2278
+ technology: [nodejs, javascript, typescript]
2279
+ likelihood: MEDIUM
2280
+ impact: HIGH
2281
+ confidence: HIGH
2282
+ vulnerability_class:
2283
+ - Cryptographic Issues
2284
+ references:
2285
+ - https://cwe.mitre.org/data/definitions/295.html
2286
+ paths:
2287
+ exclude:
2288
+ - "**/test/**"
2289
+ - "**/tests/**"
2290
+ - "**/__tests__/**"
2291
+ - "**/*.test.js"
2292
+ - "**/*.test.ts"
2293
+ patterns:
2294
+ - pattern-either:
2295
+ - pattern: |
2296
+ {..., rejectUnauthorized: false, ...}
2297
+ - pattern: 'new https.Agent({..., rejectUnauthorized: false, ...})'
2298
+ - pattern: 'new tls.TLSSocket(..., {..., rejectUnauthorized: false, ...})'
2299
+
2300
+ - id: narvy.javascript.tls.node-tls-reject-unauthorized-env-disabled.node-tls-reject-unauthorized-env-disabled
2301
+ message: >-
2302
+ NODE_TLS_REJECT_UNAUTHORIZED is set to '0' at runtime. This
2303
+ disables certificate validation for EVERY TLS connection made by
2304
+ the Node process for as long as this remains set, not just one
2305
+ request (CWE-295). Remove this and fix the underlying certificate
2306
+ problem (add the CA to the trust store, or fix the hostname/cert
2307
+ mismatch) instead of disabling verification process-wide.
2308
+ severity: ERROR
2309
+ languages: [javascript, typescript]
2310
+ metadata:
2311
+ cwe:
2312
+ - "CWE-295: Improper Certificate Validation"
2313
+ owasp:
2314
+ - "A02:2021 - Cryptographic Failures"
2315
+ category: security
2316
+ subcategory: [vuln]
2317
+ technology: [nodejs, javascript, typescript]
2318
+ likelihood: MEDIUM
2319
+ impact: HIGH
2320
+ confidence: HIGH
2321
+ vulnerability_class:
2322
+ - Cryptographic Issues
2323
+ patterns:
2324
+ - pattern-either:
2325
+ - pattern: process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
2326
+ - pattern: process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
2327
+ - pattern: process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0
2328
+
2329
+ - id: narvy.javascript.tls.min-version-pinned-obsolete.min-version-pinned-obsolete
2330
+ message: >-
2331
+ TLS minVersion (or secureProtocol) is pinned to an obsolete
2332
+ protocol (SSLv3 or TLSv1/TLSv1.0/TLSv1.1). These protocol versions
2333
+ have known cryptographic weaknesses (POODLE, BEAST) and are
2334
+ disallowed by modern compliance baselines (CWE-327). Set minVersion
2335
+ to 'TLSv1.2' or 'TLSv1.3'.
2336
+ severity: WARNING
2337
+ languages: [javascript, typescript]
2338
+ metadata:
2339
+ cwe:
2340
+ - "CWE-327: Use of a Broken or Risky Cryptographic Algorithm"
2341
+ owasp:
2342
+ - "A02:2021 - Cryptographic Failures"
2343
+ category: security
2344
+ subcategory: [vuln]
2345
+ technology: [nodejs, javascript, typescript]
2346
+ likelihood: LOW
2347
+ impact: MEDIUM
2348
+ confidence: HIGH
2349
+ vulnerability_class:
2350
+ - Cryptographic Issues
2351
+ patterns:
2352
+ - pattern-either:
2353
+ - pattern: |
2354
+ {..., minVersion: "$V", ...}
2355
+ - pattern: |
2356
+ {..., secureProtocol: "$V", ...}
2357
+ - metavariable-regex:
2358
+ metavariable: $V
2359
+ regex: (?i)^(SSLv2|SSLv3|SSLv23_method|TLSv1|TLSv1_method|TLSv1\.0|TLSv1\.1|TLSv1_1_method)$
2360
+
2361
+ # XSS DOM
2362
+
2363
+ # Rule 1: innerHTML / outerHTML assignment
2364
+ - id: narvy.javascript.xss.dom-innerhtml-assign.dom-innerhtml-assign
2365
+ message: >-
2366
+ Assignment to `.innerHTML` / `.outerHTML` with a non-literal value.
2367
+ The browser parses this value as HTML, so if it contains
2368
+ attacker-controlled data this is DOM-based XSS (CWE-79). Use
2369
+ `.textContent` for plain text, or sanitize with DOMPurify before
2370
+ assigning.
2371
+ severity: ERROR
2372
+ languages: [javascript, typescript]
2373
+ metadata:
2374
+ cwe:
2375
+ - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
2376
+ owasp:
2377
+ - "A03:2021 - Injection"
2378
+ category: security
2379
+ subcategory: [vuln]
2380
+ technology: [javascript, typescript, dom]
2381
+ likelihood: MEDIUM
2382
+ impact: HIGH
2383
+ confidence: MEDIUM
2384
+ vulnerability_class:
2385
+ - Cross-Site-Scripting (XSS)
2386
+ patterns:
2387
+ - pattern-either:
2388
+ - pattern: $EL.innerHTML = $X
2389
+ - pattern: $EL.outerHTML = $X
2390
+ - pattern-not: $EL.innerHTML = "..."
2391
+ - pattern-not: $EL.outerHTML = "..."
2392
+ - pattern-not: $EL.innerHTML = `...`
2393
+ - pattern-not: $EL.outerHTML = `...`
2394
+ - pattern-not: $EL.innerHTML = ""
2395
+ - pattern-not: $EL.innerHTML = $SANITIZER.sanitize(...)
2396
+ - pattern-not: $EL.outerHTML = $SANITIZER.sanitize(...)
2397
+ # Exclude sanitizer-named calls and SCREAMING_SNAKE_CASE markup constants (naming heuristic, RE2-safe).
2398
+ - pattern-not:
2399
+ patterns:
2400
+ - pattern-either:
2401
+ - pattern: $EL.innerHTML = $SAN(...)
2402
+ - pattern: $EL.outerHTML = $SAN(...)
2403
+ - pattern: $EL.innerHTML = await $SAN(...)
2404
+ - pattern: $EL.outerHTML = await $SAN(...)
2405
+ - metavariable-regex:
2406
+ metavariable: $SAN
2407
+ regex: (?i).*(saniti[sz]|purif|dompurify|xss[_-]?clean|clean[_-]?html|html[_-]?clean|escape[_-]?html|html[_-]?escape|encode[_-]?html|html[_-]?encode|safe[_-]?html|html[_-]?safe).*
2408
+ # -- sanitizer-named method call (optionally awaited).
2409
+ - pattern-not:
2410
+ patterns:
2411
+ - pattern-either:
2412
+ - pattern: $EL.innerHTML = $O.$SAN(...)
2413
+ - pattern: $EL.outerHTML = $O.$SAN(...)
2414
+ - pattern: $EL.innerHTML = await $O.$SAN(...)
2415
+ - pattern: $EL.outerHTML = await $O.$SAN(...)
2416
+ - metavariable-regex:
2417
+ metavariable: $SAN
2418
+ regex: (?i).*(saniti[sz]|purif|xss[_-]?clean|clean[_-]?html|html[_-]?clean|escape[_-]?html|html[_-]?escape|encode[_-]?html|html[_-]?encode|safe[_-]?html|html[_-]?safe).*
2419
+ # -- module CONSTANT of static markup (SCREAMING_SNAKE_CASE identifier).
2420
+ - pattern-not:
2421
+ patterns:
2422
+ - pattern-either:
2423
+ - pattern: $EL.innerHTML = $CONST
2424
+ - pattern: $EL.outerHTML = $CONST
2425
+ - metavariable-regex:
2426
+ metavariable: $CONST
2427
+ regex: ^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$
2428
+ # Exclude static icon/SVG constants and no-input builders (icon/SVG naming) so real data flow still fires.
2429
+ - pattern-not:
2430
+ patterns:
2431
+ - pattern-either:
2432
+ - pattern: $EL.innerHTML = $ICONS.$ICO
2433
+ - pattern: $EL.outerHTML = $ICONS.$ICO
2434
+ - metavariable-regex:
2435
+ metavariable: $ICONS
2436
+ regex: ^(IC|ICO|ICON|ICONS|Icon|Icons|SVG|SVGS|Svg|Svgs|GLYPH|GLYPHS|Glyph|Glyphs)$
2437
+ # Static icon constant concatenated with a string literal:
2438
+ # `tag.innerHTML = IC.crown + ' Owner'` (either operand order).
2439
+ - pattern-not:
2440
+ patterns:
2441
+ - pattern-either:
2442
+ - pattern: $EL.innerHTML = $ICONS.$ICO + "..."
2443
+ - pattern: $EL.innerHTML = "..." + $ICONS.$ICO
2444
+ - pattern: $EL.outerHTML = $ICONS.$ICO + "..."
2445
+ - pattern: $EL.outerHTML = "..." + $ICONS.$ICO
2446
+ - metavariable-regex:
2447
+ metavariable: $ICONS
2448
+ regex: ^(IC|ICO|ICON|ICONS|Icon|Icons|SVG|SVGS|Svg|Svgs|GLYPH|GLYPHS|Glyph|Glyphs)$
2449
+ # Ternary between two icon constants of the SAME registry object:
2450
+ # `btn.innerHTML = muted ? IC.micOff : IC.mic`.
2451
+ - pattern-not:
2452
+ patterns:
2453
+ - pattern-either:
2454
+ - pattern: "$EL.innerHTML = $C ? $ICONS.$A : $ICONS.$B"
2455
+ - pattern: "$EL.outerHTML = $C ? $ICONS.$A : $ICONS.$B"
2456
+ - metavariable-regex:
2457
+ metavariable: $ICONS
2458
+ regex: ^(IC|ICO|ICON|ICONS|Icon|Icons|SVG|SVGS|Svg|Svgs|GLYPH|GLYPHS|Glyph|Glyphs)$
2459
+ # No-input static icon/SVG builder call, gated on the method name.
2460
+ - pattern-not:
2461
+ patterns:
2462
+ - pattern-either:
2463
+ - pattern: $EL.innerHTML = $O.$M(...)
2464
+ - pattern: $EL.outerHTML = $O.$M(...)
2465
+ - pattern: $EL.innerHTML = $M(...)
2466
+ - pattern: $EL.outerHTML = $M(...)
2467
+ - metavariable-regex:
2468
+ metavariable: $M
2469
+ regex: (?i).*(icon|svg|glyph|chevron|arrow|caret|spinner).*
2470
+
2471
+ # Rule 2: insertAdjacentHTML
2472
+ - id: narvy.javascript.xss.insert-adjacent-html.insert-adjacent-html
2473
+ message: >-
2474
+ `insertAdjacentHTML()` with a non-literal HTML fragment. Like
2475
+ `innerHTML`, this parses the string as HTML markup. If the fragment
2476
+ contains attacker-controlled data this is DOM-based XSS (CWE-79).
2477
+ Sanitize with DOMPurify first, or build the DOM with `createElement`
2478
+ / `textContent` instead.
2479
+ severity: ERROR
2480
+ languages: [javascript, typescript]
2481
+ metadata:
2482
+ cwe:
2483
+ - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
2484
+ owasp:
2485
+ - "A03:2021 - Injection"
2486
+ category: security
2487
+ subcategory: [vuln]
2488
+ technology: [javascript, typescript, dom]
2489
+ likelihood: MEDIUM
2490
+ impact: HIGH
2491
+ confidence: MEDIUM
2492
+ vulnerability_class:
2493
+ - Cross-Site-Scripting (XSS)
2494
+ patterns:
2495
+ - pattern: $EL.insertAdjacentHTML($POS, $X)
2496
+ - pattern-not: $EL.insertAdjacentHTML($POS, "...")
2497
+ - pattern-not: $EL.insertAdjacentHTML($POS, `...`)
2498
+ - pattern-not: $EL.insertAdjacentHTML($POS, $SANITIZER.sanitize(...))
2499
+
2500
+ # Rule 3: document.write / document.writeln
2501
+ - id: narvy.javascript.xss.document-write.document-write
2502
+ message: >-
2503
+ `document.write()` / `document.writeln()` with a non-literal argument.
2504
+ The string is parsed and injected directly into the page HTML stream.
2505
+ If it contains attacker-controlled data this is DOM-based XSS
2506
+ (CWE-79). Avoid `document.write`; use DOM APIs with `.textContent`,
2507
+ or sanitize the content first.
2508
+ severity: ERROR
2509
+ languages: [javascript, typescript]
2510
+ metadata:
2511
+ cwe:
2512
+ - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
2513
+ owasp:
2514
+ - "A03:2021 - Injection"
2515
+ category: security
2516
+ subcategory: [vuln]
2517
+ technology: [javascript, typescript, dom]
2518
+ likelihood: MEDIUM
2519
+ impact: HIGH
2520
+ confidence: MEDIUM
2521
+ vulnerability_class:
2522
+ - Cross-Site-Scripting (XSS)
2523
+ patterns:
2524
+ - pattern-either:
2525
+ - pattern: document.write($X)
2526
+ - pattern: document.writeln($X)
2527
+ - pattern-not: document.write("...")
2528
+ - pattern-not: document.writeln("...")
2529
+ - pattern-not: document.write(`...`)
2530
+ - pattern-not: document.writeln(`...`)
2531
+
2532
+ # Rule 4: jQuery .html()
2533
+ - id: narvy.javascript.xss.jquery-html.jquery-html
2534
+ message: >-
2535
+ jQuery `.html()` called with a non-literal argument. Unlike `.text()`,
2536
+ `.html()` parses its argument as markup. If the value is
2537
+ attacker-controlled this is DOM-based XSS (CWE-79). Use `.text()` for
2538
+ plain text, or sanitize with DOMPurify before passing to `.html()`.
2539
+ severity: WARNING
2540
+ languages: [javascript, typescript]
2541
+ metadata:
2542
+ cwe:
2543
+ - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
2544
+ owasp:
2545
+ - "A03:2021 - Injection"
2546
+ category: security
2547
+ subcategory: [vuln]
2548
+ technology: [javascript, typescript, jquery]
2549
+ likelihood: MEDIUM
2550
+ impact: MEDIUM
2551
+ confidence: MEDIUM
2552
+ vulnerability_class:
2553
+ - Cross-Site-Scripting (XSS)
2554
+ patterns:
2555
+ - pattern-either:
2556
+ - pattern: $(...).html($X)
2557
+ - pattern: $EL.html($X)
2558
+ - pattern-not: $(...).html("...")
2559
+ - pattern-not: $EL.html("...")
2560
+ - pattern-not: $(...).html()
2561
+ - pattern-not: $EL.html()
2562
+ - pattern-not: $EL.html($SANITIZER.sanitize(...))
2563
+ - metavariable-pattern:
2564
+ metavariable: $X
2565
+ patterns:
2566
+ - pattern-not: "..."
2567
+
2568
+ # Rule 5: React dangerouslySetInnerHTML without a recognized sanitizer
2569
+ - id: narvy.javascript.xss.react-dangerously-set-innerhtml.react-dangerously-set-innerhtml
2570
+ message: >-
2571
+ `dangerouslySetInnerHTML` is set to a value that does not come from a
2572
+ recognized sanitizer call (DOMPurify.sanitize / sanitizeHtml). React
2573
+ does not escape this value; it is injected as raw HTML. If it
2574
+ contains attacker-controlled data this is stored/reflected XSS
2575
+ (CWE-79). Wrap the value with `DOMPurify.sanitize(...)` before use.
2576
+ severity: ERROR
2577
+ languages: [javascript, typescript]
2578
+ metadata:
2579
+ cwe:
2580
+ - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
2581
+ owasp:
2582
+ - "A03:2021 - Injection"
2583
+ category: security
2584
+ subcategory: [vuln]
2585
+ technology: [javascript, typescript, react]
2586
+ likelihood: MEDIUM
2587
+ impact: HIGH
2588
+ confidence: MEDIUM
2589
+ vulnerability_class:
2590
+ - Cross-Site-Scripting (XSS)
2591
+ patterns:
2592
+ - pattern: '<$EL dangerouslySetInnerHTML={{__html: $X}} />'
2593
+ - pattern-not: '<$EL dangerouslySetInnerHTML={{__html: "..."}} />'
2594
+ - pattern-not: '<$EL dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(...)}} />'
2595
+ - pattern-not: '<$EL dangerouslySetInnerHTML={{__html: $ANY.sanitize(...)}} />'
2596
+ - pattern-not: '<$EL dangerouslySetInnerHTML={{__html: sanitizeHtml(...)}} />'
2597
+
2598
+ # XSS
2599
+
2600
+ # Reflected request value written into an HTML response (taint); focus $SINK, HTML-context regex; res.json/res.render excluded.
2601
+ - id: narvy.javascript.xss.reflected-request-value-in-html-response.reflected-request-value-in-html-response
2602
+ message: >-
2603
+ A request value (query/params/body/headers/cookies) reaches an HTML
2604
+ string passed to res.send/res.write/res.end with no escaping. Reflected
2605
+ Cross-Site Scripting (CWE-79). Escape the value for HTML context (a
2606
+ templating engine's auto-escaping, or a helper like `escape-html`)
2607
+ before writing it into the response body.
2608
+ severity: ERROR
2609
+ languages: [javascript, typescript]
2610
+ mode: taint
2611
+ metadata:
2612
+ cwe:
2613
+ - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
2614
+ owasp:
2615
+ - "A03:2021 - Injection"
2616
+ category: security
2617
+ subcategory: [vuln]
2618
+ technology: [express, nodejs, javascript, typescript]
2619
+ likelihood: MEDIUM
2620
+ impact: HIGH
2621
+ confidence: MEDIUM
2622
+ vulnerability_class:
2623
+ - Cross-Site-Scripting (XSS)
2624
+ references:
2625
+ - https://cwe.mitre.org/data/definitions/79.html
2626
+ pattern-sources:
2627
+ - patterns:
2628
+ - pattern-either:
2629
+ - pattern: $REQ.body
2630
+ - pattern: $REQ.query
2631
+ - pattern: $REQ.params
2632
+ - pattern: $REQ.cookies
2633
+ - metavariable-regex:
2634
+ metavariable: $REQ
2635
+ regex: ^(req|request|_req|ctx|httpRequest|httpReq)$
2636
+ - patterns:
2637
+ - pattern: $REQ.$KIND.$X
2638
+ - metavariable-regex:
2639
+ metavariable: $KIND
2640
+ regex: ^(body|params|query|headers|cookies)$
2641
+ pattern-sanitizers:
2642
+ - pattern: escapeHtml(...)
2643
+ - pattern: escapeHTML(...)
2644
+ - pattern: escape(...)
2645
+ - pattern: $M.escapeHtml(...)
2646
+ - pattern: $M.escapeHTML(...)
2647
+ - pattern: $M.escape(...)
2648
+ - pattern: sanitizeHtml(...)
2649
+ - pattern: $M.sanitize(...)
2650
+ - pattern: $M.sanitizeHtml(...)
2651
+ - pattern: encodeURIComponent(...)
2652
+ - pattern: encodeURI(...)
2653
+ - pattern: parseInt(...)
2654
+ - pattern: Number(...)
2655
+ # JSON.stringify output is used for embedding data, not raw markup.
2656
+ - pattern: JSON.stringify(...)
2657
+ pattern-sinks:
2658
+ - patterns:
2659
+ - pattern-either:
2660
+ - pattern: $RES.send($SINK, ...)
2661
+ - pattern: $RES.write($SINK, ...)
2662
+ - pattern: $RES.end($SINK, ...)
2663
+ - focus-metavariable: $SINK
2664
+ - metavariable-regex:
2665
+ metavariable: $SINK
2666
+ regex: '(?s).*<\s*[a-zA-Z!/].*'
2667
+ paths:
2668
+ exclude:
2669
+ - "**/test/**"
2670
+ - "**/tests/**"
2671
+ - "**/__tests__/**"
2672
+ - "**/*.test.js"
2673
+ - "**/*.test.ts"
2674
+
2675
+ # Rule 6b: reflected value built into a variable then written to response; requires request value + HTML markup in one RHS.
2676
+ - id: narvy.javascript.xss.reflected-request-value-in-html-response-var-indirect.reflected-request-value-in-html-response-var-indirect
2677
+ message: >-
2678
+ A request value (query/params/body/headers/cookies) is concatenated /
2679
+ interpolated into an HTML string held in a variable and then written
2680
+ to the response via res.send/res.write/res.end. Reflected Cross-Site
2681
+ Scripting (CWE-79), with one level of variable indirection. Escape the
2682
+ value for the HTML context before writing it into the response.
2683
+ severity: ERROR
2684
+ languages: [javascript, typescript]
2685
+ metadata:
2686
+ cwe:
2687
+ - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
2688
+ owasp:
2689
+ - "A03:2021 - Injection"
2690
+ category: security
2691
+ subcategory: [vuln]
2692
+ technology: [express, nodejs, javascript, typescript]
2693
+ likelihood: MEDIUM
2694
+ impact: HIGH
2695
+ confidence: MEDIUM
2696
+ vulnerability_class:
2697
+ - Cross-Site-Scripting (XSS)
2698
+ references:
2699
+ - https://cwe.mitre.org/data/definitions/79.html
2700
+ patterns:
2701
+ - pattern-either:
2702
+ # concatenation carrying request data + an HTML-markup string
2703
+ - patterns:
2704
+ - pattern: |
2705
+ $H = <... $REQ.$KIND.$X ...>
2706
+ ...
2707
+ $RES.$M($H, ...)
2708
+ - pattern: |
2709
+ $H = <... "$MARKUP" ...>
2710
+ ...
2711
+ $RES.$M($H, ...)
2712
+ - metavariable-regex:
2713
+ metavariable: $MARKUP
2714
+ regex: '(?s).*<\s*[a-zA-Z!/].*'
2715
+ # template literal carrying request data, markup in its text
2716
+ - patterns:
2717
+ - pattern: |
2718
+ $H = `...${$REQ.$KIND.$X}...`
2719
+ ...
2720
+ $RES.$M($H, ...)
2721
+ - pattern: |
2722
+ $H = $TMPL
2723
+ ...
2724
+ $RES.$M($H, ...)
2725
+ - metavariable-regex:
2726
+ metavariable: $TMPL
2727
+ regex: '(?s).*<\s*[a-zA-Z!/].*'
2728
+ - metavariable-regex:
2729
+ metavariable: $KIND
2730
+ regex: ^(body|params|query|headers|cookies)$
2731
+ - metavariable-regex:
2732
+ metavariable: $REQ
2733
+ regex: ^(req|request|_req|ctx|httpRequest|httpReq)$
2734
+ - metavariable-regex:
2735
+ metavariable: $M
2736
+ regex: ^(send|write|end)$
2737
+ paths:
2738
+ exclude:
2739
+ - "**/test/**"
2740
+ - "**/tests/**"
2741
+ - "**/__tests__/**"
2742
+ - "**/*.test.js"
2743
+ - "**/*.test.ts"
2744
+
2745
+ # Rule 7: Angular DomSanitizer.
2746
+ # bypassSecurityTrust*() with a non-literal argument.
2747
+ - id: narvy.javascript.xss.angular-bypass-security-trust.angular-bypass-security-trust
2748
+ message: >-
2749
+ Angular's DomSanitizer.bypassSecurityTrust*() is called with a
2750
+ non-literal value. These methods exist specifically to turn OFF
2751
+ Angular's built-in sanitization for the value they wrap - marking
2752
+ attacker-influenced data as "trusted" reintroduces DOM-based XSS
2753
+ that Angular would otherwise have blocked (CWE-79). Sanitize the
2754
+ value yourself first (or better, avoid needing to bypass
2755
+ sanitization at all) before wrapping it.
2756
+ severity: ERROR
2757
+ languages: [javascript, typescript]
2758
+ metadata:
2759
+ cwe:
2760
+ - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
2761
+ owasp:
2762
+ - "A03:2021 - Injection"
2763
+ category: security
2764
+ subcategory: [vuln]
2765
+ technology: [angular, javascript, typescript]
2766
+ likelihood: MEDIUM
2767
+ impact: HIGH
2768
+ confidence: MEDIUM
2769
+ vulnerability_class:
2770
+ - Cross-Site-Scripting (XSS)
2771
+ patterns:
2772
+ - pattern-either:
2773
+ - pattern: $SANITIZER.bypassSecurityTrustHtml($X)
2774
+ - pattern: $SANITIZER.bypassSecurityTrustUrl($X)
2775
+ - pattern: $SANITIZER.bypassSecurityTrustResourceUrl($X)
2776
+ - pattern: $SANITIZER.bypassSecurityTrustScript($X)
2777
+ - pattern: $SANITIZER.bypassSecurityTrustStyle($X)
2778
+ - pattern-not: $SANITIZER.bypassSecurityTrustHtml("...")
2779
+ - pattern-not: $SANITIZER.bypassSecurityTrustUrl("...")
2780
+ - pattern-not: $SANITIZER.bypassSecurityTrustResourceUrl("...")
2781
+ - pattern-not: $SANITIZER.bypassSecurityTrustScript("...")
2782
+ - pattern-not: $SANITIZER.bypassSecurityTrustStyle("...")
2783
+ - pattern-not: $SANITIZER.bypassSecurityTrustHtml(`...`)
2784
+ - pattern-not: $SANITIZER.bypassSecurityTrustUrl(`...`)
2785
+ - pattern-not: $SANITIZER.bypassSecurityTrustResourceUrl(`...`)
2786
+ - pattern-not: $SANITIZER.bypassSecurityTrustScript(`...`)
2787
+ - pattern-not: $SANITIZER.bypassSecurityTrustStyle(`...`)
2788
+
2789
+ # XXE
2790
+
2791
+ - id: narvy.javascript.xxe.libxmljs-noent-enabled.libxmljs-noent-enabled
2792
+ message: >-
2793
+ libxmljs is called with `noent: true`, which turns on external
2794
+ entity substitution while parsing XML. If the XML document being
2795
+ parsed can be influenced by an attacker, this allows XML External
2796
+ Entity injection (CWE-611): reading local files via a crafted
2797
+ `<!ENTITY>` declaration, or triggering out-of-band requests from
2798
+ the server. Remove `noent: true` unless entity substitution is
2799
+ required, and if it is, first strip any `<!DOCTYPE` declaration
2800
+ from untrusted input before parsing.
2801
+ severity: ERROR
2802
+ languages: [javascript, typescript]
2803
+ metadata:
2804
+ cwe:
2805
+ - "CWE-611: Improper Restriction of XML External Entity Reference"
2806
+ owasp:
2807
+ - "A05:2021 - Security Misconfiguration"
2808
+ - "A04:2017 - XML External Entities (XXE)"
2809
+ category: security
2810
+ subcategory: [vuln]
2811
+ technology: [libxmljs, javascript, typescript]
2812
+ likelihood: LOW
2813
+ impact: HIGH
2814
+ confidence: HIGH
2815
+ vulnerability_class:
2816
+ - XML Injection
2817
+ references:
2818
+ - https://cwe.mitre.org/data/definitions/611.html
2819
+ - https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing
2820
+ patterns:
2821
+ - pattern-either:
2822
+ - pattern: '$LIBXML.parseXml($DATA, {..., noent: true, ...})'
2823
+ - pattern: '$LIBXML.parseXmlString($DATA, {..., noent: true, ...})'
2824
+
2825
+ - id: narvy.javascript.xxe.libxmljs-dtd-load-enabled.libxmljs-dtd-load-enabled
2826
+ message: >-
2827
+ libxmljs is called with `dtdload: true` or `dtdvalid: true`, which
2828
+ allows the parser to fetch an external DTD referenced in the
2829
+ document. If the XML being parsed is attacker-controlled this
2830
+ enables blind/out-of-band XXE (CWE-611): the server will make a
2831
+ network or file request to a URL the attacker put in the DOCTYPE.
2832
+ Remove dtdload/dtdvalid for untrusted input, or restrict outbound
2833
+ network access from the parsing process.
2834
+ severity: ERROR
2835
+ languages: [javascript, typescript]
2836
+ metadata:
2837
+ cwe:
2838
+ - "CWE-611: Improper Restriction of XML External Entity Reference"
2839
+ owasp:
2840
+ - "A05:2021 - Security Misconfiguration"
2841
+ category: security
2842
+ subcategory: [vuln]
2843
+ technology: [libxmljs, javascript, typescript]
2844
+ likelihood: LOW
2845
+ impact: HIGH
2846
+ confidence: MEDIUM
2847
+ vulnerability_class:
2848
+ - XML Injection
2849
+ patterns:
2850
+ - pattern-either:
2851
+ - pattern: '$LIBXML.parseXml($DATA, {..., dtdload: true, ...})'
2852
+ - pattern: '$LIBXML.parseXmlString($DATA, {..., dtdload: true, ...})'
2853
+ - pattern: '$LIBXML.parseXml($DATA, {..., dtdvalid: true, ...})'
2854
+ - pattern: '$LIBXML.parseXmlString($DATA, {..., dtdvalid: true, ...})'
2855
+
2856
+ # COMMAND INJECTION
2857
+
2858
+ # 1. exec / execSync with request data concatenated into the command.
2859
+ - id: narvy.javascript.cmdi.child-process-exec-concat.child-process-exec-concat
2860
+ message: >-
2861
+ A request-derived value is concatenated into the command string
2862
+ passed to child_process.exec/execSync. exec runs the string through
2863
+ a shell, so shell metacharacters in the request (`;`, `|`, `&&`,
2864
+ backticks) let an attacker run arbitrary OS commands (CWE-78). Use
2865
+ execFile/execFileSync with an argument array instead of a shell
2866
+ string, or strictly allow-list the input.
2867
+ severity: ERROR
2868
+ languages: [javascript, typescript]
2869
+ metadata:
2870
+ cwe:
2871
+ - "CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')"
2872
+ owasp:
2873
+ - "A03:2021 - Injection"
2874
+ category: security
2875
+ subcategory: [vuln]
2876
+ technology: [nodejs, javascript, typescript]
2877
+ likelihood: HIGH
2878
+ impact: HIGH
2879
+ confidence: HIGH
2880
+ vulnerability_class:
2881
+ - Command Injection
2882
+ references:
2883
+ - https://cwe.mitre.org/data/definitions/78.html
2884
+ patterns:
2885
+ - pattern-either:
2886
+ - patterns:
2887
+ - pattern-either:
2888
+ - pattern: $CP.exec("$CMD" + $REQ.$KIND.$X + ..., ...)
2889
+ - pattern: $CP.exec(`...${$REQ.$KIND.$X}...`, ...)
2890
+ - pattern: $CP.execSync("$CMD" + $REQ.$KIND.$X + ...)
2891
+ - pattern: $CP.execSync(`...${$REQ.$KIND.$X}...`)
2892
+ - pattern: |
2893
+ $V = $REQ.$KIND.$X
2894
+ ...
2895
+ $CP.exec("$CMD" + $V + ..., ...)
2896
+ - pattern: |
2897
+ $V = $REQ.$KIND.$X
2898
+ ...
2899
+ $CP.exec(`...${$V}...`, ...)
2900
+ - pattern: |
2901
+ $V = $REQ.$KIND.$X
2902
+ ...
2903
+ $CP.execSync("$CMD" + $V + ...)
2904
+ - pattern: |
2905
+ $V = $REQ.$KIND.$X
2906
+ ...
2907
+ $CP.execSync(`...${$V}...`)
2908
+ # Destructured `const { exec } = require('child_process')` then bare exec(), gated on the taint shape.
2909
+ - pattern: exec("$CMD" + $REQ.$KIND.$X, ...)
2910
+ - pattern: exec("$CMD" + $REQ.$KIND.$X + ..., ...)
2911
+ - pattern: exec(`...${$REQ.$KIND.$X}...`, ...)
2912
+ # Taint last with nothing after it; covers receiver, destructured execSync, one level of indirection.
2913
+ - pattern: $CP.exec("$CMD" + $REQ.$KIND.$X, ...)
2914
+ - pattern: $CP.execSync("$CMD" + $REQ.$KIND.$X)
2915
+ - pattern: execSync("$CMD" + $REQ.$KIND.$X)
2916
+ - pattern: |
2917
+ $V = $REQ.$KIND.$X
2918
+ ...
2919
+ $CP.exec("$CMD" + $V, ...)
2920
+ - pattern: |
2921
+ $V = $REQ.$KIND.$X
2922
+ ...
2923
+ $CP.execSync("$CMD" + $V)
2924
+ - pattern: |
2925
+ $V = $REQ.$KIND.$X
2926
+ ...
2927
+ exec("$CMD" + $V, ...)
2928
+ - pattern: |
2929
+ $V = $REQ.$KIND.$X
2930
+ ...
2931
+ execSync("$CMD" + $V)
2932
+ - metavariable-regex:
2933
+ metavariable: $KIND
2934
+ regex: ^(query|params|body|headers|cookies)$
2935
+ # AWS Lambda taint source: event.queryStringParameters/.pathParameters/.headers into the command string.
2936
+ - patterns:
2937
+ - pattern-either:
2938
+ - pattern: $CP.exec("$CMD" + $EVENT.$EKIND.$X + ..., ...)
2939
+ - pattern: $CP.exec(`...${$EVENT.$EKIND.$X}...`, ...)
2940
+ - pattern: $CP.execSync("$CMD" + $EVENT.$EKIND.$X + ...)
2941
+ - pattern: $CP.execSync(`...${$EVENT.$EKIND.$X}...`)
2942
+ - pattern: |
2943
+ $V = $EVENT.$EKIND.$X
2944
+ ...
2945
+ $CP.exec("$CMD" + $V + ..., ...)
2946
+ - pattern: |
2947
+ $V = $EVENT.$EKIND.$X
2948
+ ...
2949
+ $CP.execSync("$CMD" + $V + ...)
2950
+ - pattern: exec("$CMD" + $EVENT.$EKIND.$X, ...)
2951
+ - pattern: exec("$CMD" + $EVENT.$EKIND.$X + ..., ...)
2952
+ - pattern: exec(`...${$EVENT.$EKIND.$X}...`, ...)
2953
+ - metavariable-regex:
2954
+ metavariable: $EKIND
2955
+ regex: ^(queryStringParameters|pathParameters|headers|multiValueQueryStringParameters|multiValueHeaders)$
2956
+ # event.body is flat, own branch with no $EKIND regex.
2957
+ - pattern-either:
2958
+ - pattern: $CP.exec("$CMD" + $EVENT.body + ..., ...)
2959
+ - pattern: $CP.exec("$CMD" + JSON.parse($EVENT.body).$X + ..., ...)
2960
+
2961
+ # 2. spawn/execFile with shell: true and a tainted command/argument.
2962
+ - id: narvy.javascript.cmdi.spawn-shell-true-tainted.spawn-shell-true-tainted
2963
+ message: >-
2964
+ child_process.spawn/execFile is called with `shell: true` and a
2965
+ command or argument derived from the request. With shell: true the
2966
+ command line is interpreted by a shell, so request-controlled
2967
+ shell metacharacters allow OS command injection (CWE-78). Drop
2968
+ shell: true and pass the command and arguments as a plain array,
2969
+ or allow-list the accepted values.
2970
+ severity: ERROR
2971
+ languages: [javascript, typescript]
2972
+ mode: taint
2973
+ metadata:
2974
+ cwe:
2975
+ - "CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')"
2976
+ owasp:
2977
+ - "A03:2021 - Injection"
2978
+ category: security
2979
+ subcategory: [vuln]
2980
+ technology: [nodejs, javascript, typescript]
2981
+ likelihood: HIGH
2982
+ impact: HIGH
2983
+ confidence: MEDIUM
2984
+ vulnerability_class:
2985
+ - Command Injection
2986
+ pattern-sources:
2987
+ - pattern: req.query.$Q
2988
+ - pattern: req.params.$P
2989
+ - pattern: req.body.$B
2990
+ # AWS Lambda taint source, additional to the Express req source above.
2991
+ - pattern: event.queryStringParameters.$Q
2992
+ - pattern: event.pathParameters.$P
2993
+ - pattern: event.body
2994
+ - pattern: event.headers.$H
2995
+ pattern-sinks:
2996
+ - patterns:
2997
+ - pattern-either:
2998
+ - pattern: '$CP.spawn($SINK, ..., {..., shell: true, ...})'
2999
+ - pattern: '$CP.spawn($CMD, [..., $SINK, ...], {..., shell: true, ...})'
3000
+ - pattern: '$CP.execFile($SINK, ..., {..., shell: true, ...})'
3001
+
3002
+ # 3. spawn/spawnSync with shell: true, untainted broad companion to the tainted rule.
3003
+ - id: narvy.javascript.lang.security.spawn-shell-true.spawn-shell-true
3004
+ message: >-
3005
+ spawn / spawnSync called with `{shell: true}` runs the command
3006
+ through a shell (sh / cmd.exe), enabling command injection if any
3007
+ of the arguments are user-controlled. Drop `shell: true` and pass
3008
+ arguments as an array - execve() bypasses the shell entirely.
3009
+ severity: ERROR
3010
+ languages: [javascript, typescript]
3011
+ metadata:
3012
+ cwe:
3013
+ - "CWE-78: OS Command Injection"
3014
+ owasp: ["A03:2021 - Injection"]
3015
+ category: security
3016
+ subcategory: [vuln]
3017
+ technology: [javascript, typescript]
3018
+ likelihood: HIGH
3019
+ impact: HIGH
3020
+ confidence: HIGH
3021
+ vulnerability_class:
3022
+ - Command Injection
3023
+ patterns:
3024
+ - pattern-either:
3025
+ - pattern: |
3026
+ $CP.spawn($X, $Y, {..., shell: true, ...})
3027
+ - pattern: |
3028
+ $CP.spawnSync($X, $Y, {..., shell: true, ...})
3029
+ - pattern: |
3030
+ spawn($X, $Y, {..., shell: true, ...})
3031
+ - pattern: |
3032
+ spawnSync($X, $Y, {..., shell: true, ...})
3033
+
3034
+ # Dynamic code execution (eval / new Function / vm module / setTimeout with
3035
+ # a string) is covered separately in code_injection.yml in this same pack.
3036
+
3037
+ # CODE INJECTION
3038
+
3039
+ # Rule 1: eval() with non-literal argument
3040
+ - id: narvy.javascript.code-injection.eval-non-literal.eval-non-literal
3041
+ message: >-
3042
+ `eval()` called with a non-literal argument. eval() executes its
3043
+ argument as JavaScript with full privileges of the calling context.
3044
+ If the value is attacker-influenced this is remote code execution
3045
+ (CWE-94). Remove eval() and use JSON.parse for data, or a safe
3046
+ dispatch table for logic selection.
3047
+ severity: ERROR
3048
+ languages: [javascript, typescript]
3049
+ metadata:
3050
+ cwe:
3051
+ - "CWE-94: Improper Control of Generation of Code ('Code Injection')"
3052
+ owasp:
3053
+ - "A03:2021 - Injection"
3054
+ category: security
3055
+ subcategory: [vuln]
3056
+ technology: [javascript, typescript]
3057
+ likelihood: MEDIUM
3058
+ impact: HIGH
3059
+ confidence: MEDIUM
3060
+ vulnerability_class:
3061
+ - Code Injection
3062
+ patterns:
3063
+ - pattern: eval($X)
3064
+ - pattern-not: eval("...")
3065
+ - pattern-not: eval(`...`)
3066
+
3067
+ # Rule 2: new Function(...) with non-literal body
3068
+ - id: narvy.javascript.code-injection.dynamic-function-constructor.dynamic-function-constructor
3069
+ message: >-
3070
+ `new Function(...)` constructs and compiles a function from string
3071
+ arguments at runtime - functionally equivalent to eval(). If any
3072
+ argument (especially the final one, the function body) is
3073
+ attacker-influenced this is remote code execution (CWE-94). Avoid
3074
+ the Function constructor with dynamic input.
3075
+ severity: ERROR
3076
+ languages: [javascript, typescript]
3077
+ metadata:
3078
+ cwe:
3079
+ - "CWE-94: Improper Control of Generation of Code ('Code Injection')"
3080
+ owasp:
3081
+ - "A03:2021 - Injection"
3082
+ category: security
3083
+ subcategory: [vuln]
3084
+ technology: [javascript, typescript]
3085
+ likelihood: LOW
3086
+ impact: HIGH
3087
+ confidence: MEDIUM
3088
+ vulnerability_class:
3089
+ - Code Injection
3090
+ patterns:
3091
+ - pattern: new Function(...)
3092
+ - pattern-not: new Function("...")
3093
+ - pattern-not: new Function()
3094
+
3095
+ # Rule 3: vm module running non-literal script string
3096
+ - id: narvy.javascript.code-injection.vm-run-non-literal.vm-run-non-literal
3097
+ message: >-
3098
+ Node `vm` module executes a non-literal script string
3099
+ (runInNewContext / runInContext / runInThisContext). Although `vm`
3100
+ is sandboxed for global scope, it is not a real security boundary
3101
+ against a determined attacker and does not restrict CPU/memory. If
3102
+ the script text is attacker-influenced this is code execution
3103
+ (CWE-94). Never pass untrusted input to `vm.run*`.
3104
+ severity: ERROR
3105
+ languages: [javascript, typescript]
3106
+ metadata:
3107
+ cwe:
3108
+ - "CWE-94: Improper Control of Generation of Code ('Code Injection')"
3109
+ owasp:
3110
+ - "A03:2021 - Injection"
3111
+ category: security
3112
+ subcategory: [vuln]
3113
+ technology: [javascript, typescript, node-vm]
3114
+ likelihood: MEDIUM
3115
+ impact: HIGH
3116
+ confidence: HIGH
3117
+ vulnerability_class:
3118
+ - Code Injection
3119
+ patterns:
3120
+ - pattern-either:
3121
+ - pattern: vm.runInNewContext($X, ...)
3122
+ - pattern: vm.runInContext($X, ...)
3123
+ - pattern: vm.runInThisContext($X, ...)
3124
+ - pattern: $VM.runInNewContext($X, ...)
3125
+ - pattern-not: vm.runInNewContext("...", ...)
3126
+ - pattern-not: vm.runInContext("...", ...)
3127
+ - pattern-not: vm.runInThisContext("...", ...)
3128
+
3129
+ # Rule 4: setTimeout/setInterval with a string (implicit eval)
3130
+ - id: narvy.javascript.code-injection.settimeout-string-arg.settimeout-string-arg
3131
+ message: >-
3132
+ `setTimeout` / `setInterval` called with a STRING as the first
3133
+ argument instead of a function reference. The engine compiles and
3134
+ runs the string as code (implicit eval) when the timer fires. If the
3135
+ string is built from attacker-influenced data this is code injection
3136
+ (CWE-94). Pass a function/arrow-function reference instead.
3137
+ severity: WARNING
3138
+ languages: [javascript, typescript]
3139
+ metadata:
3140
+ cwe:
3141
+ - "CWE-94: Improper Control of Generation of Code ('Code Injection')"
3142
+ owasp:
3143
+ - "A03:2021 - Injection"
3144
+ category: security
3145
+ subcategory: [vuln]
3146
+ technology: [javascript, typescript]
3147
+ likelihood: LOW
3148
+ impact: MEDIUM
3149
+ confidence: MEDIUM
3150
+ vulnerability_class:
3151
+ - Code Injection
3152
+ patterns:
3153
+ - pattern-either:
3154
+ - pattern: setTimeout($X + ..., $MS)
3155
+ - pattern: setTimeout(`...${$Y}...`, $MS)
3156
+ - pattern: setInterval($X + ..., $MS)
3157
+ - pattern: setInterval(`...${$Y}...`, $MS)
3158
+
3159
+ # ADDITIONAL COVERAGE: JWT verification, token storage, headless-browser
3160
+ # SSRF, javascript: URIs, jQuery markup sinks, unsafe deserialization, ReDoS.
3161
+
3162
+ # jwt.decode() where jwt.verify() belongs: fires only when the token is request-derived and no .verify() appears in the enclosing function.
3163
+ - id: narvy.javascript.jwt.decode-without-verify.decode-without-verify
3164
+ message: >-
3165
+ A request-supplied JWT is parsed with `jwt.decode()` and no
3166
+ `jwt.verify()` runs in the same function. `decode` does NOT check the
3167
+ signature: an attacker can forge any claim (user id, role, expiry) by
3168
+ editing the token payload. Authentication bypass / privilege
3169
+ escalation (CWE-347). Use `jwt.verify(token, key, {algorithms: [...]})`
3170
+ and read the claims from its return value.
3171
+ severity: ERROR
3172
+ languages: [javascript, typescript]
3173
+ mode: taint
3174
+ metadata:
3175
+ cwe:
3176
+ - "CWE-347: Improper Verification of Cryptographic Signature"
3177
+ - "CWE-287: Improper Authentication"
3178
+ owasp:
3179
+ - "A07:2021 - Identification and Authentication Failures"
3180
+ - "A02:2021 - Cryptographic Failures"
3181
+ category: security
3182
+ subcategory: [vuln]
3183
+ technology: [jsonwebtoken, jwt, express, nodejs, javascript, typescript]
3184
+ likelihood: HIGH
3185
+ impact: HIGH
3186
+ confidence: HIGH
3187
+ vulnerability_class:
3188
+ - Improper Authentication
3189
+ references:
3190
+ - https://cwe.mitre.org/data/definitions/347.html
3191
+ pattern-sources:
3192
+ - patterns:
3193
+ - pattern-either:
3194
+ - pattern: $REQ.headers
3195
+ - pattern: $REQ.cookies
3196
+ - pattern: $REQ.body
3197
+ - pattern: $REQ.query
3198
+ - pattern: $REQ.get("...")
3199
+ - pattern: $REQ.header("...")
3200
+ - metavariable-regex:
3201
+ metavariable: $REQ
3202
+ regex: ^(req|request|_req|ctx|httpRequest|httpReq)$
3203
+ - patterns:
3204
+ - pattern: $REQ.$KIND.$X
3205
+ - metavariable-regex:
3206
+ metavariable: $KIND
3207
+ regex: ^(body|params|query|headers|cookies)$
3208
+ pattern-sinks:
3209
+ - patterns:
3210
+ - pattern: $JWT.decode($SINK, ...)
3211
+ - focus-metavariable: $SINK
3212
+ - metavariable-regex:
3213
+ metavariable: $JWT
3214
+ regex: ^(jwt|jsonwebtoken|jsonWebToken|JWT|jwtLib)$
3215
+ # A .verify() anywhere in the function clears the finding; free $V receiver also matches wrappers.
3216
+ - pattern-not-inside: |
3217
+ function $F(...) {
3218
+ ...
3219
+ $V.verify(...)
3220
+ ...
3221
+ }
3222
+ - pattern-not-inside: |
3223
+ (...) => {
3224
+ ...
3225
+ $V.verify(...)
3226
+ ...
3227
+ }
3228
+ - pattern-not-inside: |
3229
+ async (...) => {
3230
+ ...
3231
+ $V.verify(...)
3232
+ ...
3233
+ }
3234
+ - pattern-not-inside: |
3235
+ $F(...) {
3236
+ ...
3237
+ $V.verify(...)
3238
+ ...
3239
+ }
3240
+ - pattern-not-inside: |
3241
+ try {
3242
+ ...
3243
+ $V.verify(...)
3244
+ ...
3245
+ } catch($E) {
3246
+ ...
3247
+ }
3248
+ paths:
3249
+ exclude:
3250
+ - "**/test/**"
3251
+ - "**/tests/**"
3252
+ - "**/__tests__/**"
3253
+ - "**/*.test.js"
3254
+ - "**/*.test.ts"
3255
+ - "**/*.spec.js"
3256
+ - "**/*.spec.ts"
3257
+
3258
+ # Bearer/refresh token in localStorage: readable by any script, no expiry/HttpOnly/SameSite; key-name allow-list separates credentials from UI state.
3259
+ - id: narvy.javascript.jwt.token-in-localstorage.token-in-localstorage
3260
+ message: >-
3261
+ An authentication token is written to `localStorage`. Anything stored
3262
+ there is readable by every script on the origin, survives the browser
3263
+ session, and cannot be protected with HttpOnly/SameSite - so a single
3264
+ XSS anywhere on the site steals a long-lived credential (CWE-522).
3265
+ Prefer an HttpOnly + Secure + SameSite cookie, or keep the access
3266
+ token in memory only.
3267
+ severity: WARNING
3268
+ languages: [javascript, typescript]
3269
+ metadata:
3270
+ cwe:
3271
+ - "CWE-522: Insufficiently Protected Credentials"
3272
+ - "CWE-1004: Sensitive Cookie Without 'HttpOnly' Flag"
3273
+ owasp:
3274
+ - "A07:2021 - Identification and Authentication Failures"
3275
+ category: security
3276
+ subcategory: [audit]
3277
+ technology: [browser, jwt, javascript, typescript]
3278
+ likelihood: MEDIUM
3279
+ impact: HIGH
3280
+ confidence: HIGH
3281
+ vulnerability_class:
3282
+ - Insecure Storage
3283
+ patterns:
3284
+ - pattern-either:
3285
+ - pattern: localStorage.setItem("$KEY", $V)
3286
+ - pattern: window.localStorage.setItem("$KEY", $V)
3287
+ - pattern: $S.localStorage.setItem("$KEY", $V)
3288
+ - pattern: localStorage["$KEY"] = $V
3289
+ - pattern: window.localStorage["$KEY"] = $V
3290
+ - metavariable-regex:
3291
+ metavariable: $KEY
3292
+ regex: '(?i)^[a-z0-9_.-]*(jwt|access[_-]?token|id[_-]?token|refresh[_-]?token|bearer|auth[_-]?token|session[_-]?token|api[_-]?key)[a-z0-9_.-]*$'
3293
+ paths:
3294
+ exclude:
3295
+ - "**/test/**"
3296
+ - "**/tests/**"
3297
+ - "**/__tests__/**"
3298
+ - "**/*.test.js"
3299
+ - "**/*.test.ts"
3300
+ - "**/*.test.tsx"
3301
+ - "**/*.spec.js"
3302
+ - "**/*.spec.ts"
3303
+ - "**/*.spec.tsx"
3304
+
3305
+ # Authorization decision on a client-side decoded (unverified) JWT claim; fires only when the claim drives a conditional and is authz-shaped.
3306
+ - id: narvy.javascript.jwt.unverified-claim-authorization.unverified-claim-authorization
3307
+ message: >-
3308
+ An authorization decision is made on a claim read from a
3309
+ client-side-DECODED JWT. `jwt-decode` / `decodeJwt` do not verify the
3310
+ signature, so the user can edit the claim in their own token and take
3311
+ the privileged branch (CWE-347). Client-side decoding is fine for
3312
+ display, but every authorization check must happen server-side on a
3313
+ signature-verified token.
3314
+ severity: ERROR
3315
+ languages: [javascript, typescript]
3316
+ metadata:
3317
+ cwe:
3318
+ - "CWE-347: Improper Verification of Cryptographic Signature"
3319
+ - "CWE-602: Client-Side Enforcement of Server-Side Security"
3320
+ owasp:
3321
+ - "A01:2021 - Broken Access Control"
3322
+ category: security
3323
+ subcategory: [vuln]
3324
+ technology: [jwt-decode, jose, browser, javascript, typescript]
3325
+ likelihood: MEDIUM
3326
+ impact: HIGH
3327
+ confidence: MEDIUM
3328
+ vulnerability_class:
3329
+ - Improper Authorization
3330
+ patterns:
3331
+ - pattern-either:
3332
+ - pattern: if (<... $DEC(...).$CLAIM ...>) { ... }
3333
+ - pattern: |
3334
+ $C = $DEC(...);
3335
+ ...
3336
+ if (<... $C.$CLAIM ...>) { ... }
3337
+ - metavariable-regex:
3338
+ metavariable: $DEC
3339
+ regex: ^(jwtDecode|jwt_decode|jwtDecoder|decodeJwt|decodeToken|parseJwt)$
3340
+ - metavariable-regex:
3341
+ metavariable: $CLAIM
3342
+ regex: ^(role|roles|isAdmin|is_admin|admin|scope|scopes|permissions|perms|privileges|isStaff|is_staff|groups)$
3343
+
3344
+ # SSRF via headless browser .goto()/.setContent(): the server fetches the URL; sanitizers mirror request-url-from-user.
3345
+ - id: narvy.javascript.ssrf.headless-browser-tainted-url.headless-browser-tainted-url
3346
+ message: >-
3347
+ A request-derived value reaches a headless-browser navigation sink
3348
+ (puppeteer / playwright `page.goto` or `page.setContent`). The SERVER's
3349
+ browser fetches it, so an attacker can reach cloud metadata endpoints,
3350
+ localhost admin ports and other internal services - Server-Side
3351
+ Request Forgery (CWE-918). Validate the URL against an allow-list of
3352
+ hosts and schemes, and run the browser with egress restricted.
3353
+ severity: ERROR
3354
+ languages: [javascript, typescript]
3355
+ mode: taint
3356
+ metadata:
3357
+ cwe:
3358
+ - "CWE-918: Server-Side Request Forgery (SSRF)"
3359
+ owasp:
3360
+ - "A10:2021 - Server-Side Request Forgery (SSRF)"
3361
+ category: security
3362
+ subcategory: [vuln]
3363
+ technology: [puppeteer, playwright, nodejs, javascript, typescript]
3364
+ likelihood: MEDIUM
3365
+ impact: HIGH
3366
+ confidence: MEDIUM
3367
+ vulnerability_class:
3368
+ - Server-Side Request Forgery (SSRF)
3369
+ references:
3370
+ - https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/
3371
+ pattern-sources:
3372
+ - patterns:
3373
+ - pattern-either:
3374
+ - pattern: $REQ.body
3375
+ - pattern: $REQ.query
3376
+ - pattern: $REQ.params
3377
+ - metavariable-regex:
3378
+ metavariable: $REQ
3379
+ regex: ^(req|request|_req|ctx|httpRequest|httpReq)$
3380
+ - patterns:
3381
+ - pattern: $REQ.$KIND.$X
3382
+ - metavariable-regex:
3383
+ metavariable: $KIND
3384
+ regex: ^(body|params|query|headers|cookies)$
3385
+ pattern-sanitizers:
3386
+ - pattern: safeFetch(...)
3387
+ - pattern: isAllowedUrl(...)
3388
+ - pattern: isAllowedHost(...)
3389
+ - pattern: isSafeUrl(...)
3390
+ - pattern: validateUrl(...)
3391
+ - pattern: validateHost(...)
3392
+ - patterns:
3393
+ - pattern: $V
3394
+ - pattern-inside: |
3395
+ if (<... $ALLOW.includes(...) ...>) { ... }
3396
+ - patterns:
3397
+ - pattern: $V
3398
+ - pattern-inside: |
3399
+ if (!$GUARD($V)) { ... }
3400
+ ...
3401
+ - patterns:
3402
+ - pattern: $V
3403
+ - pattern-inside: |
3404
+ if (!$GUARD($V)) return ...;
3405
+ ...
3406
+ - patterns:
3407
+ - pattern: $V
3408
+ - pattern-inside: |
3409
+ if (!$ALLOW.includes($V)) { ... }
3410
+ ...
3411
+ - patterns:
3412
+ - pattern: $V
3413
+ - pattern-inside: |
3414
+ if (!$ALLOW.includes($V)) return ...;
3415
+ ...
3416
+ pattern-sinks:
3417
+ - patterns:
3418
+ - pattern-either:
3419
+ - pattern: $PAGE.goto($SINK, ...)
3420
+ - pattern: $PAGE.setContent($SINK, ...)
3421
+ - focus-metavariable: $SINK
3422
+
3423
+ # `javascript:` URI reaching an href/src sink; regex requires a dynamic part so void(0)/; literals are ignored.
3424
+ - id: narvy.javascript.xss.javascript-uri-href-sink.javascript-uri-href-sink
3425
+ message: >-
3426
+ A `javascript:` URI with an interpolated/concatenated value is
3427
+ assigned to an href/src sink. Whatever reaches the interpolation is
3428
+ executed as script when the link is followed, so any
3429
+ attacker-influenced value there is XSS (CWE-79). Build the handler in
3430
+ JS (`addEventListener`) instead of encoding it into a URI, and never
3431
+ assign a `javascript:` scheme from data.
3432
+ severity: ERROR
3433
+ languages: [javascript, typescript]
3434
+ metadata:
3435
+ cwe:
3436
+ - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
3437
+ owasp:
3438
+ - "A03:2021 - Injection"
3439
+ category: security
3440
+ subcategory: [vuln]
3441
+ technology: [browser, react, javascript, typescript]
3442
+ likelihood: MEDIUM
3443
+ impact: HIGH
3444
+ confidence: MEDIUM
3445
+ vulnerability_class:
3446
+ - Cross-Site-Scripting (XSS)
3447
+ patterns:
3448
+ - pattern-either:
3449
+ - pattern: $EL.href = $X
3450
+ - pattern: $EL.src = $X
3451
+ - pattern: $EL.setAttribute("href", $X)
3452
+ - pattern: $EL.setAttribute("src", $X)
3453
+ - pattern: $EL.setAttribute("xlink:href", $X)
3454
+ - pattern: <$TAG href={$X} ...>...</$TAG>
3455
+ - pattern: <$TAG href={$X} ... />
3456
+ - pattern: <$TAG src={$X} ...>...</$TAG>
3457
+ - pattern: <$TAG src={$X} ... />
3458
+ - metavariable-regex:
3459
+ metavariable: $X
3460
+ regex: '(?s)^["''`]\s*javascript\s*:.*(\$\{|["''`]\s*\+).*'
3461
+
3462
+ # jQuery markup-insertion sinks beyond .html(); guards: string/template arg, contains markup, at least one raw-data operand.
3463
+ - id: narvy.javascript.xss.jquery-markup-insertion.jquery-markup-insertion
3464
+ message: >-
3465
+ A jQuery markup-insertion method is called with an HTML string built
3466
+ by concatenation/interpolation. Like `.html()`, these methods PARSE
3467
+ their string argument as markup, so any unescaped value in it is
3468
+ DOM-based or stored XSS (CWE-79). Insert text with `.text()`, build
3469
+ nodes with `$("<tag>").text(value)`, or sanitize with DOMPurify.
3470
+ severity: WARNING
3471
+ languages: [javascript, typescript]
3472
+ metadata:
3473
+ cwe:
3474
+ - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
3475
+ owasp:
3476
+ - "A03:2021 - Injection"
3477
+ category: security
3478
+ subcategory: [vuln]
3479
+ technology: [jquery, browser, javascript, typescript]
3480
+ likelihood: MEDIUM
3481
+ impact: MEDIUM
3482
+ confidence: MEDIUM
3483
+ vulnerability_class:
3484
+ - Cross-Site-Scripting (XSS)
3485
+ patterns:
3486
+ - pattern: $EL.$M($ARG)
3487
+ - metavariable-regex:
3488
+ metavariable: $M
3489
+ regex: ^(append|prepend|after|before|replaceWith|wrap|wrapInner)$
3490
+ # Guard 1: a string/template expression, not a node or jQuery object.
3491
+ - metavariable-regex:
3492
+ metavariable: $ARG
3493
+ regex: '(?s)^["''`].*'
3494
+ # Guard 2: it actually contains markup.
3495
+ - metavariable-regex:
3496
+ metavariable: $ARG
3497
+ regex: '(?s).*<\s*[a-zA-Z/].*'
3498
+ # Guard 3: at least one raw-data operand, not only formatted/encoded call results.
3499
+ - metavariable-regex:
3500
+ metavariable: $ARG
3501
+ regex: '(?s).*(\+\s*[A-Za-z_$][A-Za-z0-9_$.\[\]]*\s*(\+|$)|\$\{\s*[A-Za-z_$][A-Za-z0-9_$.\[\]]*\s*\}).*'
3502
+ - pattern-not: $EL.$M($SAN.sanitize(...))
3503
+ - pattern-not: $EL.$M(DOMPurify.sanitize(...))
3504
+
3505
+ # jQuery selector from a URL fragment; only the sentinel-stripped form (slice/substring/replace/URLSearchParams) can parse as HTML.
3506
+ - id: narvy.javascript.xss.jquery-selector-from-url.jquery-selector-from-url
3507
+ message: >-
3508
+ A jQuery selector is built from a URL fragment/query value whose
3509
+ leading `#`/`?` sentinel has been stripped. jQuery parses a string
3510
+ starting with `<` as MARKUP rather than as a selector, so an attacker
3511
+ controlling the fragment gets DOM-based XSS (CWE-79). Look the element
3512
+ up explicitly (`document.getElementById(value)`) instead of passing
3513
+ URL data to `$()`.
3514
+ severity: ERROR
3515
+ languages: [javascript, typescript]
3516
+ metadata:
3517
+ cwe:
3518
+ - "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
3519
+ owasp:
3520
+ - "A03:2021 - Injection"
3521
+ category: security
3522
+ subcategory: [vuln]
3523
+ technology: [jquery, browser, javascript, typescript]
3524
+ likelihood: MEDIUM
3525
+ impact: HIGH
3526
+ confidence: MEDIUM
3527
+ vulnerability_class:
3528
+ - Cross-Site-Scripting (XSS)
3529
+ patterns:
3530
+ - pattern-either:
3531
+ - pattern: $($LOC.hash.$STRIP(...))
3532
+ - pattern: $($LOC.search.$STRIP(...))
3533
+ - pattern: $(decodeURIComponent($LOC.hash.$STRIP(...)))
3534
+ - pattern: $(decodeURIComponent($LOC.search.$STRIP(...)))
3535
+ - pattern: $(decodeURI($LOC.hash.$STRIP(...)))
3536
+ - pattern: |
3537
+ $V = $LOC.hash.$STRIP(...);
3538
+ ...
3539
+ $($V)
3540
+ - pattern: |
3541
+ $V = decodeURIComponent($LOC.hash.$STRIP(...));
3542
+ ...
3543
+ $($V)
3544
+ - metavariable-regex:
3545
+ metavariable: $STRIP
3546
+ regex: ^(slice|substring|substr|replace|split)$
3547
+
3548
+ # node-serialize / serialize-to-js .unserialize() eval-s embedded functions (RCE); scoped to receivers imported from those packages.
3549
+ - id: narvy.javascript.deserialization.node-serialize-unserialize.node-serialize-unserialize
3550
+ message: >-
3551
+ `unserialize()` from node-serialize / serialize-to-js is called on a
3552
+ non-literal value. These libraries restore FUNCTIONS by evaluating the
3553
+ serialized payload, so attacker-controlled input here is remote code
3554
+ execution (CWE-502). Use `JSON.parse` and rebuild behaviour from data,
3555
+ or a serializer that never evaluates code.
3556
+ severity: ERROR
3557
+ languages: [javascript, typescript]
3558
+ metadata:
3559
+ cwe:
3560
+ - "CWE-502: Deserialization of Untrusted Data"
3561
+ - "CWE-94: Improper Control of Generation of Code ('Code Injection')"
3562
+ owasp:
3563
+ - "A08:2021 - Software and Data Integrity Failures"
3564
+ category: security
3565
+ subcategory: [vuln]
3566
+ technology: [node-serialize, serialize-to-js, nodejs, javascript, typescript]
3567
+ likelihood: HIGH
3568
+ impact: HIGH
3569
+ confidence: HIGH
3570
+ vulnerability_class:
3571
+ - Insecure Deserialization
3572
+ references:
3573
+ - https://cwe.mitre.org/data/definitions/502.html
3574
+ pattern-either:
3575
+ - patterns:
3576
+ - pattern: $S.unserialize($X)
3577
+ - pattern-not: $S.unserialize("...")
3578
+ - pattern-either:
3579
+ - pattern-inside: |
3580
+ $S = require("node-serialize");
3581
+ ...
3582
+ - pattern-inside: |
3583
+ $S = require("serialize-to-js");
3584
+ ...
3585
+ - pattern-inside: |
3586
+ import $S from "node-serialize";
3587
+ ...
3588
+ - pattern-inside: |
3589
+ import * as $S from "node-serialize";
3590
+ ...
3591
+ - pattern-inside: |
3592
+ import $S from "serialize-to-js";
3593
+ ...
3594
+ - pattern-inside: |
3595
+ import * as $S from "serialize-to-js";
3596
+ ...
3597
+ - patterns:
3598
+ - pattern-either:
3599
+ - pattern: require("node-serialize").unserialize($X)
3600
+ - pattern: require("serialize-to-js").unserialize($X)
3601
+ - pattern-not: require("...").unserialize("...")
3602
+
3603
+ # Catastrophic-backtracking regex literal (static ReDoS): a quantified group holding a single quantified atom.
3604
+ - id: narvy.javascript.misc.redos-nested-quantifier-literal.redos-nested-quantifier-literal
3605
+ message: >-
3606
+ Regex literal with a nested quantifier over a single atom (e.g.
3607
+ `(a+)+`, `([a-z]+)*`). The engine can split the input between the
3608
+ inner and outer quantifier in exponentially many ways, so a crafted
3609
+ non-matching subject string hangs the event loop - catastrophic
3610
+ backtracking / ReDoS (CWE-1333). Drop the redundant outer quantifier,
3611
+ make the group unambiguous, or match with a linear-time engine (re2).
3612
+ severity: WARNING
3613
+ languages: [javascript, typescript]
3614
+ metadata:
3615
+ cwe:
3616
+ - "CWE-1333: Inefficient Regular Expression Complexity"
3617
+ - "CWE-400: Uncontrolled Resource Consumption"
3618
+ owasp:
3619
+ - "A05:2021 - Security Misconfiguration"
3620
+ category: security
3621
+ subcategory: [audit]
3622
+ technology: [javascript, typescript]
3623
+ likelihood: LOW
3624
+ impact: MEDIUM
3625
+ confidence: MEDIUM
3626
+ vulnerability_class:
3627
+ - Denial-of-Service (DoS)
3628
+ patterns:
3629
+ - pattern-either:
3630
+ - pattern: $RE.test(...)
3631
+ - pattern: $RE.exec(...)
3632
+ - pattern: $X.match($RE)
3633
+ - pattern: $X.matchAll($RE)
3634
+ - pattern: $X.replace($RE, ...)
3635
+ - pattern: $X.replaceAll($RE, ...)
3636
+ - pattern: $X.split($RE)
3637
+ - pattern: $X.search($RE)
3638
+ - pattern: new RegExp($RE)
3639
+ - pattern: $V = $RE
3640
+ - metavariable-regex:
3641
+ metavariable: $RE
3642
+ regex: '^/.*\((\?:)?(\[[^\]]+\]|\\[dwsSWD]|\.|[A-Za-z0-9])[+*]\)[+*][^+*?].*/[gimsuyd]*$'
3643
+
3644
+ # CORS allow-list regex with an unescaped dot matches attacker domains; regex-on-regex requires the dot followed by a letter, scoped to origin-shaped identifiers.
3645
+ - id: narvy.javascript.cors.allowlist-regex-unescaped-dot.allowlist-regex-unescaped-dot
3646
+ message: >-
3647
+ CORS origin allow-list regex contains an UNESCAPED dot. `.` matches
3648
+ any character, so `/^https://app.example.com$/` also accepts
3649
+ `https://appXexampleYcom` - a hostname an attacker can register - and
3650
+ the server will reflect it in Access-Control-Allow-Origin (CWE-942).
3651
+ Escape the dots (`app\.example\.com`) and anchor the pattern with
3652
+ `^`/`$`.
3653
+ severity: ERROR
3654
+ languages: [javascript, typescript]
3655
+ metadata:
3656
+ cwe:
3657
+ - "CWE-942: Permissive Cross-domain Policy with Untrusted Domains"
3658
+ - "CWE-346: Origin Validation Error"
3659
+ owasp:
3660
+ - "A05:2021 - Security Misconfiguration"
3661
+ category: security
3662
+ subcategory: [vuln]
3663
+ technology: [cors, express, nodejs, javascript, typescript]
3664
+ likelihood: MEDIUM
3665
+ impact: HIGH
3666
+ confidence: MEDIUM
3667
+ vulnerability_class:
3668
+ - Improper Access Control
3669
+ patterns:
3670
+ - pattern-either:
3671
+ - patterns:
3672
+ - pattern-either:
3673
+ - pattern: $RE.test($ORIGIN)
3674
+ - pattern: $ORIGIN.match($RE)
3675
+ - pattern: $RE.exec($ORIGIN)
3676
+ - metavariable-regex:
3677
+ metavariable: $ORIGIN
3678
+ regex: '(?i)^_?(origin|reqorigin|requestorigin|referer|referrer|allowedorigin|targetorigin)$'
3679
+ - pattern: "cors({..., origin: $RE, ...})"
3680
+ - pattern: "cors({..., origin: [..., $RE, ...], ...})"
3681
+ - metavariable-regex:
3682
+ metavariable: $RE
3683
+ regex: '^/.*[^\\]\.[a-zA-Z].*/[gimsuyd]*$'