security-mcp 1.1.4 → 1.3.1

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 (129) hide show
  1. package/README.md +116 -264
  2. package/defaults/checklists/ai.json +20 -1
  3. package/defaults/checklists/api.json +35 -1
  4. package/defaults/checklists/infra.json +34 -1
  5. package/defaults/checklists/mobile.json +23 -1
  6. package/defaults/checklists/payments.json +15 -1
  7. package/defaults/checklists/web.json +11 -1
  8. package/defaults/security-policy.json +2 -2
  9. package/dist/cli/index.js +0 -0
  10. package/dist/gate/baseline.js +82 -7
  11. package/dist/gate/catalog.js +10 -2
  12. package/dist/gate/checks/ai.js +757 -39
  13. package/dist/gate/checks/auth-deep.js +920 -216
  14. package/dist/gate/checks/business-logic.js +751 -0
  15. package/dist/gate/checks/ci-pipeline.js +399 -4
  16. package/dist/gate/checks/crypto.js +423 -2
  17. package/dist/gate/checks/dependencies.js +571 -15
  18. package/dist/gate/checks/graphql.js +201 -19
  19. package/dist/gate/checks/infra.js +246 -1
  20. package/dist/gate/checks/injection-deep.js +827 -184
  21. package/dist/gate/checks/k8s.js +114 -1
  22. package/dist/gate/checks/mobile-android.js +917 -3
  23. package/dist/gate/checks/mobile-ios.js +797 -5
  24. package/dist/gate/checks/required-artifacts.js +194 -0
  25. package/dist/gate/checks/runtime.js +178 -0
  26. package/dist/gate/checks/secrets.js +244 -13
  27. package/dist/gate/checks/supply-chain-deep.js +787 -0
  28. package/dist/gate/checks/web-nextjs.js +572 -48
  29. package/dist/gate/diff.js +17 -5
  30. package/dist/gate/evidence.js +8 -1
  31. package/dist/gate/exceptions.js +131 -9
  32. package/dist/gate/policy.js +280 -131
  33. package/dist/mcp/audit-chain.js +122 -28
  34. package/dist/mcp/auth.js +169 -0
  35. package/dist/mcp/learning.js +129 -4
  36. package/dist/mcp/model-router.js +158 -21
  37. package/dist/mcp/orchestration.js +186 -51
  38. package/dist/mcp/server.js +337 -53
  39. package/dist/repo/fs.js +24 -1
  40. package/dist/repo/search.js +31 -6
  41. package/dist/review/store.js +52 -1
  42. package/package.json +7 -7
  43. package/skills/_TEMPLATE/SKILL.md +99 -0
  44. package/skills/advanced-dos-tester/SKILL.md +109 -0
  45. package/skills/agentic-loop-exploiter/SKILL.md +368 -0
  46. package/skills/ai-llm-redteam/SKILL.md +104 -0
  47. package/skills/ai-model-supply-chain-agent/SKILL.md +103 -0
  48. package/skills/algorithm-implementation-reviewer/SKILL.md +98 -0
  49. package/skills/android-penetration-tester/SKILL.md +455 -46
  50. package/skills/anti-replay-tester/SKILL.md +106 -0
  51. package/skills/appsec-code-auditor/SKILL.md +85 -0
  52. package/skills/artifact-integrity-analyst/SKILL.md +441 -0
  53. package/skills/attack-navigator/SKILL.md +467 -8
  54. package/skills/auth-session-hacker/SKILL.md +102 -0
  55. package/skills/aws-penetration-tester/SKILL.md +456 -0
  56. package/skills/azure-penetration-tester/SKILL.md +490 -3
  57. package/skills/binary-auth-validator/SKILL.md +111 -0
  58. package/skills/bot-detection-specialist/SKILL.md +109 -0
  59. package/skills/business-logic-attacker/SKILL.md +231 -0
  60. package/skills/capec-code-mapper/SKILL.md +84 -0
  61. package/skills/cert-pin-rotation-specialist/SKILL.md +112 -0
  62. package/skills/cicd-pipeline-hijacker/SKILL.md +405 -0
  63. package/skills/ciso-orchestrator/SKILL.md +454 -43
  64. package/skills/cloud-infra-specialist/SKILL.md +118 -0
  65. package/skills/compliance-gap-analyst/SKILL.md +422 -0
  66. package/skills/compliance-grc/SKILL.md +85 -0
  67. package/skills/compliance-lifecycle-tracker/SKILL.md +84 -0
  68. package/skills/credential-stuffing-specialist/SKILL.md +102 -0
  69. package/skills/crypto-pki-specialist/SKILL.md +87 -0
  70. package/skills/csa-ccm-mapper/SKILL.md +84 -0
  71. package/skills/csf2-governance-mapper/SKILL.md +84 -0
  72. package/skills/deep-link-fuzzer/SKILL.md +109 -0
  73. package/skills/dependency-confusion-attacker/SKILL.md +415 -0
  74. package/skills/device-integrity-aggregator/SKILL.md +108 -0
  75. package/skills/dos-resilience-tester/SKILL.md +97 -0
  76. package/skills/dread-scorer/SKILL.md +84 -0
  77. package/skills/egress-policy-enforcer/SKILL.md +99 -0
  78. package/skills/evidence-collector/SKILL.md +98 -0
  79. package/skills/file-upload-attacker/SKILL.md +109 -0
  80. package/skills/gcp-penetration-tester/SKILL.md +459 -2
  81. package/skills/git-history-secret-scanner/SKILL.md +106 -0
  82. package/skills/iam-privesc-graph-builder/SKILL.md +152 -0
  83. package/skills/incident-responder/SKILL.md +111 -0
  84. package/skills/injection-specialist/SKILL.md +102 -0
  85. package/skills/ios-security-auditor/SKILL.md +282 -0
  86. package/skills/json-ambiguity-tester/SKILL.md +0 -0
  87. package/skills/k8s-container-escaper/SKILL.md +384 -0
  88. package/skills/key-management-lifecycle-analyst/SKILL.md +98 -0
  89. package/skills/kill-switch-engineer/SKILL.md +102 -0
  90. package/skills/linddun-privacy-analyst/SKILL.md +102 -0
  91. package/skills/logic-race-fuzzer/SKILL.md +443 -0
  92. package/skills/mobile-api-network-attacker/SKILL.md +421 -0
  93. package/skills/mobile-binary-hardener/SKILL.md +102 -0
  94. package/skills/mobile-security-specialist/SKILL.md +85 -0
  95. package/skills/mobile-webview-auditor/SKILL.md +96 -0
  96. package/skills/model-extraction-attacker/SKILL.md +219 -0
  97. package/skills/multipart-abuse-tester/SKILL.md +84 -0
  98. package/skills/oauth-pkce-specialist/SKILL.md +104 -0
  99. package/skills/parser-exhaustion-tester/SKILL.md +142 -0
  100. package/skills/pentest-infra/SKILL.md +98 -0
  101. package/skills/pentest-social/SKILL.md +201 -0
  102. package/skills/pentest-team/SKILL.md +87 -0
  103. package/skills/pentest-web-api/SKILL.md +98 -0
  104. package/skills/privacy-flow-analyst/SKILL.md +234 -0
  105. package/skills/prompt-injection-specialist/SKILL.md +394 -0
  106. package/skills/quantum-migration-planner/SKILL.md +96 -0
  107. package/skills/rag-poisoning-specialist/SKILL.md +358 -0
  108. package/skills/registry-mirror-enforcer/SKILL.md +84 -0
  109. package/skills/rotation-validation-agent/SKILL.md +112 -0
  110. package/skills/samm-assessor/SKILL.md +85 -0
  111. package/skills/secrets-mask-bypass-tester/SKILL.md +100 -0
  112. package/skills/senior-security-engineer/SKILL.md +167 -0
  113. package/skills/serialization-memory-attacker/SKILL.md +332 -0
  114. package/skills/session-timeout-tester/SKILL.md +161 -0
  115. package/skills/slsa-level3-enforcer/SKILL.md +112 -0
  116. package/skills/slsa-provenance-enforcer/SKILL.md +102 -0
  117. package/skills/ssrf-detection-validator/SKILL.md +108 -0
  118. package/skills/step-up-auth-enforcer/SKILL.md +84 -0
  119. package/skills/stride-pasta-analyst/SKILL.md +420 -0
  120. package/skills/supply-chain-devsecops/SKILL.md +98 -0
  121. package/skills/threat-infrastructure-analyst/SKILL.md +84 -0
  122. package/skills/threat-modeler/SKILL.md +85 -0
  123. package/skills/tls-certificate-auditor/SKILL.md +573 -18
  124. package/skills/token-reuse-detector/SKILL.md +95 -0
  125. package/skills/trike-risk-modeler/SKILL.md +84 -0
  126. package/skills/unicode-homograph-tester/SKILL.md +84 -0
  127. package/skills/waf-rule-lifecycle-agent/SKILL.md +97 -0
  128. package/skills/webhook-security-tester/SKILL.md +102 -0
  129. package/skills/zero-trust-architect/SKILL.md +109 -0
