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.
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/action.yml +154 -0
- package/dist/banner.d.ts +1 -0
- package/dist/banner.js +14 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +103 -0
- package/dist/data/popular.d.ts +17 -0
- package/dist/data/popular.js +103 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/report.d.ts +15 -0
- package/dist/report.js +317 -0
- package/dist/scan.d.ts +24 -0
- package/dist/scan.js +81 -0
- package/dist/scanners/community.d.ts +2 -0
- package/dist/scanners/community.js +155 -0
- package/dist/scanners/dependencies.d.ts +21 -0
- package/dist/scanners/dependencies.js +549 -0
- package/dist/scanners/index.d.ts +8 -0
- package/dist/scanners/index.js +13 -0
- package/dist/scanners/rls.d.ts +7 -0
- package/dist/scanners/rls.js +531 -0
- package/dist/scanners/secrets.d.ts +2 -0
- package/dist/scanners/secrets.js +439 -0
- package/dist/scanners/server-actions.d.ts +2 -0
- package/dist/scanners/server-actions.js +466 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +10 -0
- package/dist/utils/ast.d.ts +13 -0
- package/dist/utils/ast.js +107 -0
- package/dist/utils/detect.d.ts +2 -0
- package/dist/utils/detect.js +77 -0
- package/dist/utils/entropy.d.ts +7 -0
- package/dist/utils/entropy.js +19 -0
- package/dist/utils/files.d.ts +13 -0
- package/dist/utils/files.js +154 -0
- package/dist/utils/osv.d.ts +26 -0
- package/dist/utils/osv.js +123 -0
- package/dist/utils/registry.d.ts +44 -0
- package/dist/utils/registry.js +195 -0
- package/dist/utils/sql.d.ts +20 -0
- package/dist/utils/sql.js +157 -0
- package/dist/utils/suppress.d.ts +19 -0
- package/dist/utils/suppress.js +56 -0
- package/dist/utils/traverse.d.ts +2 -0
- package/dist/utils/traverse.js +14 -0
- package/dist/vendor/gitleaks/rules.d.ts +13 -0
- package/dist/vendor/gitleaks/rules.js +1571 -0
- package/dist/vendor/guardvibe/index.d.ts +11 -0
- package/dist/vendor/guardvibe/index.js +60 -0
- package/dist/vendor/guardvibe/rules/advanced-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/advanced-security.js +418 -0
- package/dist/vendor/guardvibe/rules/ai-host-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/ai-host-security.js +190 -0
- package/dist/vendor/guardvibe/rules/ai-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/ai-security.js +413 -0
- package/dist/vendor/guardvibe/rules/ai-tool-runtime.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/ai-tool-runtime.js +194 -0
- package/dist/vendor/guardvibe/rules/api-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/api-security.js +154 -0
- package/dist/vendor/guardvibe/rules/auth.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/auth.js +222 -0
- package/dist/vendor/guardvibe/rules/cicd.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/cicd.js +108 -0
- package/dist/vendor/guardvibe/rules/core.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/core.js +484 -0
- package/dist/vendor/guardvibe/rules/cve-versions.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/cve-versions.js +1130 -0
- package/dist/vendor/guardvibe/rules/database.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/database.js +173 -0
- package/dist/vendor/guardvibe/rules/deployment.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/deployment.js +269 -0
- package/dist/vendor/guardvibe/rules/dockerfile.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/dockerfile.js +99 -0
- package/dist/vendor/guardvibe/rules/firebase.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/firebase.js +96 -0
- package/dist/vendor/guardvibe/rules/go.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/go.js +85 -0
- package/dist/vendor/guardvibe/rules/modern-stack.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/modern-stack.js +631 -0
- package/dist/vendor/guardvibe/rules/nextjs.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/nextjs.js +233 -0
- package/dist/vendor/guardvibe/rules/other-services.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/other-services.js +72 -0
- package/dist/vendor/guardvibe/rules/payments.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/payments.js +123 -0
- package/dist/vendor/guardvibe/rules/react-native.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/react-native.js +132 -0
- package/dist/vendor/guardvibe/rules/services.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/services.js +149 -0
- package/dist/vendor/guardvibe/rules/shell.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/shell.js +72 -0
- package/dist/vendor/guardvibe/rules/sql.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/sql.js +60 -0
- package/dist/vendor/guardvibe/rules/supply-chain.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/supply-chain.js +243 -0
- package/dist/vendor/guardvibe/rules/terraform.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/terraform.js +84 -0
- package/dist/vendor/guardvibe/rules/types.d.ts +14 -0
- package/dist/vendor/guardvibe/rules/types.js +11 -0
- package/dist/vendor/guardvibe/rules/web-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/web-security.js +258 -0
- package/examples/security.yml +23 -0
- package/package.json +70 -0
|
@@ -0,0 +1,233 @@
|
|
|
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 Next.js App Router patterns
|
|
12
|
+
export const nextjsRules = [
|
|
13
|
+
{
|
|
14
|
+
id: "VG400",
|
|
15
|
+
name: "Client Component Secret Exposure",
|
|
16
|
+
severity: "critical",
|
|
17
|
+
owasp: "A01:2025 Broken Access Control",
|
|
18
|
+
description: "Server-side environment variable accessed in a 'use client' component. These values are exposed to the browser. Only NEXT_PUBLIC_ variables are safe in client components.",
|
|
19
|
+
pattern: /["']use client["'][\s\S]{0,500}?process\.env\.(?!NEXT_PUBLIC_)\w+/g,
|
|
20
|
+
languages: ["javascript", "typescript"],
|
|
21
|
+
fix: "Move this logic to a Server Component or Server Action. Only process.env.NEXT_PUBLIC_* variables are available in client components.",
|
|
22
|
+
fixCode: '// Move to a Server Component (no \'use client\')\nexport default async function Page() {\n const secret = process.env.SECRET_KEY;\n return <ClientComponent data={safeData} />;\n}',
|
|
23
|
+
compliance: ["SOC2:CC6.1", "HIPAA:§164.312(a)"],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "VG401",
|
|
27
|
+
name: "Server Action Missing Input Validation",
|
|
28
|
+
severity: "high",
|
|
29
|
+
owasp: "A03:2025 Injection",
|
|
30
|
+
description: "Server Action processes form data without schema validation. Unvalidated input can lead to injection attacks.",
|
|
31
|
+
pattern: /["']use server["'](?![\s\S]{0,500}?(?:z\.object|schema\.parse|\.safeParse|yup\.object|valibot\.|\.validate\s*\())[\s\S]{0,500}?formData\.get\s*\(/g,
|
|
32
|
+
languages: ["javascript", "typescript"],
|
|
33
|
+
fix: "Validate all inputs with a schema library (zod, yup, valibot) before processing.",
|
|
34
|
+
fixCode: '"use server";\nimport { z } from "zod";\n\nconst schema = z.object({ name: z.string().min(1), email: z.string().email() });\n\nexport async function createUser(formData: FormData) {\n const data = schema.parse(Object.fromEntries(formData));\n}',
|
|
35
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "VG402",
|
|
39
|
+
name: "Server Action Missing Auth Check",
|
|
40
|
+
severity: "critical",
|
|
41
|
+
owasp: "A01:2025 Broken Access Control",
|
|
42
|
+
description: "Server Action performs data mutations without verifying user authentication. Anyone can invoke Server Actions directly via POST request.",
|
|
43
|
+
pattern: /["']use server["'][\s\S]{0,500}?export\s+async\s+function\s+\w+\s*\([^)]*\)\s*\{(?![\s\S]{0,800}?(?:auth\s*\(|getServerSession|currentUser|getUser|requireAuth|requireAdmin|clerkClient|verifyAuth|checkPermission|assertAuth|ensureAuth|authorize|withAuth))/g,
|
|
44
|
+
languages: ["javascript", "typescript"],
|
|
45
|
+
fix: "Always verify authentication at the start of every Server Action.",
|
|
46
|
+
fixCode: '"use server";\nimport { auth } from "@clerk/nextjs/server";\n\nexport async function deleteItem(id: string) {\n const { userId } = await auth();\n if (!userId) throw new Error("Unauthorized");\n}',
|
|
47
|
+
compliance: ["SOC2:CC6.6", "PCI-DSS:Req6.5.10", "HIPAA:§164.312(d)"],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "VG403",
|
|
51
|
+
name: "Route Handler CORS Wildcard",
|
|
52
|
+
severity: "high",
|
|
53
|
+
owasp: "A01:2025 Broken Access Control",
|
|
54
|
+
description: "Route handler sets Access-Control-Allow-Origin to wildcard (*). This allows any website to make requests to your API.",
|
|
55
|
+
pattern: /(?:GET|POST|PUT|DELETE|PATCH|OPTIONS)\s*\([\s\S]*?["']Access-Control-Allow-Origin["']\s*[,:]\s*["']\*["']/g,
|
|
56
|
+
languages: ["javascript", "typescript"],
|
|
57
|
+
fix: "Restrict CORS to specific trusted origins instead of wildcard.",
|
|
58
|
+
fixCode: '// Restrict to specific origin\nconst allowedOrigin = process.env.ALLOWED_ORIGIN;\nreturn new Response(data, {\n headers: { "Access-Control-Allow-Origin": allowedOrigin }\n});',
|
|
59
|
+
compliance: ["SOC2:CC6.6"],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "VG404",
|
|
63
|
+
name: "Middleware Auth Bypass Risk",
|
|
64
|
+
severity: "high",
|
|
65
|
+
owasp: "A01:2025 Broken Access Control",
|
|
66
|
+
description: "Middleware or proxy has overly broad public path matcher that may accidentally expose protected routes.",
|
|
67
|
+
pattern: /(?:matcher|config)\s*[=:]\s*[\s\S]*?["']\/\(\.\*\)["']/g,
|
|
68
|
+
languages: ["javascript", "typescript"],
|
|
69
|
+
fix: "Use specific path matchers instead of catch-all patterns.",
|
|
70
|
+
fixCode: '// Be specific with matchers\nexport const config = {\n matcher: ["/dashboard/:path*", "/api/:path*"]\n};',
|
|
71
|
+
compliance: ["SOC2:CC6.6"],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "VG405",
|
|
75
|
+
name: "Missing Security Headers in Next Config",
|
|
76
|
+
severity: "medium",
|
|
77
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
78
|
+
description: "next.config is missing important security headers (Content-Security-Policy, Strict-Transport-Security, X-Frame-Options).",
|
|
79
|
+
pattern: /(?:async\s+)?headers\s*\(\s*\)\s*\{[\s\S]{0,20}return\s+\[(?![\s\S]*(?:X-Frame-Options|Strict-Transport-Security|Content-Security-Policy))/g,
|
|
80
|
+
languages: ["javascript", "typescript"],
|
|
81
|
+
fix: "Add security headers in next.config.ts headers() function.",
|
|
82
|
+
fixCode: '// next.config.ts\nasync headers() {\n return [{\n source: "/(.*)",\n headers: [\n { key: "X-Frame-Options", value: "DENY" },\n { key: "X-Content-Type-Options", value: "nosniff" },\n { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },\n ]\n }];\n}',
|
|
83
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.5.10"],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "VG406",
|
|
87
|
+
name: "Unsanitized Dynamic Route Params",
|
|
88
|
+
severity: "high",
|
|
89
|
+
owasp: "A03:2025 Injection",
|
|
90
|
+
description: "Dynamic route parameters (params, searchParams) are used directly in database queries or operations without validation.",
|
|
91
|
+
pattern: /(?:(?:await\s+)?(?:params|searchParams))\s*[\)\.\[]\s*["']?\w+["']?\s*[\]\)]?\s*(?:;|\))\s*[\s\S]*?(?:query|execute|findUnique|findFirst|findMany|delete|update|create)\s*\(/g,
|
|
92
|
+
languages: ["javascript", "typescript"],
|
|
93
|
+
fix: "Always validate and sanitize dynamic route parameters before using them in queries.",
|
|
94
|
+
fixCode: '// Validate params before use\nimport { z } from "zod";\nconst idSchema = z.string().uuid();\n\nexport default async function Page({ params }: { params: { id: string } }) {\n const id = idSchema.parse((await params).id);\n const item = await db.item.findUnique({ where: { id } });\n}',
|
|
95
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "VG407",
|
|
99
|
+
name: "Server Data Leaked to Client Component",
|
|
100
|
+
severity: "high",
|
|
101
|
+
owasp: "A01:2025 Broken Access Control",
|
|
102
|
+
description: "Sensitive data (tokens, secrets, internal IDs) appears to be passed from server to client component as props.",
|
|
103
|
+
// Require no-space `=` to match JSX prop syntax (`token={value}`) and exclude JS object
|
|
104
|
+
// literal assignments (`apiKey = { ... }`, `token = { ... }`) common in test helpers,
|
|
105
|
+
// default-param destructuring, and config objects.
|
|
106
|
+
pattern: /(?:(?:^|[^a-zA-Z])(?:secret|token|password|apiKey|api_key|privateKey|private_key|internalId|ssn|creditCard|credit_card))=\{[\s\S]*?\}/g,
|
|
107
|
+
languages: ["javascript", "typescript"],
|
|
108
|
+
fix: "Never pass sensitive data as props to client components. Keep secrets server-side.",
|
|
109
|
+
fixCode: "// Keep sensitive data server-side\nexport default async function Page() {\n const secret = process.env.API_SECRET;\n const publicData = await fetchData(secret);\n return <ClientComponent data={publicData} />;\n}",
|
|
110
|
+
compliance: ["SOC2:CC6.1", "HIPAA:§164.312(a)"],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "VG408",
|
|
114
|
+
name: "Unsafe innerHTML Usage",
|
|
115
|
+
severity: "high",
|
|
116
|
+
owasp: "A03:2025 Injection",
|
|
117
|
+
description: "Using dangerouslySetInnerHTML renders unescaped HTML, which can lead to XSS if the content includes user input. This is a security rule detector — it flags vulnerable code patterns.",
|
|
118
|
+
pattern: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
|
|
119
|
+
languages: ["javascript", "typescript"],
|
|
120
|
+
fix: "Sanitize HTML with DOMPurify before rendering, or use a markdown renderer instead.",
|
|
121
|
+
fixCode: '// Use a sanitizer library\nimport DOMPurify from "dompurify";\nconst clean = DOMPurify.sanitize(content);\n\n// Or use a markdown renderer\nimport ReactMarkdown from "react-markdown";\n<ReactMarkdown>{content}</ReactMarkdown>',
|
|
122
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.7"],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "VG409",
|
|
126
|
+
name: "Open Redirect via User Input",
|
|
127
|
+
severity: "medium",
|
|
128
|
+
owasp: "A01:2025 Broken Access Control",
|
|
129
|
+
description: "redirect() or NextResponse.redirect() uses user-controlled input (searchParams, query) which can redirect users to malicious sites.",
|
|
130
|
+
pattern: /(?:(?:redirect|NextResponse\.redirect|res\.redirect|Response\.redirect)\s*\(\s*(?:searchParams|params|req\.query|request\.url|url|query|returnTo|callbackUrl|next|goto|returnUrl|redirectUrl|destination)\b|(?:res|reply)\.setHeader\s*\(\s*['"][Ll]ocation['"]\s*,\s*(?:req\.|request\.|searchParams|params\b|url\b|query\b|returnTo|callbackUrl|returnUrl|redirectUrl|destination))/gi,
|
|
131
|
+
languages: ["javascript", "typescript"],
|
|
132
|
+
fix: "Validate redirect URLs against an allowlist of trusted domains.",
|
|
133
|
+
fixCode: '// Validate redirect URL\nconst ALLOWED_HOSTS = ["example.com"];\nconst target = searchParams.get("next") ?? "/";\ntry {\n const url = new URL(target, request.url);\n if (!ALLOWED_HOSTS.includes(url.hostname)) redirect("/");\n redirect(url.pathname);\n} catch {\n redirect("/");\n}',
|
|
134
|
+
compliance: ["SOC2:CC6.6"],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "VG410",
|
|
138
|
+
name: "Unauthorized Cache Revalidation",
|
|
139
|
+
severity: "medium",
|
|
140
|
+
owasp: "A01:2025 Broken Access Control",
|
|
141
|
+
description: "revalidateTag() or revalidatePath() is called in a route handler without authentication check. Anyone could trigger cache invalidation.",
|
|
142
|
+
pattern: /(?:GET|POST|PUT|DELETE)\s*\([\s\S]*?(?:revalidateTag|revalidatePath)\s*\([\s\S]*?(?![\s\S]*?(?:auth\s*\(|getServerSession|currentUser))/g,
|
|
143
|
+
languages: ["javascript", "typescript"],
|
|
144
|
+
fix: "Protect revalidation endpoints with authentication or a secret token.",
|
|
145
|
+
fixCode: 'import { auth } from "@clerk/nextjs/server";\n\nexport async function POST(req: Request) {\n const { userId } = await auth();\n if (!userId) return new Response("Unauthorized", { status: 401 });\n revalidateTag("posts");\n return Response.json({ revalidated: true });\n}',
|
|
146
|
+
compliance: ["SOC2:CC6.6"],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "VG411",
|
|
150
|
+
name: "NEXT_PUBLIC_ Secret Leak",
|
|
151
|
+
severity: "critical",
|
|
152
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
153
|
+
description: "Environment variable prefixed with NEXT_PUBLIC_ contains a secret keyword (SECRET, KEY, PASSWORD, TOKEN). NEXT_PUBLIC_ variables are embedded in the client bundle and visible to anyone.",
|
|
154
|
+
pattern: /NEXT_PUBLIC_(?!\w*PUBLISHABLE)\w*(?:SECRET|_KEY|PASSWORD|TOKEN|PRIVATE|CREDENTIAL)\w*\s*=/gi,
|
|
155
|
+
languages: ["javascript", "typescript", "shell"],
|
|
156
|
+
fix: "Remove NEXT_PUBLIC_ prefix from secret variables. Access them only server-side.",
|
|
157
|
+
fixCode: "# .env.local — WRONG\n# NEXT_PUBLIC_SECRET_KEY=sk_live_xxx\n\n# .env.local — CORRECT\nSECRET_KEY=sk_live_xxx\n# Access server-side only: process.env.SECRET_KEY",
|
|
158
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3", "HIPAA:§164.312(a)"],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "VG412",
|
|
162
|
+
name: "Server Action Returns Full Database Object",
|
|
163
|
+
severity: "high",
|
|
164
|
+
owasp: "A01:2025 Broken Access Control",
|
|
165
|
+
description: "Server Action returns a full database query result without field selection. Sensitive fields (passwordHash, internalNotes) get serialized to the client.",
|
|
166
|
+
pattern: /["']use server["'][\s\S]{0,1500}?\breturn\b[^\n;]{0,80}?(?:findUnique|findFirst|findMany)\s*\((?:(?!select\s*:)[\s\S])*?\)/g,
|
|
167
|
+
languages: ["javascript", "typescript"],
|
|
168
|
+
fix: "Always use select to return only needed fields from Server Actions.",
|
|
169
|
+
fixCode: '"use server";\nexport async function getUser(id: string) {\n return prisma.user.findUnique({\n where: { id },\n select: { id: true, name: true, email: true },\n });\n}',
|
|
170
|
+
compliance: ["SOC2:CC6.1", "HIPAA:§164.312(a)"],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: "VG413",
|
|
174
|
+
name: "Next.js Missing serverActions.allowedOrigins",
|
|
175
|
+
severity: "high",
|
|
176
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
177
|
+
description: "Next.js config uses Server Actions but does not set serverActions.allowedOrigins. Without this, CSRF protection relies only on comparing Origin and Host headers — which can be bypassed behind reverse proxies or CDNs that strip or rewrite the Origin header.",
|
|
178
|
+
pattern: /(?:experimental\s*:\s*\{[\s\S]*?serverActions\s*:\s*\{|serverActions\s*:\s*\{)(?:(?!allowedOrigins)[\s\S]){5,}?\}/g,
|
|
179
|
+
languages: ["javascript", "typescript"],
|
|
180
|
+
fix: "Add serverActions.allowedOrigins to your next.config with your production domain(s).",
|
|
181
|
+
fixCode: '// next.config.ts\nexport default {\n serverActions: {\n allowedOrigins: ["myapp.com", "*.myapp.com"],\n },\n};',
|
|
182
|
+
compliance: ["SOC2:CC6.6"],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "VG414",
|
|
186
|
+
name: "Server-Side Template Injection (SSTI)",
|
|
187
|
+
severity: "critical",
|
|
188
|
+
owasp: "A02:2025 Injection",
|
|
189
|
+
description: "User input is rendered using an unescaped template directive (EJS <%- %>, Handlebars {{{ }}}, Pug != operator, Nunjucks | safe filter). These directives bypass HTML escaping, allowing attackers to inject arbitrary HTML and JavaScript that executes server-side or client-side.",
|
|
190
|
+
pattern: /(?:<%-\s*\w|(?:\{\{\{)\s*\w|\|\s*safe\s*(?:\}\}|\%\}))/gi,
|
|
191
|
+
languages: ["javascript", "typescript", "html"],
|
|
192
|
+
fix: "Always use escaped template directives: EJS <%= %>, Handlebars {{ }}, Pug =. Only use unescaped rendering for trusted, developer-controlled content.",
|
|
193
|
+
fixCode: '<!-- EJS: use escaped output -->\n<p><%= userInput %></p> <!-- SAFE: HTML-escaped -->\n<!-- NOT: <%- userInput %> DANGEROUS: raw HTML -->\n\n<!-- Handlebars: use double braces -->\n<p>{{userInput}}</p> <!-- SAFE: escaped -->\n<!-- NOT: {{{userInput}}} DANGEROUS: raw HTML -->\n\n<!-- Pug: use = not != -->\np= userInput //- SAFE: escaped\n//- NOT: p!= userInput DANGEROUS: raw HTML',
|
|
194
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "VG415",
|
|
198
|
+
name: "Cached Function Exposes User-Specific Data",
|
|
199
|
+
severity: "high",
|
|
200
|
+
owasp: "A01:2025 Broken Access Control",
|
|
201
|
+
description: "A function marked with 'use cache' accesses user-specific data (auth, session, cookies, headers) but caches the result. Cached data is shared across all users, leaking one user's data to others.",
|
|
202
|
+
pattern: /["']use cache["'][\s\S]{0,800}?(?:auth\s*\(|getServerSession|currentUser|getUser|cookies\s*\(|headers\s*\()/g,
|
|
203
|
+
languages: ["javascript", "typescript"],
|
|
204
|
+
fix: "Do not access user-specific data inside cached functions. Pass user-independent parameters only, or use cacheTag with user-specific tags.",
|
|
205
|
+
fixCode: '// BAD: caches user-specific data\n"use cache";\nasync function getData() {\n const { userId } = await auth(); // WRONG in cached fn!\n return db.items.findMany({ where: { userId } });\n}\n\n// GOOD: cache only shared data\n"use cache";\nasync function getPublicPosts() {\n return db.posts.findMany({ where: { published: true } });\n}',
|
|
206
|
+
compliance: ["SOC2:CC6.1"],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: "VG416",
|
|
210
|
+
name: "Cached Function Without Revalidation Strategy",
|
|
211
|
+
severity: "medium",
|
|
212
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
213
|
+
description: "A function marked with 'use cache' does not specify a cacheLife or cacheTag for revalidation. Without explicit revalidation, stale data may be served indefinitely, including outdated security-sensitive information.",
|
|
214
|
+
pattern: /["']use cache["'](?:(?!cacheLife|cacheTag|unstable_cache)[\s\S]){10,}?(?:return|export)/g,
|
|
215
|
+
languages: ["javascript", "typescript"],
|
|
216
|
+
fix: "Add cacheLife() or cacheTag() inside cached functions to control revalidation.",
|
|
217
|
+
fixCode: '"use cache";\nimport { cacheLife, cacheTag } from "next/cache";\n\nasync function getCachedData() {\n cacheLife("hours");\n cacheTag("data-feed");\n return db.posts.findMany();\n}\n\n// Revalidate when data changes:\nimport { revalidateTag } from "next/cache";\nrevalidateTag("data-feed");',
|
|
218
|
+
compliance: ["SOC2:CC7.1"],
|
|
219
|
+
},
|
|
220
|
+
// ── RSC Header Middleware Bypass ─────────────────────────────────
|
|
221
|
+
{
|
|
222
|
+
id: "VG417",
|
|
223
|
+
name: "Next.js RSC/Prefetch Header Bypasses Middleware Auth",
|
|
224
|
+
severity: "high",
|
|
225
|
+
owasp: "A01:2025 Broken Access Control",
|
|
226
|
+
description: "Middleware skips auth checks when RSC: 1 or Next-Router-Prefetch header is present. Attackers can add these headers to any request to bypass authentication on protected routes.",
|
|
227
|
+
pattern: /(?:headers\.get\s*\(\s*["'](?:RSC|Next-Router-Prefetch|Next-Url)["']\s*\))[\s\S]{0,100}?(?:NextResponse\.next|return\s+NextResponse\.next|continue|return)/gi,
|
|
228
|
+
languages: ["javascript", "typescript"],
|
|
229
|
+
fix: "Never skip auth checks based on RSC or prefetch headers. These headers are user-controllable and cannot be trusted for security decisions.",
|
|
230
|
+
fixCode: '// BAD: attackers can add RSC header to skip auth\nif (request.headers.get("RSC") === "1") {\n return NextResponse.next(); // Auth bypassed!\n}\n\n// GOOD: always enforce auth regardless of headers\nconst { userId } = await auth();\nif (!userId && isProtectedRoute(pathname)) {\n return NextResponse.redirect(new URL("/login", request.url));\n}',
|
|
231
|
+
compliance: ["SOC2:CC6.6"],
|
|
232
|
+
},
|
|
233
|
+
];
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
export const otherServiceRules = [
|
|
12
|
+
{
|
|
13
|
+
id: "VG800",
|
|
14
|
+
name: "Sentry Auth Token Exposure",
|
|
15
|
+
severity: "critical",
|
|
16
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
17
|
+
description: "Sentry auth token exposed in client-side code or committed to source. Auth tokens grant full org access. Note: Sentry DSN is safe to expose publicly.",
|
|
18
|
+
pattern: /(?:SENTRY_AUTH_TOKEN\s*[:=]\s*["']?\w{10,}|["']sntrys_[A-Za-z0-9]{20,}["'])/g,
|
|
19
|
+
languages: ["javascript", "typescript", "shell"],
|
|
20
|
+
fix: "Use SENTRY_AUTH_TOKEN only in CI/CD environment. DSN is safe to be public.",
|
|
21
|
+
fixCode: '# .env.local (server only, for source maps upload)\nSENTRY_AUTH_TOKEN=sntrys_xxx\n\n# Safe to expose (DSN)\nNEXT_PUBLIC_SENTRY_DSN=https://abc@sentry.io/123',
|
|
22
|
+
compliance: ["SOC2:CC6.1"],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "VG801",
|
|
26
|
+
name: "Twilio Auth Token Exposure",
|
|
27
|
+
severity: "critical",
|
|
28
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
29
|
+
description: "Twilio auth token or API key hardcoded or exposed in client-side code. This allows sending SMS/calls from your account.",
|
|
30
|
+
pattern: /(?:["']use client["'][\s\S]{0,500}?(?:TWILIO_AUTH_TOKEN|TWILIO_API_SECRET)|(?:authToken|apiSecret)\s*[:=]\s*["'][a-f0-9]{32}["'])/g,
|
|
31
|
+
languages: ["javascript", "typescript"],
|
|
32
|
+
fix: "Use Twilio credentials server-side only. Validate phone numbers to prevent SMS injection.",
|
|
33
|
+
fixCode: '// Server-side only\nimport twilio from "twilio";\nconst client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);\n\n// Validate phone number before sending\nconst phoneRegex = /^\\+[1-9]\\d{1,14}$/;\nif (!phoneRegex.test(to)) throw new Error("Invalid phone");',
|
|
34
|
+
compliance: ["SOC2:CC6.1"],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "VG802",
|
|
38
|
+
name: "Neon/Postgres Connection String Exposure",
|
|
39
|
+
severity: "critical",
|
|
40
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
41
|
+
description: "Neon or PostgreSQL connection string hardcoded in source code with embedded credentials (user:pass@host shape). Local/docker URIs without credentials (e.g. postgres://localhost/db) are not flagged.",
|
|
42
|
+
pattern: /(?:DATABASE_URL|connectionString|connection_string|pgConnectionString)\s*[:=]\s*["']postgres(?:ql)?:\/\/[^"'\s/@]*@[^"'\s]+["']/gi,
|
|
43
|
+
languages: ["javascript", "typescript"],
|
|
44
|
+
fix: "Use DATABASE_URL environment variable. Never hardcode connection strings.",
|
|
45
|
+
fixCode: 'import { neon } from "@neondatabase/serverless";\nconst sql = neon(process.env.DATABASE_URL!);',
|
|
46
|
+
compliance: ["SOC2:CC6.1"],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "VG803",
|
|
50
|
+
name: "Convex Deploy Key Exposure",
|
|
51
|
+
severity: "critical",
|
|
52
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
53
|
+
description: "Convex deploy key or admin key hardcoded or exposed in client-side code. Deploy keys grant write access to your Convex backend.",
|
|
54
|
+
pattern: /(?:CONVEX_DEPLOY_KEY|["']use client["'][\s\S]{0,500}?CONVEX_ADMIN_KEY|(?:deployKey|adminKey)\s*[:=]\s*["'](?:prod|dev):[\w]+:[\w]+["'])/g,
|
|
55
|
+
languages: ["javascript", "typescript", "shell"],
|
|
56
|
+
fix: "Use CONVEX_DEPLOY_KEY only in CI/CD. Client-side should only use the public CONVEX_URL.",
|
|
57
|
+
fixCode: '# Client-safe\nNEXT_PUBLIC_CONVEX_URL=https://xxx.convex.cloud\n\n# Server/CI only\nCONVEX_DEPLOY_KEY=prod:xxx:yyy',
|
|
58
|
+
compliance: ["SOC2:CC6.1"],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "VG804",
|
|
62
|
+
name: "MongoDB Connection String Exposure",
|
|
63
|
+
severity: "critical",
|
|
64
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
65
|
+
description: "MongoDB connection string hardcoded in source code with embedded credentials (user:pass@host shape). Local/docker URIs without credentials (e.g. mongodb://localhost/db) are not flagged.",
|
|
66
|
+
pattern: /(?:MONGODB_URI|MONGO_URL|mongoUri|mongoUrl|connectionString)\s*[:=]\s*["']mongodb(?:\+srv)?:\/\/[^"'\s/@]*@[^"'\s]+["']/gi,
|
|
67
|
+
languages: ["javascript", "typescript"],
|
|
68
|
+
fix: "Use MONGODB_URI environment variable. Never hardcode connection strings.",
|
|
69
|
+
fixCode: 'import { MongoClient } from "mongodb";\nconst client = new MongoClient(process.env.MONGODB_URI!);',
|
|
70
|
+
compliance: ["SOC2:CC6.1"],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
export const paymentRules = [
|
|
12
|
+
// Stripe
|
|
13
|
+
{
|
|
14
|
+
id: "VG600",
|
|
15
|
+
name: "Stripe Secret Key Client Exposure",
|
|
16
|
+
severity: "critical",
|
|
17
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
18
|
+
description: "Stripe secret key (sk_live_ or sk_test_) exposed in client-side code. This key can charge any amount to any card.",
|
|
19
|
+
pattern: /["']use client["'][\s\S]{0,500}?sk_(?:live|test)_/g,
|
|
20
|
+
languages: ["javascript", "typescript"],
|
|
21
|
+
fix: "Never use Stripe secret keys in client code. Use them only in server-side API routes.",
|
|
22
|
+
fixCode: "// Server-side only (API route or Server Action)\nimport Stripe from 'stripe';\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);",
|
|
23
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3"],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "VG601",
|
|
27
|
+
name: "Stripe Webhook Missing Signature Verification",
|
|
28
|
+
severity: "critical",
|
|
29
|
+
owasp: "A01:2025 Broken Access Control",
|
|
30
|
+
description: "Stripe webhook endpoint processes events without verifying the webhook signature. Anyone can send fake payment events.",
|
|
31
|
+
pattern: /(?:\/api\/webhook|\/api\/stripe|webhook.*stripe)[\s\S]*?(?:req\.body|request\.json|JSON\.parse)[\s\S]{0,300}?(?![\s\S]{0,300}?(?:constructEvent|verifyHeader|stripe\.webhooks|svix\.verify|webhook\.verify|verify\w*Webhook|verifyWebhookSignature|wh\.verify|crypto\.timingSafeEqual|verifyQstashSignature|verifyQstash|verifySignature|verifyVercelSignature|Receiver\b|new\s+Webhook\b))/g,
|
|
32
|
+
languages: ["javascript", "typescript"],
|
|
33
|
+
fix: "Always verify Stripe webhook signatures using stripe.webhooks.constructEvent().",
|
|
34
|
+
fixCode: "// Verify webhook signature\nconst sig = request.headers.get('stripe-signature')!;\nconst event = stripe.webhooks.constructEvent(\n body, sig, process.env.STRIPE_WEBHOOK_SECRET!\n);",
|
|
35
|
+
compliance: ["SOC2:CC6.6", "PCI-DSS:Req6.5.10"],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "VG602",
|
|
39
|
+
name: "Stripe Price Amount Client-Side",
|
|
40
|
+
severity: "high",
|
|
41
|
+
owasp: "A04:2025 Insecure Design",
|
|
42
|
+
description: "Price amount sent from client to server for payment. Users can modify the amount in browser DevTools.",
|
|
43
|
+
pattern: /(?:amount|price|total)\s*[:=]\s*(?:req\.body|request\.body|body\.)[\s\S]{0,100}?(?:stripe|payment|checkout|charge)/gi,
|
|
44
|
+
languages: ["javascript", "typescript"],
|
|
45
|
+
fix: "Always calculate prices server-side. Use Stripe Price IDs or calculate from your database, never trust client-sent amounts.",
|
|
46
|
+
fixCode: "// Use Stripe Price IDs, not client amounts\nconst session = await stripe.checkout.sessions.create({\n line_items: [{ price: 'price_xxx', quantity: 1 }], // Price ID from Stripe dashboard\n});",
|
|
47
|
+
compliance: ["PCI-DSS:Req6.5.1"],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "VG603",
|
|
51
|
+
name: "Hardcoded Stripe Key",
|
|
52
|
+
severity: "critical",
|
|
53
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
54
|
+
description: "Stripe API key hardcoded in source code instead of environment variable.",
|
|
55
|
+
pattern: /(?:stripe|Stripe)\s*\(\s*["']sk_(?:live|test)_[A-Za-z0-9]{10,}["']/g,
|
|
56
|
+
languages: ["javascript", "typescript"],
|
|
57
|
+
fix: "Use environment variables for Stripe keys.",
|
|
58
|
+
fixCode: "const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);",
|
|
59
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3"],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "VG604",
|
|
63
|
+
name: "NEXT_PUBLIC Stripe Secret Key",
|
|
64
|
+
severity: "critical",
|
|
65
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
66
|
+
description: "Stripe secret key exposed via NEXT_PUBLIC_ prefix. Only the publishable key (pk_) should be public.",
|
|
67
|
+
pattern: /NEXT_PUBLIC_\w*STRIPE\w*SECRET/gi,
|
|
68
|
+
languages: ["javascript", "typescript", "shell"],
|
|
69
|
+
fix: "Remove NEXT_PUBLIC_ prefix from Stripe secret key. Only NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY should be public.",
|
|
70
|
+
fixCode: "# .env.local\nSTRIPE_SECRET_KEY=sk_live_xxx # server only\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxx # safe to expose",
|
|
71
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3"],
|
|
72
|
+
},
|
|
73
|
+
// LemonSqueezy
|
|
74
|
+
{
|
|
75
|
+
id: "VG605",
|
|
76
|
+
name: "LemonSqueezy API Key Exposure",
|
|
77
|
+
severity: "critical",
|
|
78
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
79
|
+
description: "LemonSqueezy API key exposed in client-side code.",
|
|
80
|
+
pattern: /["']use client["'][\s\S]{0,500}?(?:LEMONSQUEEZY_API_KEY|LEMON_SQUEEZY_API_KEY)/g,
|
|
81
|
+
languages: ["javascript", "typescript"],
|
|
82
|
+
fix: "Use LemonSqueezy API key only in server-side code.",
|
|
83
|
+
fixCode: '// Server-side only (API route)\nimport { lemonSqueezySetup } from "@lemonsqueezy/lemonsqueezy.js";\nlemonSqueezySetup({ apiKey: process.env.LEMONSQUEEZY_API_KEY! });',
|
|
84
|
+
compliance: ["SOC2:CC6.1"],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "VG606",
|
|
88
|
+
name: "LemonSqueezy Webhook Missing Signature",
|
|
89
|
+
severity: "high",
|
|
90
|
+
owasp: "A01:2025 Broken Access Control",
|
|
91
|
+
description: "LemonSqueezy webhook processes events without verifying the X-Signature header.",
|
|
92
|
+
pattern: /(?:lemonsqueezy|lemon.?squeezy)[\s\S]{0,500}?(?:req\.body|request\.json)[\s\S]{0,300}?(?![\s\S]{0,300}?(?:verify|signature|x-signature|hmac|crypto))/gi,
|
|
93
|
+
languages: ["javascript", "typescript"],
|
|
94
|
+
fix: "Verify the X-Signature header using HMAC-SHA256 with your webhook secret.",
|
|
95
|
+
fixCode: "// Verify LemonSqueezy webhook\nimport crypto from 'crypto';\nconst sig = request.headers.get('x-signature');\nconst hash = crypto.createHmac('sha256', process.env.LEMON_SQUEEZY_WEBHOOK_SECRET!)\n .update(body).digest('hex');\nif (sig !== hash) return new Response('Invalid', { status: 401 });",
|
|
96
|
+
compliance: ["SOC2:CC6.6"],
|
|
97
|
+
},
|
|
98
|
+
// Polar.sh
|
|
99
|
+
{
|
|
100
|
+
id: "VG607",
|
|
101
|
+
name: "Polar API Key Exposure",
|
|
102
|
+
severity: "critical",
|
|
103
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
104
|
+
description: "Polar.sh API key or access token exposed in client-side code.",
|
|
105
|
+
pattern: /["']use client["'][\s\S]{0,500}?(?:POLAR_ACCESS_TOKEN|POLAR_API_KEY|polar.*(?:access_token|api_key))/gi,
|
|
106
|
+
languages: ["javascript", "typescript"],
|
|
107
|
+
fix: "Use Polar API keys only in server-side code.",
|
|
108
|
+
fixCode: '// Server-side only\nimport { Polar } from "@polar-sh/sdk";\nconst polar = new Polar({ accessToken: process.env.POLAR_ACCESS_TOKEN! });',
|
|
109
|
+
compliance: ["SOC2:CC6.1"],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "VG608",
|
|
113
|
+
name: "Payment Webhook Missing Auth",
|
|
114
|
+
severity: "high",
|
|
115
|
+
owasp: "A01:2025 Broken Access Control",
|
|
116
|
+
description: "Payment webhook endpoint has no signature verification or authentication. Attackers can fake payment confirmations.",
|
|
117
|
+
pattern: /(?:\/api\/webhook|\/api\/payment|\/api\/checkout)[\s\S]*?export\s+(?:async\s+)?function\s+POST\s*\([^)]*\)\s*\{(?:(?!verify|signature|constructEvent|hmac|crypto\.createHmac|webhookSecret)[\s\S])*?\}/g,
|
|
118
|
+
languages: ["javascript", "typescript"],
|
|
119
|
+
fix: "Always verify webhook signatures before processing payment events.",
|
|
120
|
+
fixCode: "// Verify webhook signature\nimport crypto from 'crypto';\nconst sig = request.headers.get('x-webhook-signature');\nconst expected = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET!)\n .update(body).digest('hex');\nif (sig !== expected) return new Response('Unauthorized', { status: 401 });",
|
|
121
|
+
compliance: ["SOC2:CC6.6", "PCI-DSS:Req6.5.10"],
|
|
122
|
+
},
|
|
123
|
+
];
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
export const reactNativeRules = [
|
|
12
|
+
{
|
|
13
|
+
id: "VG700",
|
|
14
|
+
name: "AsyncStorage Sensitive Data",
|
|
15
|
+
severity: "critical",
|
|
16
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
17
|
+
description: "Storing secrets, tokens, or passwords in AsyncStorage. AsyncStorage is unencrypted plain text on device. Use expo-secure-store or react-native-keychain instead.",
|
|
18
|
+
pattern: /AsyncStorage\s*\.\s*(?:setItem|multiSet)\s*\(\s*["'][^"']*(?:token|secret|password|jwt|session|auth|apiKey|api_key|credential|refresh)["']/gi,
|
|
19
|
+
languages: ["javascript", "typescript"],
|
|
20
|
+
fix: "Use expo-secure-store (Expo) or react-native-keychain (bare RN) for sensitive data.",
|
|
21
|
+
fixCode: 'import * as SecureStore from "expo-secure-store";\nawait SecureStore.setItemAsync("authToken", token);',
|
|
22
|
+
compliance: ["SOC2:CC6.1", "HIPAA:§164.312(a)"],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "VG701",
|
|
26
|
+
name: "Deep Link Auth Bypass",
|
|
27
|
+
severity: "high",
|
|
28
|
+
owasp: "A07:2025 Identification and Authentication Failures",
|
|
29
|
+
description: "Deep link handler processes authentication parameters (token, code, session) without validation. Attackers can craft malicious deep links to bypass auth.",
|
|
30
|
+
pattern: /(?:Linking\.addEventListener|useURL|createURL|expo-linking)[\s\S]{0,500}?(?:url|event)[\s\S]{0,300}?(?:token|code|session|auth)/gi,
|
|
31
|
+
languages: ["javascript", "typescript"],
|
|
32
|
+
fix: "Validate deep link origin, verify tokens server-side, and never trust URL parameters for auth state.",
|
|
33
|
+
fixCode: '// Validate token from deep link server-side\nconst { token } = parseURL(url);\nconst res = await fetch("/api/verify-token", { method: "POST", body: JSON.stringify({ token }) });\nif (!res.ok) throw new Error("Invalid deep link token");',
|
|
34
|
+
compliance: ["SOC2:CC6.1"],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "VG702",
|
|
38
|
+
name: "Expo Push Token Exposure",
|
|
39
|
+
severity: "high",
|
|
40
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
41
|
+
description: "Expo push token logged or sent to client-side analytics. Push tokens can be abused to send spam notifications.",
|
|
42
|
+
pattern: /(?:console\.\w+|analytics|posthog|capture|track)\s*[\.\(][\s\S]{0,200}?(?:ExpoPushToken|expoPushToken|pushToken)/gi,
|
|
43
|
+
languages: ["javascript", "typescript"],
|
|
44
|
+
fix: "Never log push tokens. Send them only to your own server over HTTPS.",
|
|
45
|
+
fixCode: '// Send push token securely to your server\nawait fetch("/api/register-push", {\n method: "POST",\n headers: { Authorization: `Bearer ${authToken}` },\n body: JSON.stringify({ pushToken }),\n});',
|
|
46
|
+
compliance: ["SOC2:CC6.1"],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "VG703",
|
|
50
|
+
name: "Hardcoded Secrets in EAS Config",
|
|
51
|
+
severity: "critical",
|
|
52
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
53
|
+
description: "Hardcoded secrets, API keys, or tokens in eas.json or eas.config. These end up in version control.",
|
|
54
|
+
pattern: /(?:"env"|env\s*:\s*\{)[\s\S]{0,300}?(?:SECRET|TOKEN|API_KEY|PASSWORD|PRIVATE)\s*["']?\s*[:=]\s*["'][^"']{8,}["']/gi,
|
|
55
|
+
languages: ["json", "javascript", "typescript"],
|
|
56
|
+
fix: "Use EAS Secrets (eas secret:create) instead of hardcoding in eas.json.",
|
|
57
|
+
fixCode: '# Set secrets via EAS CLI\neas secret:create --name API_KEY --value "your-key" --scope project\n\n# Reference in eas.json\n{ "build": { "production": { "env": { "API_KEY": "eas-secret" } } } }',
|
|
58
|
+
compliance: ["SOC2:CC6.1"],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "VG704",
|
|
62
|
+
name: "WebView JavaScript Injection",
|
|
63
|
+
severity: "critical",
|
|
64
|
+
owasp: "A03:2025 Injection",
|
|
65
|
+
description: "WebView loading untrusted URLs or injecting unvalidated JavaScript. This allows XSS and data theft from the app context.",
|
|
66
|
+
pattern: /(?:WebView|webview)\s*[\s\S]{0,300}?(?:injectedJavaScript|source\s*=\s*\{\s*\{\s*uri\s*:\s*(?:user|params|route|input|req|data|body)[\s\S]{0,100}?\}|javaScriptEnabled\s*=\s*\{?\s*true)/gi,
|
|
67
|
+
languages: ["javascript", "typescript"],
|
|
68
|
+
fix: "Validate WebView URLs against an allowlist. Avoid injecting dynamic JavaScript. Disable JavaScript if not needed.",
|
|
69
|
+
fixCode: 'const ALLOWED_HOSTS = ["example.com", "docs.example.com"];\nconst url = new URL(inputUrl);\nif (!ALLOWED_HOSTS.includes(url.hostname)) throw new Error("Blocked URL");\n\n<WebView source={{ uri: url.toString() }} javaScriptEnabled={false} />',
|
|
70
|
+
compliance: ["SOC2:CC7.1"],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "VG705",
|
|
74
|
+
name: "Missing Certificate Pinning",
|
|
75
|
+
severity: "medium",
|
|
76
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
77
|
+
description: "Network requests to API without certificate pinning. On mobile, this allows man-in-the-middle attacks on compromised networks.",
|
|
78
|
+
pattern: /(?:fetch|axios|http)\s*[\.\(][\s\S]{0,200}?(?:api\.|\/api\/)[\s\S]{0,300}?(?:Authorization|Bearer|token)/gi,
|
|
79
|
+
languages: ["javascript", "typescript"],
|
|
80
|
+
fix: "Implement certificate pinning using react-native-ssl-pinning or expo-certificate-transparency.",
|
|
81
|
+
fixCode: '// Use react-native-ssl-pinning\nimport { fetch } from "react-native-ssl-pinning";\nconst res = await fetch("https://api.example.com/data", {\n sslPinning: { certs: ["api-cert"] },\n headers: { Authorization: `Bearer ${token}` },\n});',
|
|
82
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req4"],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "VG706",
|
|
86
|
+
name: "Hardcoded API URL",
|
|
87
|
+
severity: "medium",
|
|
88
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
89
|
+
description: "API base URL hardcoded directly in source. This makes it hard to switch environments and may expose staging/internal endpoints.",
|
|
90
|
+
pattern: /(?:baseURL|apiUrl|API_URL|apiBase|BASE_URL)\s*[:=]\s*["']https?:\/\/(?!localhost)[^"']{5,}["']/gi,
|
|
91
|
+
languages: ["javascript", "typescript"],
|
|
92
|
+
fix: "Use environment variables or app config for API URLs.",
|
|
93
|
+
fixCode: 'import Constants from "expo-constants";\nconst API_URL = Constants.expoConfig?.extra?.apiUrl ?? process.env.EXPO_PUBLIC_API_URL;',
|
|
94
|
+
compliance: ["SOC2:CC6.1"],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "VG707",
|
|
98
|
+
name: "Disabled App Transport Security",
|
|
99
|
+
severity: "high",
|
|
100
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
101
|
+
description: "App Transport Security (ATS) disabled in iOS config, allowing insecure HTTP connections.",
|
|
102
|
+
pattern: /NSAppTransportSecurity[\s\S]{0,200}?NSAllowsArbitraryLoads[\s\S]{0,50}?(?:true|YES|<true\s*\/>)/gi,
|
|
103
|
+
languages: ["xml", "json", "javascript", "typescript"],
|
|
104
|
+
fix: "Do not disable ATS. If specific domains need HTTP, use NSExceptionDomains instead of blanket allow.",
|
|
105
|
+
fixCode: "<!-- Info.plist — allow HTTP only for specific domains -->\n<key>NSAppTransportSecurity</key>\n<dict>\n <key>NSExceptionDomains</key>\n <dict>\n <key>legacy-api.example.com</key>\n <dict>\n <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>\n <true/>\n </dict>\n </dict>\n</dict>",
|
|
106
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req4"],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "VG708",
|
|
110
|
+
name: "Sensitive Data in Expo Config",
|
|
111
|
+
severity: "critical",
|
|
112
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
113
|
+
description: "Secrets or API keys hardcoded in app.json, app.config.js, or app.config.ts. These are embedded in the app bundle and visible to anyone who decompiles the app.",
|
|
114
|
+
pattern: /(?:app\.json|app\.config\.[jt]s)[\s\S]{0,50}|(?:"extra"|extra\s*:\s*\{)[\s\S]{0,500}?(?:SECRET|PRIVATE_KEY|API_SECRET|DATABASE_URL|SERVICE_ACCOUNT)\s*[:=]\s*["'][^"']{8,}["']/gi,
|
|
115
|
+
languages: ["json", "javascript", "typescript"],
|
|
116
|
+
fix: "Use EXPO_PUBLIC_ prefix only for truly public values. Keep secrets server-side or in EAS Secrets.",
|
|
117
|
+
fixCode: '// app.config.ts — only public values in extra\nexport default {\n extra: {\n apiUrl: process.env.EXPO_PUBLIC_API_URL, // OK: public\n // NEVER: apiSecret: process.env.API_SECRET\n },\n};',
|
|
118
|
+
compliance: ["SOC2:CC6.1"],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "VG709",
|
|
122
|
+
name: "React Native Bridge Sensitive Data",
|
|
123
|
+
severity: "high",
|
|
124
|
+
owasp: "A07:2025 Sensitive Data Exposure",
|
|
125
|
+
description: "Sensitive data (tokens, keys, passwords) passed through React Native bridge/NativeModules without encryption. Bridge data can be intercepted on rooted/jailbroken devices.",
|
|
126
|
+
pattern: /NativeModules\.\w+\.\w+\s*\([\s\S]{0,200}?(?:token|secret|password|key|credential|jwt|session)/gi,
|
|
127
|
+
languages: ["javascript", "typescript"],
|
|
128
|
+
fix: "Encrypt sensitive data before passing through the bridge. Use native secure storage instead.",
|
|
129
|
+
fixCode: '// Use secure storage instead of passing through bridge\nimport * as SecureStore from "expo-secure-store";\nawait SecureStore.setItemAsync("authToken", token);\n\n// Read securely\nconst token = await SecureStore.getItemAsync("authToken");',
|
|
130
|
+
compliance: ["SOC2:CC6.1"],
|
|
131
|
+
},
|
|
132
|
+
];
|