cleartoship 0.4.0

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 (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +264 -0
  3. package/action.yml +154 -0
  4. package/dist/banner.d.ts +1 -0
  5. package/dist/banner.js +14 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +103 -0
  8. package/dist/data/popular.d.ts +17 -0
  9. package/dist/data/popular.js +103 -0
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.js +4 -0
  12. package/dist/report.d.ts +15 -0
  13. package/dist/report.js +317 -0
  14. package/dist/scan.d.ts +24 -0
  15. package/dist/scan.js +81 -0
  16. package/dist/scanners/community.d.ts +2 -0
  17. package/dist/scanners/community.js +155 -0
  18. package/dist/scanners/dependencies.d.ts +21 -0
  19. package/dist/scanners/dependencies.js +549 -0
  20. package/dist/scanners/index.d.ts +8 -0
  21. package/dist/scanners/index.js +13 -0
  22. package/dist/scanners/rls.d.ts +7 -0
  23. package/dist/scanners/rls.js +531 -0
  24. package/dist/scanners/secrets.d.ts +2 -0
  25. package/dist/scanners/secrets.js +439 -0
  26. package/dist/scanners/server-actions.d.ts +2 -0
  27. package/dist/scanners/server-actions.js +466 -0
  28. package/dist/types.d.ts +65 -0
  29. package/dist/types.js +10 -0
  30. package/dist/utils/ast.d.ts +13 -0
  31. package/dist/utils/ast.js +107 -0
  32. package/dist/utils/detect.d.ts +2 -0
  33. package/dist/utils/detect.js +77 -0
  34. package/dist/utils/entropy.d.ts +7 -0
  35. package/dist/utils/entropy.js +19 -0
  36. package/dist/utils/files.d.ts +13 -0
  37. package/dist/utils/files.js +154 -0
  38. package/dist/utils/osv.d.ts +26 -0
  39. package/dist/utils/osv.js +123 -0
  40. package/dist/utils/registry.d.ts +44 -0
  41. package/dist/utils/registry.js +195 -0
  42. package/dist/utils/sql.d.ts +20 -0
  43. package/dist/utils/sql.js +157 -0
  44. package/dist/utils/suppress.d.ts +19 -0
  45. package/dist/utils/suppress.js +56 -0
  46. package/dist/utils/traverse.d.ts +2 -0
  47. package/dist/utils/traverse.js +14 -0
  48. package/dist/vendor/gitleaks/rules.d.ts +13 -0
  49. package/dist/vendor/gitleaks/rules.js +1571 -0
  50. package/dist/vendor/guardvibe/index.d.ts +11 -0
  51. package/dist/vendor/guardvibe/index.js +60 -0
  52. package/dist/vendor/guardvibe/rules/advanced-security.d.ts +2 -0
  53. package/dist/vendor/guardvibe/rules/advanced-security.js +418 -0
  54. package/dist/vendor/guardvibe/rules/ai-host-security.d.ts +2 -0
  55. package/dist/vendor/guardvibe/rules/ai-host-security.js +190 -0
  56. package/dist/vendor/guardvibe/rules/ai-security.d.ts +2 -0
  57. package/dist/vendor/guardvibe/rules/ai-security.js +413 -0
  58. package/dist/vendor/guardvibe/rules/ai-tool-runtime.d.ts +2 -0
  59. package/dist/vendor/guardvibe/rules/ai-tool-runtime.js +194 -0
  60. package/dist/vendor/guardvibe/rules/api-security.d.ts +2 -0
  61. package/dist/vendor/guardvibe/rules/api-security.js +154 -0
  62. package/dist/vendor/guardvibe/rules/auth.d.ts +2 -0
  63. package/dist/vendor/guardvibe/rules/auth.js +222 -0
  64. package/dist/vendor/guardvibe/rules/cicd.d.ts +2 -0
  65. package/dist/vendor/guardvibe/rules/cicd.js +108 -0
  66. package/dist/vendor/guardvibe/rules/core.d.ts +2 -0
  67. package/dist/vendor/guardvibe/rules/core.js +484 -0
  68. package/dist/vendor/guardvibe/rules/cve-versions.d.ts +2 -0
  69. package/dist/vendor/guardvibe/rules/cve-versions.js +1130 -0
  70. package/dist/vendor/guardvibe/rules/database.d.ts +2 -0
  71. package/dist/vendor/guardvibe/rules/database.js +173 -0
  72. package/dist/vendor/guardvibe/rules/deployment.d.ts +2 -0
  73. package/dist/vendor/guardvibe/rules/deployment.js +269 -0
  74. package/dist/vendor/guardvibe/rules/dockerfile.d.ts +2 -0
  75. package/dist/vendor/guardvibe/rules/dockerfile.js +99 -0
  76. package/dist/vendor/guardvibe/rules/firebase.d.ts +2 -0
  77. package/dist/vendor/guardvibe/rules/firebase.js +96 -0
  78. package/dist/vendor/guardvibe/rules/go.d.ts +2 -0
  79. package/dist/vendor/guardvibe/rules/go.js +85 -0
  80. package/dist/vendor/guardvibe/rules/modern-stack.d.ts +2 -0
  81. package/dist/vendor/guardvibe/rules/modern-stack.js +631 -0
  82. package/dist/vendor/guardvibe/rules/nextjs.d.ts +2 -0
  83. package/dist/vendor/guardvibe/rules/nextjs.js +233 -0
  84. package/dist/vendor/guardvibe/rules/other-services.d.ts +2 -0
  85. package/dist/vendor/guardvibe/rules/other-services.js +72 -0
  86. package/dist/vendor/guardvibe/rules/payments.d.ts +2 -0
  87. package/dist/vendor/guardvibe/rules/payments.js +123 -0
  88. package/dist/vendor/guardvibe/rules/react-native.d.ts +2 -0
  89. package/dist/vendor/guardvibe/rules/react-native.js +132 -0
  90. package/dist/vendor/guardvibe/rules/services.d.ts +2 -0
  91. package/dist/vendor/guardvibe/rules/services.js +149 -0
  92. package/dist/vendor/guardvibe/rules/shell.d.ts +2 -0
  93. package/dist/vendor/guardvibe/rules/shell.js +72 -0
  94. package/dist/vendor/guardvibe/rules/sql.d.ts +2 -0
  95. package/dist/vendor/guardvibe/rules/sql.js +60 -0
  96. package/dist/vendor/guardvibe/rules/supply-chain.d.ts +2 -0
  97. package/dist/vendor/guardvibe/rules/supply-chain.js +243 -0
  98. package/dist/vendor/guardvibe/rules/terraform.d.ts +2 -0
  99. package/dist/vendor/guardvibe/rules/terraform.js +84 -0
  100. package/dist/vendor/guardvibe/rules/types.d.ts +14 -0
  101. package/dist/vendor/guardvibe/rules/types.js +11 -0
  102. package/dist/vendor/guardvibe/rules/web-security.d.ts +2 -0
  103. package/dist/vendor/guardvibe/rules/web-security.js +258 -0
  104. package/examples/security.yml +23 -0
  105. package/package.json +70 -0
@@ -0,0 +1,631 @@
1
+ /*
2
+ * Vendored from GuardVibe — https://github.com/goklab/guardvibe
3
+ * Copyright 2026 GokLab. Licensed under the Apache License, Version 2.0.
4
+ * Full licence: LICENSES/guardvibe-Apache-2.0.txt
5
+ *
6
+ * Modifications by ClearToShip: import paths rewritten for this package
7
+ * layout. Rule content is unchanged; rules that duplicate ClearToShip's own
8
+ * AST checks are disabled at runtime in src/scanners/community.ts rather
9
+ * than deleted here, so this file stays a faithful copy of upstream.
10
+ */
11
+ // Security rules for modern vibecoding stack:
12
+ // Zod, file uploads, server-only, webhooks, OAuth, cron,
13
+ // AI SDK, tRPC, Hono, GraphQL
14
+ export const modernStackRules = [
15
+ // =====================================================
16
+ // Zod / Validation
17
+ // =====================================================
18
+ {
19
+ id: "VG960",
20
+ name: "Zod passthrough Allows Mass Assignment",
21
+ severity: "high",
22
+ owasp: "API3:2023 Broken Object Property Level Authorization",
23
+ description: "Using .passthrough() on a Zod schema allows unknown fields to pass through validation. Attackers can inject extra fields (role, isAdmin, price) into the validated object.",
24
+ pattern: /\.passthrough\s*\(\s*\)[\s\S]{0,300}?(?:create|update|insert|upsert|save|set|assign)/gi,
25
+ languages: ["javascript", "typescript"],
26
+ fix: "Use .strict() instead of .passthrough(), or use .strip() (default) which removes unknown fields. Only use .passthrough() when you explicitly need to forward unknown fields.",
27
+ fixCode: '// BAD: allows extra fields\nconst schema = z.object({ name: z.string() }).passthrough();\n\n// GOOD: rejects extra fields\nconst schema = z.object({ name: z.string() }).strict();\n\n// GOOD: strips extra fields (default behavior)\nconst schema = z.object({ name: z.string() });',
28
+ compliance: ["SOC2:CC6.6", "PCI-DSS:Req6.5.1"],
29
+ },
30
+ {
31
+ id: "VG961",
32
+ name: "Input Validation Disabled via z.any() or z.unknown()",
33
+ severity: "medium",
34
+ owasp: "API3:2023 Broken Object Property Level Authorization",
35
+ description: "Using z.any() or z.unknown() for request body/input validation effectively disables validation, allowing any data through.",
36
+ pattern: /\b(?:body|input|data|payload|params)\b\s*[:=]\s*z\.(?:any|unknown)\s*\(\s*\)(?!\s*\.(?:describe|nullish|nullable|optional|catch|default|transform|pipe|refine|superRefine|brand|readonly))/gi,
37
+ languages: ["javascript", "typescript"],
38
+ fix: "Define explicit Zod schemas for all inputs. Use z.object() with specific field types.",
39
+ fixCode: '// BAD: no validation\nconst schema = z.object({ data: z.any() });\n\n// GOOD: explicit validation\nconst schema = z.object({\n name: z.string().min(1).max(200),\n email: z.string().email(),\n});',
40
+ compliance: ["SOC2:CC7.1"],
41
+ },
42
+ // =====================================================
43
+ // File Upload Validation
44
+ // =====================================================
45
+ {
46
+ id: "VG962",
47
+ name: "File Upload Without Type Validation",
48
+ severity: "high",
49
+ owasp: "A04:2023 Unrestricted Resource Consumption",
50
+ description: "File upload handler does not validate file type (MIME type or extension). Attackers can upload executable files, scripts, or malicious content.",
51
+ pattern: /(?:formData\.get\s*\(|req\.file\b|multer\s*\(|busboy|formidable\s*\(|Files?\s*\[)[\s\S]{0,500}?(?:writeFile|putObject|\.upload\s*\(|\.save\s*\(|createBucket|\.put\s*\()(?:(?!mime|type|extension|contentType|allowedTypes|accept|fileFilter|allowedMimeTypes)[\s\S]){0,200}/gi,
52
+ languages: ["javascript", "typescript"],
53
+ fix: "Always validate file types against an allowlist before storing. Check both MIME type and extension.",
54
+ fixCode: '// Validate file type before upload\nconst ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"];\nconst file = formData.get("file") as File;\nif (!ALLOWED_TYPES.includes(file.type)) {\n return new Response("Invalid file type", { status: 400 });\n}\nif (file.size > 5 * 1024 * 1024) {\n return new Response("File too large", { status: 400 });\n}',
55
+ compliance: ["SOC2:CC7.1"],
56
+ },
57
+ {
58
+ id: "VG963",
59
+ name: "File Upload Without Size Limit",
60
+ severity: "medium",
61
+ owasp: "A04:2023 Unrestricted Resource Consumption",
62
+ description: "File upload handler does not enforce a file size limit. Attackers can upload extremely large files to exhaust storage or memory.",
63
+ pattern: /(?:formData\.get\s*\(|req\.file\b|multer\s*\()[\s\S]{0,300}?(?:writeFile|putObject|\.upload\s*\(|\.save\s*\(|\.put\s*\()(?:(?!size|limit|max|MB|GB|bytes|fileSizeLimit)[\s\S]){0,200}/gi,
64
+ languages: ["javascript", "typescript"],
65
+ fix: "Enforce a file size limit before processing uploads. Typical limits: 5MB for images, 50MB for documents.",
66
+ fixCode: '// Check file size before upload\nconst MAX_SIZE = 5 * 1024 * 1024; // 5MB\nconst file = formData.get("file") as File;\nif (file.size > MAX_SIZE) {\n return new Response("File too large (max 5MB)", { status: 400 });\n}',
67
+ compliance: ["SOC2:CC7.1"],
68
+ },
69
+ // =====================================================
70
+ // Server-Only Data Leak Prevention
71
+ // =====================================================
72
+ {
73
+ id: "VG964",
74
+ name: "Server-Only Module Missing in Sensitive File",
75
+ severity: "high",
76
+ owasp: "A01:2025 Broken Access Control",
77
+ description: 'File contains sensitive server-side logic (database queries, secret access) but does not import "server-only". Without this guard, the module can be accidentally imported by a Client Component, leaking server code to the browser bundle.',
78
+ pattern: /^(?=[\s\S]*?(?:from\s+['"]next(?:\/[^'"]*)?['"]|require\s*\(\s*['"]next(?:\/[^'"]*)?['"]))(?![\s\S]*?(?:['"]server-only['"]|['"]use server['"]|['"]use client['"])[\s\S]*?)[\s\S]*?(?:process\.env\.(?!NEXT_PUBLIC_)\w+(?:_KEY|_SECRET|_TOKEN)|(?:prisma|db|supabase)\.(?:query|from|\$queryRaw))/g,
79
+ languages: ["javascript", "typescript"],
80
+ fix: 'Add import "server-only" at the top of files that contain server-side logic.',
81
+ fixCode: '// Add at the very top of server-only modules\nimport "server-only";\n\n// Now this file cannot be imported by Client Components\nexport async function getSecretData() {\n const key = process.env.SECRET_KEY;\n return prisma.user.findMany();\n}',
82
+ compliance: ["SOC2:CC6.1"],
83
+ },
84
+ // =====================================================
85
+ // Webhook Replay Protection
86
+ // =====================================================
87
+ {
88
+ id: "VG965",
89
+ name: "Webhook Missing Timestamp/Replay Check",
90
+ severity: "high",
91
+ owasp: "A01:2025 Broken Access Control",
92
+ description: "Webhook handler verifies signature but does not check the event timestamp. Without a timestamp check, attackers can replay old webhook events indefinitely.",
93
+ pattern: /(?:constructEvent|webhooks\.verify|svix\.verify)\s*\((?:(?!timestamp|age|Date\.now|stale|expired|tolerance|replay|created)[\s\S]){15,}?(?:switch|event\.type|event\.data)/g,
94
+ languages: ["javascript", "typescript"],
95
+ fix: "Check the webhook event timestamp and reject events older than 5 minutes to prevent replay attacks.",
96
+ fixCode: '// After verifying the webhook signature:\nconst eventTime = new Date(event.created * 1000);\nconst now = new Date();\nconst fiveMinutes = 5 * 60 * 1000;\nif (now.getTime() - eventTime.getTime() > fiveMinutes) {\n return new Response("Event too old", { status: 400 });\n}',
97
+ compliance: ["SOC2:CC6.6"],
98
+ },
99
+ // =====================================================
100
+ // OAuth / OIDC Security
101
+ // =====================================================
102
+ {
103
+ id: "VG966",
104
+ name: "OAuth Callback Missing State Parameter",
105
+ severity: "high",
106
+ owasp: "A01:2025 Broken Access Control",
107
+ description: "OAuth callback does not verify the state parameter. Without state verification, the app is vulnerable to CSRF attacks that can link an attacker's account to the victim.",
108
+ pattern: /(?:\/callback|\/auth\/callback|oauth\/callback)[\s\S]*?(?:code\s*=|searchParams\.get\s*\(\s*["']code["']\))(?:(?!state|csrfToken|csrf_token|nonce)[\s\S]){0,300}?(?:token|session|exchange)/gi,
109
+ languages: ["javascript", "typescript"],
110
+ fix: "Always verify the OAuth state parameter against the value stored in the session before exchanging the authorization code.",
111
+ fixCode: '// Verify state parameter in OAuth callback\nconst code = searchParams.get("code");\nconst state = searchParams.get("state");\nconst savedState = cookies().get("oauth_state")?.value;\n\nif (!state || state !== savedState) {\n return new Response("Invalid state", { status: 400 });\n}\n// Now exchange code for token...',
112
+ compliance: ["SOC2:CC6.6"],
113
+ },
114
+ {
115
+ id: "VG967",
116
+ name: "OAuth Without PKCE (Proof Key for Code Exchange)",
117
+ severity: "medium",
118
+ owasp: "A07:2025 Auth Failures",
119
+ description: "OAuth authorization request does not include PKCE parameters (code_challenge, code_verifier). Without PKCE, the authorization code can be intercepted and exchanged by an attacker.",
120
+ pattern: /(?:authorization_endpoint|\/authorize|\/oauth\/authorize)[\s\S]{0,300}?(?:response_type\s*[:=]\s*["']code["']|grant_type\s*[:=]\s*["']authorization_code["'])(?:(?!code_challenge|code_verifier|pkce|PKCE)[\s\S]){0,300}$/gm,
121
+ languages: ["javascript", "typescript"],
122
+ fix: "Include PKCE (code_challenge and code_verifier) in all OAuth authorization code flows.",
123
+ fixCode: '// Generate PKCE parameters\nimport crypto from "node:crypto";\nconst codeVerifier = crypto.randomBytes(32).toString("base64url");\nconst codeChallenge = crypto\n .createHash("sha256")\n .update(codeVerifier)\n .digest("base64url");\n\n// Include in authorization URL\nconst authUrl = `${authEndpoint}?response_type=code&code_challenge=${codeChallenge}&code_challenge_method=S256`;',
124
+ compliance: ["SOC2:CC6.6"],
125
+ },
126
+ // =====================================================
127
+ // Cron Job Security
128
+ // =====================================================
129
+ {
130
+ id: "VG968",
131
+ name: "Cron Endpoint Missing CRON_SECRET Verification",
132
+ severity: "high",
133
+ owasp: "A01:2025 Broken Access Control",
134
+ description: "Vercel cron job endpoint does not verify the CRON_SECRET header. Anyone can trigger the cron job by calling the endpoint directly.",
135
+ pattern: /(?:\/api\/cron|cron)[\s\S]*?export\s+(?:async\s+)?function\s+GET\s*\([^)]*\)\s*\{(?:(?!CRON_SECRET|authorization|Bearer|verifySignature|x-vercel-cron|verifyVercelSignature|verifyQstash|verifyQstashSignature|verifySignatureAppRouter|Receiver|verifyCron)[\s\S]){10,}?(?:prisma|db|supabase|fetch|sql|resend|stripe)\.\w+/g,
136
+ languages: ["javascript", "typescript"],
137
+ fix: "Verify the CRON_SECRET header at the start of every cron endpoint.",
138
+ fixCode: 'export async function GET(request: Request) {\n const authHeader = request.headers.get("authorization");\n if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {\n return new Response("Unauthorized", { status: 401 });\n }\n // ... cron job logic\n}',
139
+ compliance: ["SOC2:CC6.6"],
140
+ },
141
+ // =====================================================
142
+ // AI SDK Specific
143
+ // =====================================================
144
+ {
145
+ id: "VG998",
146
+ name: "OpenAI Client with dangerouslyAllowBrowser",
147
+ severity: "critical",
148
+ owasp: "A07:2025 Sensitive Data Exposure",
149
+ description: "OpenAI client is configured with dangerouslyAllowBrowser: true, which runs in the browser and exposes your API key to anyone. Your API key can be stolen and used to make requests at your expense.",
150
+ pattern: /dangerouslyAllowBrowser\s*:\s*true/g,
151
+ languages: ["javascript", "typescript"],
152
+ fix: "Never run the OpenAI client in the browser. Use an API route or Server Action to proxy AI requests.",
153
+ fixCode: '// BAD: runs in browser, leaks API key\nconst openai = new OpenAI({ dangerouslyAllowBrowser: true });\n\n// GOOD: use an API route\n// app/api/chat/route.ts (server-side)\nimport OpenAI from "openai";\nconst openai = new OpenAI(); // reads OPENAI_API_KEY from env\n\nexport async function POST(req: Request) {\n const { prompt } = await req.json();\n const completion = await openai.chat.completions.create({ ... });\n return Response.json(completion);\n}',
154
+ compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3"],
155
+ },
156
+ {
157
+ id: "VG999",
158
+ name: "AI Request Without maxTokens Limit",
159
+ severity: "medium",
160
+ owasp: "A04:2023 Unrestricted Resource Consumption",
161
+ description: "AI generateText/streamText call does not set maxTokens. Without a limit, a single request can generate unlimited tokens, leading to unexpected costs and potential DoS.",
162
+ pattern: /(?:generateText|streamText)\s*\(\s*\{(?:(?!maxTokens|max_tokens|maxOutputTokens)[\s\S]){20,}?\}\s*\)/g,
163
+ languages: ["javascript", "typescript"],
164
+ fix: "Always set maxTokens to limit response length and control costs.",
165
+ fixCode: 'const result = await generateText({\n model: "anthropic/claude-sonnet-4.6",\n maxTokens: 1024, // always set a limit!\n prompt: userInput,\n});',
166
+ compliance: ["SOC2:CC7.1"],
167
+ },
168
+ {
169
+ id: "VG876",
170
+ name: "AI API Key in Client Environment Variable",
171
+ severity: "critical",
172
+ owasp: "A07:2025 Sensitive Data Exposure",
173
+ description: "AI provider API key is set as a NEXT_PUBLIC_ environment variable or used directly in client-side code. This exposes the key to anyone viewing the page source.",
174
+ pattern: /(?:NEXT_PUBLIC_\w*(?:OPENAI|ANTHROPIC|AI|LLM|GPT|CLAUDE)\w*(?:KEY|TOKEN|SECRET)\s*=|["']use client["'][\s\S]{0,800}?(?:OPENAI_API_KEY|ANTHROPIC_API_KEY|apiKey\s*:\s*process\.env))/gi,
175
+ languages: ["javascript", "typescript", "shell"],
176
+ fix: "AI API keys must only be used server-side. Use AI Gateway with OIDC or Server Actions to proxy AI requests.",
177
+ fixCode: '// Server-side only (API route or Server Action)\nimport { generateText } from "ai";\n\nexport async function POST(req: Request) {\n // API key is read from env server-side only\n const result = await generateText({\n model: "anthropic/claude-sonnet-4.6",\n prompt: (await req.json()).prompt,\n });\n return Response.json({ text: result.text });\n}',
178
+ compliance: ["SOC2:CC6.1"],
179
+ },
180
+ // =====================================================
181
+ // tRPC Security
182
+ // =====================================================
183
+ {
184
+ id: "VG970",
185
+ name: "tRPC Public Procedure Accesses Database",
186
+ severity: "high",
187
+ owasp: "A01:2025 Broken Access Control",
188
+ description: "tRPC publicProcedure accesses the database without authentication. Public procedures are accessible to anyone without authentication.",
189
+ pattern: /publicProcedure[\s\S]{0,300}?(?:\.query|\.mutation)\s*\([\s\S]{0,500}?(?:prisma|db|supabase|ctx\.db|ctx\.prisma)\.\w+/g,
190
+ languages: ["javascript", "typescript"],
191
+ fix: "Use protectedProcedure (or a procedure with auth middleware) for any operation that accesses the database.",
192
+ fixCode: '// BAD: public access to database\nexport const appRouter = router({\n getUsers: publicProcedure.query(async ({ ctx }) => {\n return ctx.db.user.findMany(); // anyone can access!\n }),\n});\n\n// GOOD: require authentication\nexport const appRouter = router({\n getUsers: protectedProcedure.query(async ({ ctx }) => {\n return ctx.db.user.findMany(); // only authenticated users\n }),\n});',
193
+ compliance: ["SOC2:CC6.6", "PCI-DSS:Req6.5.10"],
194
+ },
195
+ {
196
+ id: "VG971",
197
+ name: "tRPC Procedure Missing Input Validation",
198
+ severity: "high",
199
+ owasp: "A03:2025 Injection",
200
+ description: "tRPC mutation or query does not use .input() for validation. Without input validation, user-supplied data goes directly to the handler unvalidated.",
201
+ pattern: /(?:publicProcedure|protectedProcedure)(?![\s\S]{0,50}?\.input\s*\()[\s\S]{0,30}?\.(?:mutation|query)\s*\(/g,
202
+ languages: ["javascript", "typescript"],
203
+ fix: "Always use .input() with a Zod schema to validate tRPC procedure inputs.",
204
+ fixCode: '// BAD: no input validation\nprotectedProcedure.mutation(async ({ ctx, input }) => { ... });\n\n// GOOD: validate with Zod\nprotectedProcedure\n .input(z.object({ id: z.string().uuid(), title: z.string().min(1) }))\n .mutation(async ({ ctx, input }) => {\n await ctx.db.post.update({ where: { id: input.id }, data: { title: input.title } });\n });',
205
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
206
+ },
207
+ // =====================================================
208
+ // Hono / Elysia Security
209
+ // =====================================================
210
+ {
211
+ id: "VG972",
212
+ name: "Hono Route Without Authentication Middleware",
213
+ severity: "high",
214
+ owasp: "A01:2025 Broken Access Control",
215
+ description: "Hono API route accesses database or performs mutations without authentication middleware. Hono routes are publicly accessible by default.",
216
+ pattern: /app\.(?:get|post|put|delete|patch)\s*\(\s*['"]\/api\/[^'"]+['"]\s*,\s*(?:async\s+)?\(\s*c\s*\)\s*=>[\s\S]{0,500}?(?:prisma|db|supabase|sql|drizzle)\.\w+/g,
217
+ languages: ["javascript", "typescript"],
218
+ fix: "Add authentication middleware to Hono routes that access data.",
219
+ fixCode: '// Add auth middleware\nimport { bearerAuth } from "hono/bearer-auth";\n\napp.use("/api/*", bearerAuth({ token: process.env.API_TOKEN! }));\n\n// Or custom auth\napp.use("/api/*", async (c, next) => {\n const session = await verifySession(c.req.header("Authorization"));\n if (!session) return c.json({ error: "Unauthorized" }, 401);\n c.set("user", session.user);\n await next();\n});',
220
+ compliance: ["SOC2:CC6.6"],
221
+ },
222
+ {
223
+ id: "VG973",
224
+ name: "Hono CORS Wildcard",
225
+ severity: "high",
226
+ owasp: "A05:2025 Security Misconfiguration",
227
+ description: "Hono app uses cors() with wildcard origin, allowing any website to make requests to your API. (When combined with credentials:true this is the account-takeover-grade CVE-2026-54290 case — flagged separately by VG1094.)",
228
+ pattern: /cors\s*\(\s*\{(?![\s\S]{0,400}?credentials\s*:\s*true)[\s\S]{0,200}?origin\s*:\s*['"]\*['"]/g,
229
+ languages: ["javascript", "typescript"],
230
+ fix: "Set specific allowed origins in Hono CORS configuration.",
231
+ fixCode: 'import { cors } from "hono/cors";\n\napp.use("/*", cors({\n origin: ["https://myapp.com", "https://staging.myapp.com"],\n}));',
232
+ compliance: ["SOC2:CC6.6"],
233
+ },
234
+ {
235
+ id: "VG1094",
236
+ name: "CORS Origin Reflection With Credentials (CVE-2026-54290)",
237
+ severity: "high",
238
+ owasp: "A05:2025 Security Misconfiguration",
239
+ description: "cors() is configured with credentials:true together with a reflected or wildcard origin — origin:'*', origin:true, an arrow function that returns its origin argument unchanged (origin: (o) => o), OR no origin key at all (the middleware default reflects/wildcards). Any of these echoes an arbitrary request Origin back with Access-Control-Allow-Credentials:true, so any website can make authenticated cross-origin requests on the victim's behalf (account-takeover-grade CSRF). This is exactly the misconfiguration that made Hono CVE-2026-54290 exploitable, and it is dangerous on any CORS middleware (Hono, Express). An explicit origin allowlist (origin: ['https://app.example.com']) with credentials:true is NOT flagged. VG973 covers the wildcard-without-credentials case.",
240
+ pattern: /cors\s*\(\s*\{(?=[\s\S]{0,400}?credentials\s*:\s*true)(?:[\s\S]{0,400}?origin\s*:\s*(?:['"]\*['"]|true\b|\(\s*(\w+)\s*\)\s*=>\s*\1\b)|(?![\s\S]{0,400}?\borigin\s*:)[\s\S]{0,200}?credentials\s*:\s*true)/g,
241
+ languages: ["javascript", "typescript"],
242
+ fix: "Never combine credentials:true with a reflected/wildcard origin or an omitted origin. Pass an explicit allowlist of trusted origins, or validate the incoming origin against an allowlist before returning it.",
243
+ fixCode: 'import { cors } from "hono/cors";\n\nconst ALLOWED = new Set(["https://myapp.com", "https://app.myapp.com"]);\napp.use("/api/*", cors({\n origin: (origin) => (ALLOWED.has(origin) ? origin : null),\n credentials: true,\n}));',
244
+ compliance: ["SOC2:CC6.1", "SOC2:CC6.6", "PCI-DSS:Req6.2"],
245
+ },
246
+ // =====================================================
247
+ // GraphQL Security
248
+ // =====================================================
249
+ {
250
+ id: "VG974",
251
+ name: "GraphQL Introspection Enabled in Production",
252
+ severity: "medium",
253
+ owasp: "A05:2025 Security Misconfiguration",
254
+ description: "GraphQL introspection is enabled, exposing the entire schema including internal types, mutations, and field descriptions. Attackers can use this to map your API surface.",
255
+ pattern: /(?:introspection\s*:\s*true|enableIntrospection|ApolloServer|createYoga|createHandler)\s*\([\s\S]{0,500}?(?:(?!introspection\s*:\s*false)[\s\S]){0,300}\)/g,
256
+ languages: ["javascript", "typescript"],
257
+ fix: "Disable introspection in production: introspection: process.env.NODE_ENV !== 'production'",
258
+ fixCode: '// Disable introspection in production\nconst server = new ApolloServer({\n typeDefs,\n resolvers,\n introspection: process.env.NODE_ENV !== "production",\n});',
259
+ compliance: ["SOC2:CC6.1"],
260
+ },
261
+ {
262
+ id: "VG975",
263
+ name: "GraphQL Query Without Depth Limiting",
264
+ severity: "high",
265
+ owasp: "A04:2023 Unrestricted Resource Consumption",
266
+ description: "GraphQL server does not limit query depth. Attackers can send deeply nested queries (e.g., user.posts.author.posts.author...) to cause exponential database load and crash the server.",
267
+ pattern: /(?:ApolloServer|createYoga|createHandler|graphqlHTTP)\s*\(\s*\{(?:(?!depthLimit|maxDepth|queryDepth|complexityLimit|validationRules)[\s\S]){10,}?\}/g,
268
+ languages: ["javascript", "typescript"],
269
+ fix: "Add query depth limiting to prevent deeply nested query attacks.",
270
+ fixCode: '// Add depth limiting\nimport depthLimit from "graphql-depth-limit";\n\nconst server = new ApolloServer({\n typeDefs,\n resolvers,\n validationRules: [depthLimit(5)], // max 5 levels deep\n});',
271
+ compliance: ["SOC2:CC7.1"],
272
+ },
273
+ {
274
+ id: "VG976",
275
+ name: "GraphQL Resolver Without Authorization",
276
+ severity: "high",
277
+ owasp: "A01:2025 Broken Access Control",
278
+ description: "GraphQL resolver accesses the database without checking user authentication or authorization from the context.",
279
+ pattern: /(?:Query|Mutation)\s*[:=]\s*\{[\s\S]{0,300}?(?:async\s+)?(?:\w+)\s*[:=]\s*(?:async\s+)?(?:\([^)]*\)|[^=]*=>)\s*[\s\S]{0,200}?(?:prisma|db|supabase|sql)\.\w+(?:(?!context\.user|ctx\.user|context\.userId|ctx\.userId|requireAuth|isAuthenticated)[\s\S]){0,300}?\}/g,
280
+ languages: ["javascript", "typescript"],
281
+ fix: "Check authentication in every resolver that accesses data.",
282
+ fixCode: '// Check auth in resolvers\nconst resolvers = {\n Query: {\n users: async (_, args, context) => {\n if (!context.user) throw new Error("Unauthorized");\n return prisma.user.findMany();\n },\n },\n};',
283
+ compliance: ["SOC2:CC6.6"],
284
+ },
285
+ // =====================================================
286
+ // CSP (Content Security Policy)
287
+ // =====================================================
288
+ {
289
+ id: "VG977",
290
+ name: "Missing Content-Security-Policy Header",
291
+ severity: "high",
292
+ owasp: "A05:2025 Security Misconfiguration",
293
+ description: "Next.js app does not set a Content-Security-Policy header. CSP is the strongest defense against XSS — without it, injected scripts run freely in your users' browsers.",
294
+ pattern: /(?:async\s+)?headers\s*\(\s*\)(?=[\s\S]*(?:X-Frame-Options|Strict-Transport-Security|X-Content-Type-Options))(?![\s\S]*Content-Security-Policy)/g,
295
+ languages: ["javascript", "typescript"],
296
+ fix: "Add a Content-Security-Policy header in next.config.ts headers().",
297
+ fixCode: "// next.config.ts\nasync headers() {\n return [{\n source: '/(.*)',\n headers: [{\n key: 'Content-Security-Policy',\n value: \"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;\"\n }]\n }];\n}",
298
+ compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.5.7"],
299
+ },
300
+ {
301
+ id: "VG978",
302
+ name: "CSP Contains unsafe-inline or unsafe-" + "eval",
303
+ severity: "high",
304
+ owasp: "A05:2025 Security Misconfiguration",
305
+ description: "Content-Security-Policy uses 'unsafe-inline' or 'unsafe-eval' for script-src. This defeats the purpose of CSP — inline scripts and dynamic code execution will still work, allowing XSS attacks.",
306
+ pattern: /Content-Security-Policy[\s\S]{0,300}?script-src[\s\S]{0,200}?(?:'unsafe-inline'|'unsafe-eval')/gi,
307
+ languages: ["javascript", "typescript"],
308
+ fix: "Remove 'unsafe-inline' and 'unsafe-eval' from script-src. Use nonces or hashes for inline scripts instead.",
309
+ fixCode: "// Use nonces instead of unsafe-inline\n// script-src 'self' 'nonce-${nonce}';\n// Or use strict-dynamic for modern browsers",
310
+ compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.5.7"],
311
+ },
312
+ // =====================================================
313
+ // next/dynamic ssr:false data leak
314
+ // =====================================================
315
+ {
316
+ id: "VG979",
317
+ name: "Server Data Passed to Client-Only Dynamic Component",
318
+ severity: "high",
319
+ owasp: "A01:2025 Broken Access Control",
320
+ description: "A next/dynamic component with ssr: false receives props containing sensitive data. The props are serialized into the HTML payload and visible to anyone viewing page source.",
321
+ pattern: /dynamic\s*\(\s*\(\)\s*=>\s*import\s*\([\s\S]{0,200}?\{\s*ssr\s*:\s*false\s*\}[\s\S]{0,300}?(?:secret|token|apiKey|password|privateKey|internalId|ssn|creditCard)\s*[=:]/gi,
322
+ languages: ["javascript", "typescript"],
323
+ fix: "Never pass sensitive data as props to ssr: false components. Fetch sensitive data inside the client component using an API call.",
324
+ fixCode: '// BAD: server data leaked in HTML payload\nconst Chart = dynamic(() => import("./Chart"), { ssr: false });\n<Chart data={secretData} />\n\n// GOOD: fetch data client-side\nconst Chart = dynamic(() => import("./Chart"), { ssr: false });\n<Chart /> // Chart fetches its own data via API',
325
+ compliance: ["SOC2:CC6.1"],
326
+ },
327
+ // =====================================================
328
+ // Email Template Injection
329
+ // =====================================================
330
+ {
331
+ id: "VG980",
332
+ name: "Email HTML Injection via User Input",
333
+ severity: "high",
334
+ owasp: "A02:2025 Injection",
335
+ description: "User input is interpolated directly into an HTML email template without sanitization. Attackers can inject HTML/CSS to create phishing content or redirect links within emails sent from your domain.",
336
+ pattern: /(?:resend|sendgrid|nodemailer|transporter)[\s\S]{0,500}?html\s*:\s*(?:`[^`]*\$\{(?:.*?(?:name|email|user|input|body|message|comment|title|content))[\s\S]{0,100}?`|['"][^'"]*['"]\s*\+\s*(?:name|email|user|input|body|message|comment|title|content))/gi,
337
+ languages: ["javascript", "typescript"],
338
+ fix: "Sanitize user input before embedding in HTML emails. Escape HTML entities manually.",
339
+ fixCode: '// Escape HTML entities before embedding in email\nfunction escapeHtml(str: string) {\n return str.replace(/&/g, "&amp;").replace(/</g, "&lt;")\n .replace(/>/g, "&gt;").replace(/"/g, "&quot;");\n}\n\nawait resend.emails.send({\n html: `<p>Hello ${escapeHtml(userName)}</p>`,\n});',
340
+ compliance: ["SOC2:CC7.1"],
341
+ },
342
+ // =====================================================
343
+ // Uploadthing Security
344
+ // =====================================================
345
+ {
346
+ id: "VG981",
347
+ name: "Uploadthing Missing Auth in Middleware",
348
+ severity: "high",
349
+ owasp: "A01:2025 Broken Access Control",
350
+ description: "Uploadthing file router does not check authentication in the middleware function. Anyone can upload files to your storage.",
351
+ pattern: /\.middleware\s*\(\s*(?:async\s+)?\(\s*\{?\s*(?:req|request)?\s*\}?\s*\)\s*=>\s*\{?(?:(?!auth\s*\(|getServerSession|currentUser|getUser|session|userId|clerkClient|getToken)[\s\S]){5,}?(?:return|files|metadata)/g,
352
+ languages: ["javascript", "typescript"],
353
+ fix: "Always verify authentication in Uploadthing middleware before allowing uploads.",
354
+ fixCode: 'import { auth } from "@clerk/nextjs/server";\n\n.middleware(async ({ req }) => {\n const { userId } = await auth();\n if (!userId) throw new Error("Unauthorized");\n return { userId };\n})',
355
+ compliance: ["SOC2:CC6.6"],
356
+ },
357
+ {
358
+ id: "VG982",
359
+ name: "Uploadthing Missing File Type/Size Config",
360
+ severity: "medium",
361
+ owasp: "A04:2023 Unrestricted Resource Consumption",
362
+ description: "Uploadthing file route uses f() without specifying file type restrictions or size limits.",
363
+ pattern: /f\s*\(\s*\{[\s\S]{0,50}?\}\s*\)(?:(?!maxFileSize|maxFileCount|image|pdf|video|audio|text|blob)[\s\S]){5,}?\.middleware/g,
364
+ languages: ["javascript", "typescript"],
365
+ fix: "Always specify allowed file types and size limits in Uploadthing route config.",
366
+ fixCode: '// Specify file type and size limits\nf({ image: { maxFileSize: "4MB", maxFileCount: 5 } })\n// Or for documents:\nf({ pdf: { maxFileSize: "16MB", maxFileCount: 1 } })',
367
+ compliance: ["SOC2:CC7.1"],
368
+ },
369
+ // =====================================================
370
+ // Turso / LibSQL Security
371
+ // =====================================================
372
+ {
373
+ id: "VG983",
374
+ name: "Turso Database URL Client Exposure",
375
+ severity: "critical",
376
+ owasp: "A07:2025 Sensitive Data Exposure",
377
+ description: "Turso/LibSQL database URL or auth token is accessed in client-side code or exposed via NEXT_PUBLIC_ prefix.",
378
+ pattern: /(?:["']use client["'][\s\S]{0,500}?(?:TURSO_DATABASE_URL|TURSO_AUTH_TOKEN|LIBSQL)|NEXT_PUBLIC_\w*(?:TURSO|LIBSQL)\w*(?:URL|TOKEN|AUTH)\s*=)/gi,
379
+ languages: ["javascript", "typescript", "shell"],
380
+ fix: "Turso credentials must only be used server-side. Never prefix with NEXT_PUBLIC_.",
381
+ fixCode: '// Server-side only\nimport { createClient } from "@libsql/client";\n\nconst db = createClient({\n url: process.env.TURSO_DATABASE_URL!,\n authToken: process.env.TURSO_AUTH_TOKEN!,\n});',
382
+ compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3"],
383
+ },
384
+ {
385
+ id: "VG984",
386
+ name: "Turso/LibSQL Raw SQL Interpolation",
387
+ severity: "critical",
388
+ owasp: "A02:2025 Injection",
389
+ description: "Template literal interpolation used in Turso/LibSQL call. This allows SQL injection attacks.",
390
+ pattern: /(?:db|client|turso|libsql)\.execute\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+)/gi,
391
+ languages: ["javascript", "typescript"],
392
+ fix: "Use parameterized queries with args array instead of string interpolation.",
393
+ fixCode: '// GOOD: parameterized query\nawait db.execute({\n sql: "SELECT * FROM users WHERE id = ?",\n args: [userId],\n});',
394
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
395
+ },
396
+ // =====================================================
397
+ // Convex Security
398
+ // =====================================================
399
+ {
400
+ id: "VG985",
401
+ name: "Convex Query/Mutation Without Authentication",
402
+ severity: "high",
403
+ owasp: "A01:2025 Broken Access Control",
404
+ description: "Convex query or mutation accesses data without verifying user identity. Convex functions are callable by any client by default.",
405
+ pattern: /(?:query|mutation)\s*\(\s*\{[\s\S]{0,300}?handler\s*:\s*(?:async\s+)?\(\s*(?:ctx|context)\s*(?:,\s*args)?\s*\)\s*=>[\s\S]{0,300}?ctx\.db\.(?:get|query|insert|patch|delete|replace)(?:(?!ctx\.auth\.getUserIdentity|identity|userId|user)[\s\S]){0,200}/g,
406
+ languages: ["javascript", "typescript"],
407
+ fix: "Check user identity at the start of every Convex function that accesses data.",
408
+ fixCode: 'export const getMyItems = query({\n handler: async (ctx) => {\n const identity = await ctx.auth.getUserIdentity();\n if (!identity) throw new Error("Unauthorized");\n return ctx.db.query("items").collect();\n },\n});',
409
+ compliance: ["SOC2:CC6.6"],
410
+ },
411
+ {
412
+ id: "VG986",
413
+ name: "Convex Internal Function Exposed as Public",
414
+ severity: "high",
415
+ owasp: "A01:2025 Broken Access Control",
416
+ description: "A function that should be internal (admin, migrate, seed, cleanup) is exported as a public query/mutation instead of internalQuery/internalMutation.",
417
+ pattern: /export\s+(?:const|default)\s+(?:admin|internal|migrate|seed|cleanup|background|cron|scheduled)\w*\s*=\s*(?:query|mutation)\s*\(/gi,
418
+ languages: ["javascript", "typescript"],
419
+ fix: "Use internalQuery/internalMutation for functions that should not be callable from clients.",
420
+ fixCode: '// Use internalMutation for admin functions\nimport { internalMutation } from "./_generated/server";\nexport const adminDeleteUser = internalMutation({\n handler: async (ctx, args) => { ... },\n});',
421
+ compliance: ["SOC2:CC6.6"],
422
+ },
423
+ {
424
+ id: "VG987",
425
+ name: "Convex HTTP Action Without Auth",
426
+ severity: "high",
427
+ owasp: "A01:2025 Broken Access Control",
428
+ description: "Convex httpAction processes requests without authentication. HTTP actions are publicly accessible endpoints.",
429
+ pattern: /httpAction\s*\(\s*(?:async\s+)?\(\s*(?:ctx|context)\s*,\s*(?:request|req)\s*\)\s*=>[\s\S]{0,300}?(?:ctx\.runMutation|ctx\.runQuery|ctx\.runAction)(?:(?!auth|token|bearer|verify|signature|secret)[\s\S]){0,200}/gi,
430
+ languages: ["javascript", "typescript"],
431
+ fix: "Verify authentication in HTTP actions before processing requests.",
432
+ fixCode: 'export const webhook = httpAction(async (ctx, request) => {\n const token = request.headers.get("Authorization")?.replace("Bearer ", "");\n if (!token) return new Response("Unauthorized", { status: 401 });\n await ctx.runMutation(...);\n});',
433
+ compliance: ["SOC2:CC6.6"],
434
+ },
435
+ {
436
+ id: "VG988",
437
+ name: "GraphQL Batched Query Abuse",
438
+ severity: "medium",
439
+ owasp: "A04:2023 Unrestricted Resource Consumption",
440
+ description: "GraphQL server has query batching enabled (allowBatchedHttpRequests: true). Attackers can bypass rate limiting by sending hundreds of queries in a single HTTP request — each query counts as one request but executes independently. Also enables brute-force attacks against authentication mutations.",
441
+ pattern: /(?:allowBatchedHttpRequests|batching)\s*:\s*true/gi,
442
+ languages: ["javascript", "typescript"],
443
+ fix: "Disable query batching or add per-query rate limiting. If batching is needed, limit the batch size.",
444
+ fixCode: '// Disable batching\nconst server = new ApolloServer({\n typeDefs,\n resolvers,\n allowBatchedHttpRequests: false,\n});\n\n// Or limit batch size if needed\nconst server = new ApolloServer({\n allowBatchedHttpRequests: true,\n plugins: [ApolloServerPluginBatchHttpLink({ maxBatchSize: 5 })],\n});',
445
+ compliance: ["SOC2:CC7.1"],
446
+ },
447
+ {
448
+ id: "VG989",
449
+ name: "Rate Limit Bypass via X-Forwarded-For Trust",
450
+ severity: "high",
451
+ owasp: "A04:2023 Unrestricted Resource Consumption",
452
+ description: "Rate limiter uses X-Forwarded-For or X-Real-IP header as the client identifier without a trusted proxy configuration. Attackers can bypass rate limits by sending different spoofed IP addresses in each request.",
453
+ pattern: /(?:req\.headers\[['"]x-forwarded-for['"]\]|req\.header\s*\(\s*['"]x-forwarded-for['"]\)|req\.ip|request\.headers\.get\s*\(\s*['"]x-forwarded-for['"]\))[\s\S]{0,200}?(?:rateLimit|limiter|throttle|identifier|key\s*:|keyGenerator)/gi,
454
+ languages: ["javascript", "typescript"],
455
+ fix: "Configure your rate limiter to use the real client IP from a trusted proxy. Set Express trust proxy or use platform-provided IP (e.g., Vercel's x-real-ip behind their proxy).",
456
+ fixCode: '// Express: trust only your reverse proxy\napp.set("trust proxy", 1); // trust first proxy\n\n// Rate limiter: use req.ip (respects trust proxy)\nimport rateLimit from "express-rate-limit";\nconst limiter = rateLimit({\n keyGenerator: (req) => req.ip, // uses trusted proxy chain\n max: 100,\n windowMs: 15 * 60 * 1000,\n});',
457
+ compliance: ["SOC2:CC7.1"],
458
+ },
459
+ {
460
+ id: "VG990",
461
+ name: "SVG File Upload Without Content Sanitization",
462
+ severity: "critical",
463
+ owasp: "A07:2025 Cross-Site Scripting",
464
+ description: "File upload accepts SVG files but does not scan or sanitize SVG content. SVG files can contain embedded <script> tags, event handlers (onload, onclick), and external resource references that execute JavaScript when the SVG is rendered in a browser.",
465
+ pattern: /(?:(?:allowedMimeTypes|accept|mimeTypes|fileTypes|allowedTypes|contentType)\s*[:=]\s*(?:\[[\s\S]*?|['"`])[\s\S]{0,100}?(?:svg|image\/svg|\.svg))/gi,
466
+ languages: ["javascript", "typescript"],
467
+ fix: "Either reject SVG uploads entirely or sanitize SVG content by stripping script tags, event handlers, and external references. Use a library like DOMPurify with SVG profile.",
468
+ fixCode: '// Option 1: Reject SVGs\nconst ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp"]; // no SVG\n\n// Option 2: Sanitize SVG content\nimport DOMPurify from "dompurify";\nconst cleanSvg = DOMPurify.sanitize(svgContent, {\n USE_PROFILES: { svg: true, svgFilters: true },\n FORBID_TAGS: ["script", "foreignObject"],\n FORBID_ATTR: ["onclick", "onerror", "onload"],\n});',
469
+ compliance: ["SOC2:CC7.1"],
470
+ },
471
+ {
472
+ id: "VG991",
473
+ name: "Markdown Rendered as HTML Without Sanitization",
474
+ severity: "high",
475
+ owasp: "A07:2025 Cross-Site Scripting",
476
+ description: "Markdown library output (marked, showdown, markdown-it, remark) is rendered as HTML without sanitization. Most markdown parsers allow raw HTML by default — user-submitted markdown like `<img onerror=alert(1)>` passes through as executable HTML.",
477
+ pattern: /(?:marked|showdown|markdownIt|markdown-it|unified|remark|rehype)[\s\S]{0,300}?(?:innerHTML|dangerouslySetInnerHTML|v-html|\[innerHTML\]|\.html\s*\(|res\.send)/gi,
478
+ languages: ["javascript", "typescript"],
479
+ fix: "Sanitize markdown HTML output with DOMPurify before rendering, or configure the parser to disable raw HTML.",
480
+ fixCode: '// BAD: unsanitized markdown\n// element.innerHTML = marked.parse(userMarkdown);\n\n// GOOD: sanitize after parsing\nimport DOMPurify from "dompurify";\nimport { marked } from "marked";\nconst html = DOMPurify.sanitize(marked.parse(userMarkdown));\n\n// Or disable HTML in parser\nmarked.setOptions({ sanitize: true });\n// markdown-it: const md = markdownIt({ html: false });',
481
+ compliance: ["SOC2:CC7.1"],
482
+ },
483
+ {
484
+ id: "VG992",
485
+ name: "Rich Text Editor Output Without Sanitization",
486
+ severity: "high",
487
+ owasp: "A07:2025 Cross-Site Scripting",
488
+ description: "WYSIWYG/rich text editor content (TipTap, Draft.js, Slate, Quill, CKEditor, TinyMCE) is rendered via innerHTML or dangerouslySetInnerHTML without sanitization. Editor output is user-controlled HTML that can contain XSS payloads — especially if the editor allows source code editing or paste from external sources.",
489
+ pattern: /(?:editor\.getHTML|getContent|convertToHTML|stateToHTML|serialize|draftToHtml|renderToString)[\s\S]{0,300}?(?:innerHTML|dangerouslySetInnerHTML|v-html|\[innerHTML\])/gi,
490
+ languages: ["javascript", "typescript"],
491
+ fix: "Always sanitize rich text editor output with DOMPurify before rendering, even if the editor has its own sanitization.",
492
+ fixCode: '// BAD: direct editor output rendering\n// <div dangerouslySetInnerHTML={{ __html: editor.getHTML() }} />\n\n// GOOD: sanitize editor output\nimport DOMPurify from "dompurify";\nconst cleanHtml = DOMPurify.sanitize(editor.getHTML(), {\n ALLOWED_TAGS: ["p", "b", "i", "em", "strong", "a", "ul", "ol", "li", "br", "h1", "h2", "h3"],\n ALLOWED_ATTR: ["href", "target", "rel"],\n});\n<div dangerouslySetInnerHTML={{ __html: cleanHtml }} />',
493
+ compliance: ["SOC2:CC7.1"],
494
+ },
495
+ {
496
+ id: "VG993",
497
+ name: "Upload Filename Used Without Sanitization",
498
+ severity: "high",
499
+ owasp: "A01:2025 Broken Access Control",
500
+ description: "User-uploaded file's original filename is used directly for storage without sanitization. Attackers can use directory traversal (../../etc/passwd), null bytes (file.php%00.jpg), double extensions (file.jpg.exe), or Unicode tricks to overwrite files, bypass type checks, or achieve remote code execution.",
501
+ pattern: /(?:file\.name|originalname|req\.file\.originalname|formData\.get\s*\(\s*['"]file['"])(?:(?!randomUUID|uuid|nanoid|sanitize|safeFilename|safeName|allowedExt|allowedExtensions|\.replace\s*\(\s*\/\[)[\s\S]){0,100}?(?:writeFile|createWriteStream|save|upload|putObject|mv\s*\(|rename|storage)/gi,
502
+ languages: ["javascript", "typescript"],
503
+ fix: "Generate a random filename (UUID/nanoid) and validate the extension against an allowlist. Never use the original filename for storage.",
504
+ fixCode: 'import { randomUUID } from "crypto";\nimport path from "path";\n\n// Generate safe filename\nconst ext = path.extname(file.name).toLowerCase();\nconst ALLOWED_EXT = [".jpg", ".jpeg", ".png", ".webp", ".pdf"];\nif (!ALLOWED_EXT.includes(ext)) throw new Error("Invalid file type");\nconst safeName = `${randomUUID()}${ext}`;\nawait fs.writeFile(`/uploads/${safeName}`, buffer);',
505
+ compliance: ["SOC2:CC7.1"],
506
+ },
507
+ {
508
+ id: "VG1000",
509
+ name: "Hono SSE Injection via streamSSE",
510
+ severity: "medium",
511
+ owasp: "A02:2025 Injection",
512
+ description: "Hono's streamSSE() sends Server-Sent Events to clients. If event, id, or retry fields contain unsanitized user input, attackers can inject CR/LF characters to forge SSE messages, hijack event streams, or trigger client-side actions. Related to CVE-2026-29085.",
513
+ pattern: /streamSSE\s*\(/g,
514
+ languages: ["javascript", "typescript"],
515
+ fix: "Sanitize all SSE field values by stripping CR/LF characters (\\r, \\n) before passing them to streamSSE. Never use raw user input in event, id, or retry fields.",
516
+ fixCode: '// Sanitize SSE fields\nfunction sanitizeSSE(value: string): string {\n return value.replace(/[\\r\\n]/g, "");\n}\n\n// Usage with Hono streamSSE\nreturn streamSSE(c, async (stream) => {\n await stream.writeSSE({\n event: sanitizeSSE(eventName),\n data: sanitizeSSE(data),\n id: sanitizeSSE(id),\n });\n});',
517
+ compliance: ["SOC2:CC7.1"],
518
+ },
519
+ {
520
+ id: "VG1003",
521
+ name: "Hono ErrorBoundary XSS via Unsanitized Error Messages",
522
+ severity: "high",
523
+ owasp: "A07:2025 Cross-Site Scripting",
524
+ description: "Hono v4.11.7 öncesi ErrorBoundary bileşeni hata mesajlarını sanitize etmeden render eder. Kullanıcı kaynaklı input bir hata tetiklerse, hata mesajı ham HTML olarak render edilir ve reflected XSS'e yol açar.",
525
+ pattern: /(?:import\s.*from\s+['"]hono\/(?:jsx|components)['"])[\s\S]{0,500}?ErrorBoundary/gi,
526
+ languages: ["javascript", "typescript"],
527
+ fix: "Hono'yu v4.11.7 veya üstüne yükseltin. Yükseltemiyorsanız, ErrorBoundary fallback'inde hata mesajlarını HTML escape edin.",
528
+ fixCode: '// Upgrade: npm install hono@latest\n\n// Veya manuel sanitize:\nimport { ErrorBoundary } from "hono/jsx";\nfunction escapeHtml(s: string) {\n return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");\n}\n<ErrorBoundary fallback={(err) => <p>{escapeHtml(err.message)}</p>}>\n <MyComponent />\n</ErrorBoundary>',
529
+ compliance: ["SOC2:CC7.1"],
530
+ },
531
+ {
532
+ id: "VG1004",
533
+ name: "React Server Action Without Rate Limiting",
534
+ severity: "medium",
535
+ owasp: "API4:2023 Unrestricted Resource Consumption",
536
+ description: "React Server Action veya RSC endpoint'i rate limiting ve request size kontrolü olmadan expose edilmiş. Saldırganlar yüksek hacimli veya büyük boyutlu payload'larla DoS gerçekleştirebilir. CVE-2026-23864 ile ilişkili.",
537
+ pattern: /["']use server["'][\s\S]{0,500}?export\s+(?:async\s+)?function\s+\w+/g,
538
+ languages: ["javascript", "typescript"],
539
+ fix: "Her Server Action'a rate limiting middleware (ör. @upstash/ratelimit) ve request size validasyonu ekleyin.",
540
+ fixCode: '"use server";\nimport { Ratelimit } from "@upstash/ratelimit";\nimport { headers } from "next/headers";\n\nconst ratelimit = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(10, "10s") });\n\nexport async function submitForm(formData: FormData) {\n const ip = (await headers()).get("x-forwarded-for") ?? "127.0.0.1";\n const { success } = await ratelimit.limit(ip);\n if (!success) throw new Error("Too many requests");\n}',
541
+ compliance: ["SOC2:CC7.1"],
542
+ },
543
+ // =====================================================
544
+ // Supabase PostgREST Injection
545
+ // =====================================================
546
+ {
547
+ id: "VG1005",
548
+ name: "Supabase .or() Filter Injection",
549
+ severity: "critical",
550
+ owasp: "A03:2025 Injection",
551
+ description: "User input is interpolated into a Supabase .or() filter string via template literal or concatenation. This is equivalent to SQL injection for PostgREST — attackers can modify filter logic to access unauthorized data.",
552
+ pattern: /\.or\s*\(\s*(?:`[^`]*\$\{(?!(?:sfv|sanitize|escape|validate|encodeURIComponent)\s*\())|\.or\s*\(\s*\w+\s*\)|["'][^"']*["']\s*\+\s*\w+(?:Id|Name|Term|Input)\b[\s\S]{0,100}?\.or\s*\(/gi,
553
+ languages: ["javascript", "typescript"],
554
+ fix: "Never interpolate user input into .or() strings. Use separate .eq() filters or build the filter from validated enum values.",
555
+ fixCode: '// BAD: filter injection\n.or(`sender_id.eq.${userId},receiver_id.eq.${userId}`)\n\n// GOOD: use server-verified auth ID\nconst { data: { user } } = await supabase.auth.getUser();\n.or(`sender_id.eq.${user.id},receiver_id.eq.${user.id}`)\n\n// BEST: use RLS policies instead of client-side filtering',
556
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
557
+ },
558
+ {
559
+ id: "VG1006",
560
+ name: "Supabase select('*') Exposes All Columns",
561
+ severity: "high",
562
+ owasp: "A01:2025 Broken Access Control",
563
+ description: "Supabase query uses select('*') or select() without specifying columns. This returns all table columns including sensitive fields (email, password_hash, internal_notes) to the client.",
564
+ pattern: /\.from\s*\(\s*["'][^"']+["']\s*\)\s*\.select\s*\(\s*(?:["']\*["']|\))/gi,
565
+ languages: ["javascript", "typescript"],
566
+ fix: "Always specify explicit columns: .select('id, name, avatar_url') instead of .select('*').",
567
+ fixCode: '// BAD: returns all columns including sensitive data\nconst { data } = await supabase.from("users").select("*");\n\n// GOOD: only needed columns\nconst { data } = await supabase.from("users").select("id, name, avatar_url");',
568
+ compliance: ["SOC2:CC6.1", "GDPR:Art25"],
569
+ },
570
+ {
571
+ id: "VG1007",
572
+ name: "Supabase Service Role Key Bypasses RLS",
573
+ severity: "critical",
574
+ owasp: "A01:2025 Broken Access Control",
575
+ description: "Server-side Supabase client is initialized with the service_role key, which bypasses all Row Level Security policies. If any route handler is missing an auth check, the entire table is exposed.",
576
+ pattern: /createClient\s*\(\s*[\s\S]{0,100}?(?:SERVICE_ROLE|service_role)/gi,
577
+ languages: ["javascript", "typescript"],
578
+ fix: "Use createServerClient() with per-request auth context, or use anon key with RLS policies. Reserve service_role for admin-only background jobs.",
579
+ fixCode: '// BAD: service role bypasses all RLS\nconst supabase = createClient(url, process.env.SUPABASE_SERVICE_ROLE_KEY!);\n\n// GOOD: per-request client respects RLS\nimport { createServerClient } from "@supabase/ssr";\nconst supabase = createServerClient(url, anonKey, { cookies });',
580
+ compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.5.10", "GDPR:Art32"],
581
+ },
582
+ {
583
+ id: "VG1008",
584
+ name: "Admin Role Elevation Without Authorization Check",
585
+ severity: "critical",
586
+ owasp: "A01:2025 Broken Access Control",
587
+ description: "Code sets a user's role to 'admin' without verifying that the caller is already an admin. Any authenticated user could escalate their own privileges.",
588
+ pattern: /(?:\.update\s*\([\s\S]{0,200}?(?:role|is_?admin|permission)\s*[:=]\s*["'](?:admin|superadmin|owner)["']|\.update\s*\([\s\S]{0,200}?(?:isAdmin|is_admin)\s*[:=]\s*true)/gi,
589
+ languages: ["javascript", "typescript"],
590
+ fix: "Always verify the caller has admin privileges before allowing role elevation.",
591
+ fixCode: '// GOOD: verify caller is admin first\nexport async function setRole(targetUserId: string, newRole: string) {\n const { userId } = await auth();\n const caller = await db.user.findUnique({ where: { id: userId } });\n if (caller.role !== "admin") throw new Error("Forbidden");\n await db.user.update({ where: { id: targetUserId }, data: { role: newRole } });\n}',
592
+ compliance: ["SOC2:CC6.6", "PCI-DSS:Req6.5.10"],
593
+ },
594
+ {
595
+ id: "VG1009",
596
+ name: "Supabase ilike/like Pattern Injection",
597
+ severity: "high",
598
+ owasp: "A03:2025 Injection",
599
+ description: "User input is interpolated into a Supabase .ilike() or .like() pattern via template literal or concatenation. Special characters (%, _, \\) in the input can modify the pattern, and PostgREST filter syntax characters can break the filter entirely.",
600
+ pattern: /\.(?:ilike|like)\s*\(\s*["'][^"']+["']\s*,\s*(?:`[^`]*\$\{|["'][^"']*["']\s*\+\s*(?!\s*["']))/gi,
601
+ languages: ["javascript", "typescript"],
602
+ fix: "Escape special pattern characters (%, _, \\) in user input before using in ilike/like filters.",
603
+ fixCode: '// BAD: pattern injection\n.ilike("name", `%${query}%`)\n\n// GOOD: escape special characters\nfunction escapeLike(s: string) {\n return s.replace(/[%_\\\\]/g, "\\\\$&");\n}\n.ilike("name", `%${escapeLike(query)}%`)',
604
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
605
+ },
606
+ {
607
+ id: "VG1010",
608
+ name: "React Server Action Without Input Validation (React2Shell Risk)",
609
+ severity: "critical",
610
+ owasp: "A02:2025 Injection",
611
+ description: 'Server Action in a "use server" file accepts arguments and passes them to database operations, file system calls, or shell commands without schema validation. CVE-2025-55182 (React2Shell) exploits the React Flight protocol to send crafted payloads to Server Actions — any unvalidated action argument is a direct RCE/injection vector. CVSS 10.0.',
612
+ pattern: /["']use server["'][\s\S]{0,3000}?export\s+async\s+function\s+\w+\s*\([^)]+\)(?![\s\S]{0,500}?(?:\.parse\s*\(|\.safeParse\s*\(|valibot|superstruct|arktype|yup\.\w+\.validate|assertIs))[\s\S]{0,500}?(?:prisma|db\.|sql`|supabase|exec|spawn|readFile|writeFile|fetch\()/g,
613
+ languages: ["javascript", "typescript"],
614
+ fix: 'Validate ALL Server Action arguments with zod or a schema library at the top of every "use server" function. Never trust the deserialized payload.',
615
+ fixCode: '// BAD: unvalidated Server Action argument\n"use server";\nexport async function updateUser(data: any) {\n await prisma.user.update({ where: { id: data.id }, data });\n}\n\n// GOOD: validate with zod before any operation\n"use server";\nimport { z } from "zod";\nconst schema = z.object({ id: z.string().uuid(), name: z.string().max(100) });\nexport async function updateUser(raw: unknown) {\n const data = schema.parse(raw);\n await prisma.user.update({ where: { id: data.id }, data: { name: data.name } });\n}',
616
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.2", "PCI-DSS:Req6.5.1"],
617
+ },
618
+ {
619
+ id: "VG1072",
620
+ name: "Hono setCookie sameSite/priority From User Input (CVE-2026-47675)",
621
+ severity: "medium",
622
+ owasp: "A02:2025 Injection",
623
+ description: "Hono's setCookie / setSignedCookie helper accepts a config object with sameSite and priority attributes. When user-controlled input (c.req.X, c.get(), req.body, query/params, formData, etc.) flows into one of those attributes, an attacker can break out of the intended enum value and inject extra cookie attributes — flipping Secure or HttpOnly off, downgrading SameSite from Strict to None, or appending a duplicate Set-Cookie segment that overrides the legitimate one. CVE-2026-47675 documents the attribute-injection bypass in the affected hono line; the safe pattern always passes a literal enum value or maps the input through a strict allowlist first. Distinct from VG924, which is the older CRLF-injection CVE-2026-29086 in the cookie value itself.",
624
+ pattern: /\b(?:setCookie|setSignedCookie)\s*\([\s\S]{0,300}?(?:\bsameSite\b|\bpriority\b)\s*:\s*(?:c\.req\.|c\.get\(|req\.|request\.|input\.|params\.|body\.|query\.|searchParams|formData|ctx\.|args\.)/g,
625
+ languages: ["javascript", "typescript"],
626
+ fix: "Never pass user-controlled input to the sameSite or priority cookie attributes. Use a literal enum value ('Strict'/'Lax'/'None' for sameSite, 'Low'/'Medium'/'High' for priority) or map the user input through a strict allowlist first. Also upgrade hono to the patched release.",
627
+ fixCode: "// BAD — user input flows into sameSite attribute\nsetCookie(c, 'session', token, {\n sameSite: c.req.query('site_mode'), // attacker chooses\n httpOnly: true,\n});\n\n// GOOD — literal value, never user input\nsetCookie(c, 'session', token, {\n sameSite: 'Strict',\n httpOnly: true,\n secure: true,\n});\n\n// GOOD — allowlist if you really need to vary\nconst SAFE_SAMESITE = { strict: 'Strict', lax: 'Lax' } as const;\nconst mode = SAFE_SAMESITE[c.req.query('site_mode') as keyof typeof SAFE_SAMESITE] ?? 'Strict';\nsetCookie(c, 'session', token, { sameSite: mode, httpOnly: true });",
628
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.10"],
629
+ exploit: "Attacker submits a query parameter whose value contains cookie-attribute terminators (e.g. `Lax; Secure=false; HttpOnly=false` or `Strict\\r\\nSet-Cookie: tracker=x`). The helper renders the value into the Set-Cookie header verbatim — the browser then either downgrades the cookie's flags or treats the injected segment as a separate Set-Cookie, hijacking the session.",
630
+ },
631
+ ];