@@ -1,76 +1,600 @@
1
1
  import { searchRepo } from "../../repo/search.js";
2
2
  import fg from "fast-glob";
3
3
  import { readFileSafe } from "../../repo/fs.js";
4
- export async function checkWebNextjs(_) {
5
- const findings = [];
6
- // 1) CSP and security headers should exist (Next middleware or edge config)
7
- const headerFiles = await fg(["middleware.ts", "middleware.tsx", "src/middleware.ts", "next.config.*"], {
8
- dot: true
9
- });
4
+ async function runAll(checks) {
5
+ const results = await Promise.all(checks.map((fn) => fn()));
6
+ return results.flat();
7
+ }
8
+ // ---------------------------------------------------------------------------
9
+ // 1. CSP and security headers (EXISTING)
10
+ // ---------------------------------------------------------------------------
11
+ async function checkSecurityHeaders() {
12
+ const headerFiles = await fg(["middleware.ts", "middleware.tsx", "src/middleware.ts", "next.config.*"], { dot: true });
10
13
  if (headerFiles.length === 0) {
11
- findings.push({
12
- id: "WEB_HEADERS_MISSING",
13
- title: "Security headers not found (CSP/HSTS/etc.)",
14
- severity: "HIGH",
15
- requiredActions: [
16
- "Add strict security headers: CSP (no inline JS), HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy.",
17
- "Enforce secure cookies: HttpOnly, Secure, SameSite, short-lived tokens."
18
- ]
19
- });
20
- }
21
- else {
22
- const combined = (await Promise.all(headerFiles.map((f) => readFileSafe(f).catch(() => "")))).join("\n");
23
- const mustContain = [
24
- "content-security-policy",
25
- "strict-transport-security",
26
- "referrer-policy",
27
- "permissions-policy"
28
- ];
29
- const missing = mustContain.filter((k) => !combined.toLowerCase().includes(k));
30
- if (missing.length > 0) {
31
- findings.push({
32
- id: "WEB_HEADERS_INCOMPLETE",
33
- title: "Security headers exist but appear incomplete",
14
+ return [
15
+ {
16
+ id: "WEB_HEADERS_MISSING",
17
+ title: "Security headers not found (CSP/HSTS/etc.)",
34
18
  severity: "HIGH",
35
- evidence: [`Missing: ${missing.join(", ")}`],
36
19
  requiredActions: [
37
- "Add missing headers and ensure CSP forbids inline scripts (no 'unsafe-inline').",
38
- "Add a CSP nonce strategy if you must load dynamic scripts."
20
+ "Add strict security headers: CSP (no inline JS), HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy.",
21
+ "Enforce secure cookies: HttpOnly, Secure, SameSite, short-lived tokens."
39
22
  ]
40
- });
41
- }
23
+ }
24
+ ];
42
25
  }
43
- // 2) Flag dangerous React usage
44
- const dsi = await searchRepo({ query: "dangerouslySetInnerHTML", isRegex: false, maxMatches: 200 });
45
- if (dsi.length > 0) {
46
- findings.push({
26
+ const combined = (await Promise.all(headerFiles.map((f) => readFileSafe(f).catch(() => "")))).join("\n");
27
+ const mustContain = [
28
+ "content-security-policy",
29
+ "strict-transport-security",
30
+ "referrer-policy",
31
+ "permissions-policy"
32
+ ];
33
+ const missing = mustContain.filter((k) => !combined.toLowerCase().includes(k));
34
+ if (missing.length === 0)
35
+ return [];
36
+ return [
37
+ {
38
+ id: "WEB_HEADERS_INCOMPLETE",
39
+ title: "Security headers exist but appear incomplete",
40
+ severity: "HIGH",
41
+ evidence: [`Missing: ${missing.join(", ")}`],
42
+ requiredActions: [
43
+ "Add missing headers and ensure CSP forbids inline scripts (no 'unsafe-inline').",
44
+ "Add a CSP nonce strategy if you must load dynamic scripts."
45
+ ]
46
+ }
47
+ ];
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // 2. dangerouslySetInnerHTML (EXISTING)
51
+ // ---------------------------------------------------------------------------
52
+ async function checkDangerouslySetInnerHTML() {
53
+ const hits = await searchRepo({
54
+ query: "dangerouslySetInnerHTML",
55
+ isRegex: false,
56
+ maxMatches: 200
57
+ });
58
+ if (hits.length === 0)
59
+ return [];
60
+ return [
61
+ {
47
62
  id: "DANGEROUSLY_SET_INNER_HTML",
48
63
  title: "dangerouslySetInnerHTML usage detected",
49
64
  severity: "HIGH",
50
- evidence: dsi.slice(0, 20).map((m) => `${m.file}:${m.line}:${m.preview}`),
65
+ evidence: hits.slice(0, 20).map((m) => `${m.file}:${m.line}:${m.preview}`),
51
66
  requiredActions: [
52
67
  "Remove dangerouslySetInnerHTML where possible.",
53
68
  "If unavoidable: sanitize with a proven HTML sanitizer and add unit tests with XSS payloads."
54
69
  ]
55
- });
56
- }
57
- // 3) Basic SSRF risk pattern scan (server-side fetch)
58
- const fetchHits = await searchRepo({
70
+ }
71
+ ];
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // 3. SSRF guard (EXISTING)
75
+ // ---------------------------------------------------------------------------
76
+ async function checkSsrf() {
77
+ const hits = await searchRepo({
59
78
  query: String.raw `\bfetch\(|axios\(|got\(|undici\b`,
60
79
  isRegex: true,
61
80
  maxMatches: 200
62
81
  });
63
- if (fetchHits.length > 0) {
64
- findings.push({
82
+ if (hits.length === 0)
83
+ return [];
84
+ return [
85
+ {
65
86
  id: "SSRF_GUARD_REQUIRED",
66
87
  title: "Server-side fetch patterns detected. SSRF protections must be enforced.",
67
88
  severity: "HIGH",
68
- evidence: fetchHits.slice(0, 15).map((m) => `${m.file}:${m.line}:${m.preview}`),
89
+ evidence: hits.slice(0, 15).map((m) => `${m.file}:${m.line}:${m.preview}`),
69
90
  requiredActions: [
70
91
  "Implement SSRF guard for any server-side HTTP client: block localhost, private IP ranges, and cloud metadata endpoints.",
71
92
  "Require URL allowlists for outbound calls. Add tests for 127.0.0.1, 10/8, 172.16/12, 192.168/16, 169.254.169.254, metadata.google.internal."
72
93
  ]
73
- });
94
+ }
95
+ ];
96
+ }
97
+ // ---------------------------------------------------------------------------
98
+ // 4. WEB_OPEN_REDIRECT — unvalidated redirects with user-controlled input
99
+ // ---------------------------------------------------------------------------
100
+ async function checkOpenRedirect() {
101
+ const hits = await searchRepo({
102
+ query: String.raw `redirect\(|res\.redirect\(`,
103
+ isRegex: true,
104
+ maxMatches: 200
105
+ });
106
+ // Filter to lines that also reference common user-input sources
107
+ const suspicious = hits.filter((m) => /req\.(query|body)|searchParams|\.get\(/.test(m.preview));
108
+ if (suspicious.length === 0)
109
+ return [];
110
+ return [
111
+ {
112
+ id: "WEB_OPEN_REDIRECT",
113
+ title: "Unvalidated redirect with user-controlled input detected",
114
+ severity: "HIGH",
115
+ evidence: suspicious.slice(0, 15).map((m) => `${m.file}:${m.line}:${m.preview}`),
116
+ requiredActions: [
117
+ "Validate redirect destinations against a strict allowlist of trusted origins.",
118
+ "Never pass raw req.query, req.body, or searchParams values directly to redirect().",
119
+ "Return a 400 if the destination is not in the allowlist."
120
+ ]
121
+ }
122
+ ];
123
+ }
124
+ // ---------------------------------------------------------------------------
125
+ // 5. WEB_IDOR_RISK — direct object reference from URL params without auth check
126
+ // ---------------------------------------------------------------------------
127
+ async function checkIdorRisk() {
128
+ const hits = await searchRepo({
129
+ query: String.raw `params\.(id|userId|user_id|accountId|account_id)\b`,
130
+ isRegex: true,
131
+ maxMatches: 200
132
+ });
133
+ // Keep only hits that don't have an obvious auth guard on the same or adjacent line
134
+ const suspicious = hits.filter((m) => !/auth|session|getServerSession|currentUser|requireAuth|userId\s*===/.test(m.preview));
135
+ if (suspicious.length === 0)
136
+ return [];
137
+ return [
138
+ {
139
+ id: "WEB_IDOR_RISK",
140
+ title: "Direct object reference from URL params without visible ownership check",
141
+ severity: "HIGH",
142
+ evidence: suspicious.slice(0, 15).map((m) => `${m.file}:${m.line}:${m.preview}`),
143
+ requiredActions: [
144
+ "After fetching a resource by URL param, verify the authenticated user owns or is authorised to access it.",
145
+ "Never rely on obscurity of IDs — enforce ownership checks server-side.",
146
+ "Use opaque, non-guessable IDs (UUIDs) and still enforce access control."
147
+ ]
148
+ }
149
+ ];
150
+ }
151
+ // ---------------------------------------------------------------------------
152
+ // 6. WEB_SERVER_ACTION_UNVALIDATED — Server Actions without Zod validation
153
+ // ---------------------------------------------------------------------------
154
+ async function checkServerActionValidation() {
155
+ // Find all files that contain "use server"
156
+ const useServerHits = await searchRepo({
157
+ query: '"use server"',
158
+ isRegex: false,
159
+ maxMatches: 200
160
+ });
161
+ if (useServerHits.length === 0)
162
+ return [];
163
+ // For each unique file, check whether it also contains a Zod parse call
164
+ const serverActionFiles = [...new Set(useServerHits.map((m) => m.file))];
165
+ const unvalidated = [];
166
+ for (const file of serverActionFiles) {
167
+ const content = await readFileSafe(file).catch(() => "");
168
+ if (!content.includes(".parse(") && !content.includes(".safeParse(")) {
169
+ unvalidated.push(file);
170
+ }
171
+ }
172
+ if (unvalidated.length === 0)
173
+ return [];
174
+ return [
175
+ {
176
+ id: "WEB_SERVER_ACTION_UNVALIDATED",
177
+ title: 'Next.js Server Actions found without Zod input validation',
178
+ severity: "HIGH",
179
+ evidence: unvalidated.slice(0, 15).map((f) => `${f}: no .parse() or .safeParse() found`),
180
+ requiredActions: [
181
+ 'Add a Zod schema and call schema.parse() or schema.safeParse() at the top of every Server Action.',
182
+ "Never trust FormData or action arguments directly — validate shape, type, and constraints.",
183
+ "Throw or return an error object when validation fails; never proceed with unvalidated data."
184
+ ]
185
+ }
186
+ ];
187
+ }
188
+ // ---------------------------------------------------------------------------
189
+ // 7. WEB_API_NO_AUTH — route.ts files without auth middleware
190
+ // ---------------------------------------------------------------------------
191
+ async function checkApiRouteAuth() {
192
+ const routeFiles = await fg(["**/route.ts", "**/route.tsx"], { dot: true });
193
+ if (routeFiles.length === 0)
194
+ return [];
195
+ const unprotected = [];
196
+ for (const file of routeFiles) {
197
+ const content = await readFileSafe(file).catch(() => "");
198
+ if (!/auth\(|session\(|getServerSession|currentUser|requireAuth/.test(content)) {
199
+ unprotected.push(file);
200
+ }
74
201
  }
75
- return findings;
202
+ if (unprotected.length === 0)
203
+ return [];
204
+ return [
205
+ {
206
+ id: "WEB_API_NO_AUTH",
207
+ title: "API route handlers found without authentication middleware",
208
+ severity: "HIGH",
209
+ evidence: unprotected.slice(0, 15).map((f) => `${f}: no auth guard detected`),
210
+ requiredActions: [
211
+ "Add authentication to every route handler: call auth(), getServerSession(), or a custom requireAuth() wrapper.",
212
+ "Return HTTP 401 for unauthenticated requests before touching any business logic.",
213
+ "If the route is intentionally public, add a comment // PUBLIC ROUTE so this check can be tuned to ignore it."
214
+ ]
215
+ }
216
+ ];
217
+ }
218
+ // ---------------------------------------------------------------------------
219
+ // 8. WEB_CORS_WILDCARD — Access-Control-Allow-Origin: * in API responses
220
+ // ---------------------------------------------------------------------------
221
+ async function checkCorsWildcard() {
222
+ const hits = await searchRepo({
223
+ query: "Access-Control-Allow-Origin",
224
+ isRegex: false,
225
+ maxMatches: 200
226
+ });
227
+ const wildcards = hits.filter((m) => /:\s*['"]\*['"]|,\s*['"]\*['"]/.test(m.preview));
228
+ if (wildcards.length === 0)
229
+ return [];
230
+ return [
231
+ {
232
+ id: "WEB_CORS_WILDCARD",
233
+ title: "CORS wildcard (Access-Control-Allow-Origin: *) found in API response",
234
+ severity: "CRITICAL",
235
+ evidence: wildcards.slice(0, 15).map((m) => `${m.file}:${m.line}:${m.preview}`),
236
+ requiredActions: [
237
+ "Replace the wildcard origin with an explicit allowlist of trusted origins.",
238
+ "Never use * on endpoints that handle authenticated sessions or sensitive data.",
239
+ "Use environment-specific origin lists (dev vs prod)."
240
+ ]
241
+ }
242
+ ];
243
+ }
244
+ // ---------------------------------------------------------------------------
245
+ // 9. WEB_JWT_HARDCODED_SECRET — jwt.sign / jwt.verify with string literal secret
246
+ // ---------------------------------------------------------------------------
247
+ async function checkJwtHardcodedSecret() {
248
+ const hits = await searchRepo({
249
+ query: String.raw `jwt\.(sign|verify)\(`,
250
+ isRegex: true,
251
+ maxMatches: 200
252
+ });
253
+ // Flag lines where the secret argument looks like a string literal rather than
254
+ // a reference to process.env or a variable.
255
+ const suspicious = hits.filter((m) => /jwt\.(sign|verify)\([^)]*["'][A-Za-z0-9+/=_\-!@#$%^&*]{8,}["']/.test(m.preview));
256
+ if (suspicious.length === 0)
257
+ return [];
258
+ return [
259
+ {
260
+ id: "WEB_JWT_HARDCODED_SECRET",
261
+ title: "JWT sign/verify called with what appears to be a hardcoded secret",
262
+ severity: "CRITICAL",
263
+ evidence: suspicious.slice(0, 15).map((m) => `${m.file}:${m.line}:${m.preview}`),
264
+ requiredActions: [
265
+ "Move the JWT secret to an environment variable (e.g. process.env.JWT_SECRET).",
266
+ "Rotate any secret that was ever hardcoded in source — treat it as compromised.",
267
+ "Use a minimum 256-bit secret for HMAC-SHA256 signed tokens."
268
+ ]
269
+ }
270
+ ];
271
+ }
272
+ // ---------------------------------------------------------------------------
273
+ // 10. WEB_RATE_LIMIT_MISSING — auth/payment routes without rate limiting
274
+ // ---------------------------------------------------------------------------
275
+ async function checkRateLimitMissing() {
276
+ // Find route handlers for sensitive operations
277
+ const sensitiveRoutes = await fg([
278
+ "**/auth**/route.ts",
279
+ "**/login**/route.ts",
280
+ "**/register**/route.ts",
281
+ "**/payment**/route.ts",
282
+ "**/checkout**/route.ts",
283
+ "**/signin**/route.ts",
284
+ "**/signup**/route.ts"
285
+ ], { dot: true });
286
+ if (sensitiveRoutes.length === 0)
287
+ return [];
288
+ const unprotected = [];
289
+ for (const file of sensitiveRoutes) {
290
+ const content = await readFileSafe(file).catch(() => "");
291
+ if (!/rateLimit|upstash|rate.limit|rateLimiter/.test(content)) {
292
+ unprotected.push(file);
293
+ }
294
+ }
295
+ if (unprotected.length === 0)
296
+ return [];
297
+ return [
298
+ {
299
+ id: "WEB_RATE_LIMIT_MISSING",
300
+ title: "Auth/payment route handlers found without rate limiting",
301
+ severity: "HIGH",
302
+ evidence: unprotected.slice(0, 15).map((f) => `${f}: no rate-limit guard detected`),
303
+ requiredActions: [
304
+ "Apply rate limiting to all auth, login, register, and payment endpoints.",
305
+ "Use Upstash Rate Limit or a similar sliding-window implementation.",
306
+ "Return HTTP 429 with a Retry-After header when the limit is exceeded.",
307
+ "Set tight limits: e.g. 5 attempts / 15 minutes for login, 3 / 60 min for registration."
308
+ ]
309
+ }
310
+ ];
311
+ }
312
+ // ---------------------------------------------------------------------------
313
+ // 11. WEB_ENV_EXPOSED_CLIENT — server secrets in NEXT_PUBLIC_ vars
314
+ // ---------------------------------------------------------------------------
315
+ async function checkEnvExposedClient() {
316
+ const envFiles = await fg([".env*", "**/env.js", "**/env.ts", "**/env.mjs"], { dot: true });
317
+ const hits = await searchRepo({
318
+ query: "NEXT_PUBLIC_SECRET|NEXT_PUBLIC_API_KEY|NEXT_PUBLIC_TOKEN|NEXT_PUBLIC_PASSWORD",
319
+ isRegex: false,
320
+ maxMatches: 200
321
+ });
322
+ // Also scan env files directly for the patterns
323
+ const envHits = [];
324
+ for (const file of envFiles) {
325
+ const content = await readFileSafe(file).catch(() => "");
326
+ if (/NEXT_PUBLIC_(SECRET|API_KEY|TOKEN|PASSWORD)/.test(content)) {
327
+ envHits.push(file);
328
+ }
329
+ }
330
+ if (hits.length === 0 && envHits.length === 0)
331
+ return [];
332
+ const evidence = [
333
+ ...hits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
334
+ ...envHits.map((f) => `${f}: contains NEXT_PUBLIC_ secret variable`)
335
+ ];
336
+ return [
337
+ {
338
+ id: "WEB_ENV_EXPOSED_CLIENT",
339
+ title: "Server-side secrets detected in NEXT_PUBLIC_ environment variables",
340
+ severity: "CRITICAL",
341
+ evidence: evidence.slice(0, 20),
342
+ requiredActions: [
343
+ "Remove NEXT_PUBLIC_ prefix from any variable containing a secret, API key, token, or password.",
344
+ "NEXT_PUBLIC_ variables are bundled into the client JS and visible to all users.",
345
+ "Use server-only env vars (no NEXT_PUBLIC_ prefix) and access them in Server Components or API routes.",
346
+ "Rotate any secret that was ever exposed as NEXT_PUBLIC_ — treat it as compromised."
347
+ ]
348
+ }
349
+ ];
350
+ }
351
+ // ---------------------------------------------------------------------------
352
+ // 12. WEB_GRAPHQL_INTROSPECTION — introspection enabled without NODE_ENV guard
353
+ // ---------------------------------------------------------------------------
354
+ async function checkGraphqlIntrospection() {
355
+ const hits = await searchRepo({
356
+ query: "introspection: true",
357
+ isRegex: false,
358
+ maxMatches: 100
359
+ });
360
+ const unguarded = hits.filter((m) => !/NODE_ENV|process\.env/.test(m.preview));
361
+ if (unguarded.length === 0)
362
+ return [];
363
+ return [
364
+ {
365
+ id: "WEB_GRAPHQL_INTROSPECTION",
366
+ title: "GraphQL introspection enabled without NODE_ENV guard",
367
+ severity: "MEDIUM",
368
+ evidence: unguarded.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
369
+ requiredActions: [
370
+ "Disable GraphQL introspection in production: `introspection: process.env.NODE_ENV !== 'production'`.",
371
+ "Introspection exposes the full API schema to attackers and aids targeted exploitation.",
372
+ "Consider also disabling GraphQL Playground / Sandbox in production."
373
+ ]
374
+ }
375
+ ];
376
+ }
377
+ // ---------------------------------------------------------------------------
378
+ // 13. WEB_PATH_TRAVERSAL — user-controlled input passed to fs / path.join
379
+ // ---------------------------------------------------------------------------
380
+ async function checkPathTraversal() {
381
+ const hits = await searchRepo({
382
+ query: String.raw `fs\.readFile|fs\.readFileSync|path\.join`,
383
+ isRegex: true,
384
+ maxMatches: 200
385
+ });
386
+ const suspicious = hits.filter((m) => /req\.(query|params|body)|searchParams|\.get\(/.test(m.preview));
387
+ if (suspicious.length === 0)
388
+ return [];
389
+ return [
390
+ {
391
+ id: "WEB_PATH_TRAVERSAL",
392
+ title: "Potential path traversal — user input passed to fs or path.join",
393
+ severity: "HIGH",
394
+ evidence: suspicious.slice(0, 15).map((m) => `${m.file}:${m.line}:${m.preview}`),
395
+ requiredActions: [
396
+ "Never pass user-supplied path segments directly to fs.readFile / path.join.",
397
+ "Resolve the full path and assert it starts with the expected base directory (path.resolve check).",
398
+ "Use an allowlist of valid filenames instead of accepting arbitrary paths from user input."
399
+ ]
400
+ }
401
+ ];
402
+ }
403
+ // ---------------------------------------------------------------------------
404
+ // 14. WEB_LOG_PII — PII fields near console.log / logger calls
405
+ // ---------------------------------------------------------------------------
406
+ async function checkLogPii() {
407
+ const hits = await searchRepo({
408
+ query: String.raw `console\.(log|error|warn|info|debug)|logger\.(log|error|warn|info|debug)`,
409
+ isRegex: true,
410
+ maxMatches: 400
411
+ });
412
+ const piiFields = /email|password|token|ssn|cardNumber|card_number|cvv|dob|dateOfBirth/i;
413
+ const suspicious = hits.filter((m) => piiFields.test(m.preview));
414
+ if (suspicious.length === 0)
415
+ return [];
416
+ return [
417
+ {
418
+ id: "WEB_LOG_PII",
419
+ title: "Potential PII or sensitive fields logged in server-side code",
420
+ severity: "HIGH",
421
+ evidence: suspicious.slice(0, 15).map((m) => `${m.file}:${m.line}:${m.preview}`),
422
+ requiredActions: [
423
+ "Never log PII (email, password, token, SSN, card number, CVV, date-of-birth) at any log level.",
424
+ "Strip sensitive fields before logging: log only IDs, timestamps, and non-sensitive metadata.",
425
+ "Replace logged secrets with [REDACTED] and add a lint rule (eslint-plugin-no-secrets) to enforce this."
426
+ ]
427
+ }
428
+ ];
429
+ }
430
+ // ---------------------------------------------------------------------------
431
+ // 15. WEB_SESSION_WEAK_CONFIG — session config without secure/httpOnly/sameSite
432
+ // ---------------------------------------------------------------------------
433
+ async function checkSessionWeakConfig() {
434
+ const hits = await searchRepo({
435
+ query: String.raw `express-session|iron-session|session\(\{`,
436
+ isRegex: true,
437
+ maxMatches: 200
438
+ });
439
+ if (hits.length === 0)
440
+ return [];
441
+ // Gather unique files and inspect their full content for secure config flags
442
+ const sessionFiles = [...new Set(hits.map((m) => m.file))];
443
+ const weakFiles = [];
444
+ for (const file of sessionFiles) {
445
+ const content = await readFileSafe(file).catch(() => "");
446
+ const hasSecure = /secure\s*:\s*true/.test(content);
447
+ const hasHttpOnly = /httpOnly\s*:\s*true/.test(content);
448
+ const hasSameSite = /sameSite\s*:/.test(content);
449
+ if (!hasSecure || !hasHttpOnly || !hasSameSite) {
450
+ weakFiles.push(file);
451
+ }
452
+ }
453
+ if (weakFiles.length === 0)
454
+ return [];
455
+ return [
456
+ {
457
+ id: "WEB_SESSION_WEAK_CONFIG",
458
+ title: "Session configuration missing secure: true, httpOnly: true, or sameSite",
459
+ severity: "HIGH",
460
+ evidence: weakFiles.slice(0, 10).map((f) => `${f}: incomplete session cookie config`),
461
+ requiredActions: [
462
+ "Set secure: true so cookies are only sent over HTTPS.",
463
+ "Set httpOnly: true to prevent JavaScript access to session cookies (mitigates XSS theft).",
464
+ "Set sameSite: 'strict' or 'lax' to prevent CSRF attacks.",
465
+ "Also set a short maxAge (e.g. 15–60 minutes for sensitive sessions) and regenerate the session ID after login."
466
+ ]
467
+ }
468
+ ];
469
+ }
470
+ // ---------------------------------------------------------------------------
471
+ // 16. WEB_DANGLING_MARKUP — user input reflected in HTML attribute values
472
+ // ---------------------------------------------------------------------------
473
+ async function checkDanglingMarkup() {
474
+ const hits = await searchRepo({
475
+ query: String.raw `(?:res\.send\s*\(\s*['"][^'"]*<[a-z]+[^>]*(?:src|href|action)\s*=\s*['"][^'"]*\$\{|ejs\.render[^)]*\{[^}]*(?:req\.|body\.|params\.|query\.))`,
476
+ isRegex: true,
477
+ maxMatches: 200
478
+ });
479
+ if (hits.length === 0)
480
+ return [];
481
+ return [
482
+ {
483
+ id: "WEB_DANGLING_MARKUP",
484
+ title: "User input reflected in HTML attribute value — dangling markup injection risk",
485
+ severity: "HIGH",
486
+ evidence: hits.slice(0, 15).map((m) => `${m.file}:${m.line}:${m.preview}`),
487
+ requiredActions: [
488
+ "User input reflected in HTML attribute value — dangling markup injection enables data exfiltration (CWE-79/CWE-116).",
489
+ "Never interpolate user-controlled values directly into HTML attribute values.",
490
+ "Use a proper HTML templating engine with context-aware escaping or a sanitizer.",
491
+ "Apply output encoding appropriate to the context (HTML attribute, URL, JS, CSS)."
492
+ ]
493
+ }
494
+ ];
495
+ }
496
+ // ---------------------------------------------------------------------------
497
+ // 17. WEB_POSTMESSAGE_WILDCARD — postMessage with wildcard targetOrigin
498
+ // ---------------------------------------------------------------------------
499
+ async function checkPostMessageWildcard() {
500
+ const hits = await searchRepo({
501
+ query: String.raw `(?:postMessage|parent\.postMessage|window\.postMessage)\s*\([^,)]+,\s*['"]\*['"]`,
502
+ isRegex: true,
503
+ maxMatches: 200
504
+ });
505
+ if (hits.length === 0)
506
+ return [];
507
+ return [
508
+ {
509
+ id: "WEB_POSTMESSAGE_WILDCARD",
510
+ title: "postMessage with wildcard targetOrigin '*' detected",
511
+ severity: "MEDIUM",
512
+ evidence: hits.slice(0, 15).map((m) => `${m.file}:${m.line}:${m.preview}`),
513
+ requiredActions: [
514
+ "postMessage with wildcard targetOrigin '*' — data sent to any listening origin (CWE-346).",
515
+ "Replace '*' with an explicit trusted origin (e.g. 'https://example.com').",
516
+ "Validate the sender's origin in the message receiver with event.origin checks."
517
+ ]
518
+ }
519
+ ];
520
+ }
521
+ // ---------------------------------------------------------------------------
522
+ // 18. WEB_CACHE_POISONING — X-Forwarded-Host or unkeyed header reflected
523
+ // ---------------------------------------------------------------------------
524
+ async function checkCachePoisoningHeaders() {
525
+ const hits = await searchRepo({
526
+ query: String.raw `req\.headers\s*\[\s*['"]x-forwarded-host['"]]|req\.headers\.(?:host|x-forwarded-host|x-original-url)`,
527
+ isRegex: true,
528
+ maxMatches: 200
529
+ });
530
+ const suspicious = hits.filter((m) => !/allowlist|===.*TRUSTED_HOST|ALLOWED_HOSTS/.test(m.preview));
531
+ if (suspicious.length === 0)
532
+ return [];
533
+ return [
534
+ {
535
+ id: "WEB_CACHE_POISONING",
536
+ title: "X-Forwarded-Host or unkeyed header reflected in response — cache poisoning risk",
537
+ severity: "MEDIUM",
538
+ evidence: suspicious.slice(0, 15).map((m) => `${m.file}:${m.line}:${m.preview}`),
539
+ requiredActions: [
540
+ "X-Forwarded-Host or unkeyed header reflected in response — web cache poisoning risk (CWE-444).",
541
+ "Validate X-Forwarded-Host against a strict allowlist of trusted hostnames before use.",
542
+ "Never reflect raw Host or X-Forwarded-Host headers into cached responses (e.g. URLs, redirects, links).",
543
+ "Configure your reverse proxy / CDN to strip or normalise forwarding headers before they reach the app."
544
+ ]
545
+ }
546
+ ];
547
+ }
548
+ // ---------------------------------------------------------------------------
549
+ // 19. WEB_MISSING_SRI — external scripts without Subresource Integrity
550
+ // ---------------------------------------------------------------------------
551
+ async function checkMissingSri() {
552
+ const hits = await searchRepo({
553
+ query: String.raw `<script[^>]+src\s*=\s*['"]https?://(?!localhost|127\.)[^'"]+['"][^>]*>`,
554
+ isRegex: true,
555
+ maxMatches: 200
556
+ });
557
+ const suspicious = hits.filter((m) => !/integrity=/.test(m.preview));
558
+ if (suspicious.length === 0)
559
+ return [];
560
+ return [
561
+ {
562
+ id: "WEB_MISSING_SRI",
563
+ title: "External script loaded without Subresource Integrity (SRI)",
564
+ severity: "MEDIUM",
565
+ evidence: suspicious.slice(0, 15).map((m) => `${m.file}:${m.line}:${m.preview}`),
566
+ requiredActions: [
567
+ "External script loaded without Subresource Integrity (SRI) — CDN compromise risk (CWE-829).",
568
+ "Add integrity and crossorigin attributes to all external <script> tags.",
569
+ "Generate SRI hashes at build time (e.g. using the SRI Hash Generator or webpack-subresource-integrity).",
570
+ "Consider self-hosting critical third-party scripts to eliminate CDN supply-chain risk."
571
+ ]
572
+ }
573
+ ];
574
+ }
575
+ // ---------------------------------------------------------------------------
576
+ // Main export
577
+ // ---------------------------------------------------------------------------
578
+ export async function checkWebNextjs(_) {
579
+ return runAll([
580
+ checkSecurityHeaders,
581
+ checkDangerouslySetInnerHTML,
582
+ checkSsrf,
583
+ checkOpenRedirect,
584
+ checkIdorRisk,
585
+ checkServerActionValidation,
586
+ checkApiRouteAuth,
587
+ checkCorsWildcard,
588
+ checkJwtHardcodedSecret,
589
+ checkRateLimitMissing,
590
+ checkEnvExposedClient,
591
+ checkGraphqlIntrospection,
592
+ checkPathTraversal,
593
+ checkLogPii,
594
+ checkSessionWeakConfig,
595
+ checkDanglingMarkup,
596
+ checkPostMessageWildcard,
597
+ checkCachePoisoningHeaders,
598
+ checkMissingSri
599
+ ]);
76
600
  }