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,11 @@
|
|
|
1
|
+
import type { SecurityRule } from './rules/types.js';
|
|
2
|
+
export type { SecurityRule };
|
|
3
|
+
export declare const GUARDVIBE_RULES: SecurityRule[];
|
|
4
|
+
/**
|
|
5
|
+
* Ids of the hand-maintained CVE-version rules. When OSV.dev has answered for a
|
|
6
|
+
* package these are redundant and, being hand-written, are the likelier of the
|
|
7
|
+
* two to be stale — so they stand down. They remain the only such coverage when
|
|
8
|
+
* running with --offline.
|
|
9
|
+
*/
|
|
10
|
+
export declare const GUARDVIBE_CVE_RULE_IDS: ReadonlySet<string>;
|
|
11
|
+
export declare const GUARDVIBE_ATTRIBUTION = "GuardVibe (github.com/goklab/guardvibe), Copyright 2026 GokLab, Apache-2.0";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { advancedSecurityRules } from './rules/advanced-security.js';
|
|
2
|
+
import { aiHostSecurityRules } from './rules/ai-host-security.js';
|
|
3
|
+
import { aiSecurityRules } from './rules/ai-security.js';
|
|
4
|
+
import { aiToolRuntimeRules } from './rules/ai-tool-runtime.js';
|
|
5
|
+
import { apiSecurityRules } from './rules/api-security.js';
|
|
6
|
+
import { authRules } from './rules/auth.js';
|
|
7
|
+
import { cicdRules } from './rules/cicd.js';
|
|
8
|
+
import { coreRules } from './rules/core.js';
|
|
9
|
+
import { cveVersionRules } from './rules/cve-versions.js';
|
|
10
|
+
import { databaseRules } from './rules/database.js';
|
|
11
|
+
import { deploymentRules } from './rules/deployment.js';
|
|
12
|
+
import { dockerfileRules } from './rules/dockerfile.js';
|
|
13
|
+
import { firebaseRules } from './rules/firebase.js';
|
|
14
|
+
import { goRules } from './rules/go.js';
|
|
15
|
+
import { modernStackRules } from './rules/modern-stack.js';
|
|
16
|
+
import { nextjsRules } from './rules/nextjs.js';
|
|
17
|
+
import { otherServiceRules } from './rules/other-services.js';
|
|
18
|
+
import { paymentRules } from './rules/payments.js';
|
|
19
|
+
import { reactNativeRules } from './rules/react-native.js';
|
|
20
|
+
import { serviceRules } from './rules/services.js';
|
|
21
|
+
import { shellRules } from './rules/shell.js';
|
|
22
|
+
import { sqlRules } from './rules/sql.js';
|
|
23
|
+
import { supplyChainRules } from './rules/supply-chain.js';
|
|
24
|
+
import { terraformRules } from './rules/terraform.js';
|
|
25
|
+
import { webSecurityRules } from './rules/web-security.js';
|
|
26
|
+
export const GUARDVIBE_RULES = [
|
|
27
|
+
...advancedSecurityRules,
|
|
28
|
+
...aiHostSecurityRules,
|
|
29
|
+
...aiSecurityRules,
|
|
30
|
+
...aiToolRuntimeRules,
|
|
31
|
+
...apiSecurityRules,
|
|
32
|
+
...authRules,
|
|
33
|
+
...cicdRules,
|
|
34
|
+
...coreRules,
|
|
35
|
+
...cveVersionRules,
|
|
36
|
+
...databaseRules,
|
|
37
|
+
...deploymentRules,
|
|
38
|
+
...dockerfileRules,
|
|
39
|
+
...firebaseRules,
|
|
40
|
+
...goRules,
|
|
41
|
+
...modernStackRules,
|
|
42
|
+
...nextjsRules,
|
|
43
|
+
...otherServiceRules,
|
|
44
|
+
...paymentRules,
|
|
45
|
+
...reactNativeRules,
|
|
46
|
+
...serviceRules,
|
|
47
|
+
...shellRules,
|
|
48
|
+
...sqlRules,
|
|
49
|
+
...supplyChainRules,
|
|
50
|
+
...terraformRules,
|
|
51
|
+
...webSecurityRules,
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
54
|
+
* Ids of the hand-maintained CVE-version rules. When OSV.dev has answered for a
|
|
55
|
+
* package these are redundant and, being hand-written, are the likelier of the
|
|
56
|
+
* two to be stale — so they stand down. They remain the only such coverage when
|
|
57
|
+
* running with --offline.
|
|
58
|
+
*/
|
|
59
|
+
export const GUARDVIBE_CVE_RULE_IDS = new Set(cveVersionRules.map((r) => r.id));
|
|
60
|
+
export const GUARDVIBE_ATTRIBUTION = 'GuardVibe (github.com/goklab/guardvibe), Copyright 2026 GokLab, Apache-2.0';
|
|
@@ -0,0 +1,418 @@
|
|
|
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
|
+
// Advanced security rules that catch patterns AI assistants commonly generate
|
|
12
|
+
// These cover gaps in OWASP Top 10, CWE Top 25, and OWASP API Security Top 10
|
|
13
|
+
export const advancedSecurityRules = [
|
|
14
|
+
// ── HTTP Response Header Injection (CWE-113) ─────────────────────
|
|
15
|
+
{
|
|
16
|
+
id: "VG130",
|
|
17
|
+
name: "HTTP Response Header Injection",
|
|
18
|
+
severity: "high",
|
|
19
|
+
owasp: "A02:2025 Injection",
|
|
20
|
+
description: "User input is interpolated into HTTP response headers. Attackers can inject CRLF characters to add arbitrary headers (Set-Cookie, Location) or split the response.",
|
|
21
|
+
pattern: /(?:res(?:ponse)?\.(?:setHeader|set|append)|headers\.(?:set|append)|setHeader|appendHeader)\s*\(\s*["'][^"']+["']\s*,\s*(?:`[^`]*\$\{|[^"']*\+\s*(?:req\.|request\.|params\.|query\.|searchParams|input|body|user))/gi,
|
|
22
|
+
languages: ["javascript", "typescript"],
|
|
23
|
+
fix: "Never interpolate user input into response headers. Sanitize by removing \\r and \\n characters.",
|
|
24
|
+
fixCode: '// Sanitize header values\nconst safeValue = userInput.replace(/[\\r\\n]/g, "");\nres.setHeader("Content-Disposition", `attachment; filename="${safeValue}"`);',
|
|
25
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
26
|
+
},
|
|
27
|
+
// ── CSRF via State-Changing GET (CWE-352) ─────────────────────────
|
|
28
|
+
{
|
|
29
|
+
id: "VG131",
|
|
30
|
+
name: "State-Changing GET Request",
|
|
31
|
+
severity: "high",
|
|
32
|
+
owasp: "A01:2025 Broken Access Control",
|
|
33
|
+
description: "GET handler performs database mutations (delete, update, create). GET requests should be idempotent and safe. State-changing operations in GET handlers are vulnerable to CSRF via img tags, link prefetching, and browser preloading.",
|
|
34
|
+
pattern: /export\s+(?:async\s+)?function\s+GET\s*\([^)]*\)\s*\{[\s\S]{0,1000}?(?:\.delete\s*\(|\.update\s*\(|\.create\s*\(|\.destroy\s*\(|\.remove\s*\(|\.insert\s*\(|DELETE\s+FROM|UPDATE\s+|INSERT\s+INTO)/gi,
|
|
35
|
+
languages: ["javascript", "typescript"],
|
|
36
|
+
fix: "Move state-changing operations to POST/PUT/DELETE handlers. GET should only read data.",
|
|
37
|
+
fixCode: '// BAD: mutation in GET\nexport async function GET(req: Request) {\n await db.post.delete({ where: { id } }); // CSRF risk!\n}\n\n// GOOD: use POST/DELETE\nexport async function DELETE(req: Request) {\n await db.post.delete({ where: { id } });\n}',
|
|
38
|
+
compliance: ["SOC2:CC6.6"],
|
|
39
|
+
},
|
|
40
|
+
// ── Missing Request Body Size Limit ───────────────────────────────
|
|
41
|
+
{
|
|
42
|
+
id: "VG132",
|
|
43
|
+
name: "Missing Request Body Size Limit",
|
|
44
|
+
severity: "low",
|
|
45
|
+
owasp: "API4:2023 Unrestricted Resource Consumption",
|
|
46
|
+
description: "API endpoint reads request body without explicit size limit. Note: Next.js/Vercel applies a default 4.5MB limit, so this is informational for those platforms. For custom servers, attackers can send large payloads to exhaust memory.",
|
|
47
|
+
pattern: /export\s+(?:async\s+)?function\s+(?:POST|PUT|PATCH)\s*\([^)]*\)\s*\{(?:(?!content-length|maxBodySize|limit|MAX_|parseBody|checkBodySize|bodyParser|bodyLimit|sizeLimit)[\s\S]){5,}?(?:req\.json|req\.text|req\.body|req\.formData|request\.json|request\.text)\s*\(\s*\)/g,
|
|
48
|
+
languages: ["javascript", "typescript"],
|
|
49
|
+
fix: "Check Content-Length header before parsing body, or use a body parser with size limit.",
|
|
50
|
+
fixCode: '// Check body size before parsing\nexport async function POST(req: Request) {\n const contentLength = parseInt(req.headers.get("content-length") || "0");\n if (contentLength > 1024 * 1024) { // 1MB limit\n return new Response("Payload too large", { status: 413 });\n }\n const body = await req.json();\n}',
|
|
51
|
+
compliance: ["SOC2:CC7.1"],
|
|
52
|
+
},
|
|
53
|
+
// ── Race Condition in Check-Then-Act ──────────────────────────────
|
|
54
|
+
{
|
|
55
|
+
id: "VG133",
|
|
56
|
+
name: "Race Condition: Check-Then-Act Without Transaction",
|
|
57
|
+
severity: "high",
|
|
58
|
+
owasp: "A04:2025 Insecure Design",
|
|
59
|
+
description: "Code reads a value, checks a condition, then updates based on the check — without a database transaction. Two concurrent requests can both pass the check before either writes, leading to double-spending, overselling, or duplicate operations.",
|
|
60
|
+
// Negative lookahead at the start of the if-body skips the 404-mapping shape
|
|
61
|
+
// (`if (!x) { return …; }`). Without it, the engine's backtracking matched updates
|
|
62
|
+
// that lived OUTSIDE the if-block (within 500-char window after the brace), making
|
|
63
|
+
// every `findUnique → if(!x) 404 → update` admin route a false hit.
|
|
64
|
+
pattern: /(?:findUnique|findFirst|findOne|findById)\s*\([\s\S]{0,200}?\)\s*;?\s*\n[\s\S]{0,300}?if\s*\([\s\S]{0,200}?\)\s*\{(?!\s*(?:return\b|throw\b|res\.\w+\(|response\.\w+\(|next\.\w+\(|NextResponse\.))[\s\S]{0,500}?(?:\.update\s*\(|\.delete\s*\(|\.decrement|\.increment)(?:(?!\$transaction|\.transaction|BEGIN|SERIALIZABLE|FOR UPDATE|NOWAIT)[\s\S]){0,300}?\}/g,
|
|
65
|
+
languages: ["javascript", "typescript"],
|
|
66
|
+
fix: "Wrap check-then-act sequences in a database transaction, or use atomic operations (e.g., UPDATE WHERE balance >= amount).",
|
|
67
|
+
fixCode: '// BAD: race condition\nconst account = await db.account.findUnique({ where: { id } });\nif (account.balance >= 100) {\n await db.account.update({ where: { id }, data: { balance: { decrement: 100 } } });\n}\n\n// GOOD: atomic transaction\nawait db.$transaction(async (tx) => {\n const account = await tx.account.findUnique({ where: { id } });\n if (account.balance < 100) throw new Error("Insufficient");\n await tx.account.update({ where: { id }, data: { balance: { decrement: 100 } } });\n});',
|
|
68
|
+
compliance: ["SOC2:CC7.1"],
|
|
69
|
+
},
|
|
70
|
+
// ── WebSocket Without Authentication ──────────────────────────────
|
|
71
|
+
{
|
|
72
|
+
id: "VG134",
|
|
73
|
+
name: "WebSocket Connection Without Authentication",
|
|
74
|
+
severity: "high",
|
|
75
|
+
owasp: "A01:2025 Broken Access Control",
|
|
76
|
+
description: "WebSocket server accepts connections without verifying authentication. Any client can connect and receive or send data.",
|
|
77
|
+
pattern: /(?:WebSocketServer|WebSocket\.Server|new\s+Server)\s*\([\s\S]{0,200}?\)[\s\S]{0,300}?\.on\s*\(\s*["']connection["'][\s\S]{0,500}?(?:(?!auth|token|verify|session|cookie|jwt|bearer)[\s\S]){10,}?\.on\s*\(\s*["']message["']/gi,
|
|
78
|
+
languages: ["javascript", "typescript"],
|
|
79
|
+
fix: "Verify authentication token in the WebSocket upgrade request or first message.",
|
|
80
|
+
fixCode: '// Verify auth on connection\nwss.on("connection", (ws, req) => {\n const token = new URL(req.url!, "http://localhost").searchParams.get("token");\n if (!verifyToken(token)) { ws.close(1008, "Unauthorized"); return; }\n ws.on("message", (msg) => { /* handle */ });\n});',
|
|
81
|
+
compliance: ["SOC2:CC6.6"],
|
|
82
|
+
},
|
|
83
|
+
// ── SSE/Streaming Without Authentication ──────────────────────────
|
|
84
|
+
{
|
|
85
|
+
id: "VG135",
|
|
86
|
+
name: "Server-Sent Events Without Authentication",
|
|
87
|
+
severity: "high",
|
|
88
|
+
owasp: "A01:2025 Broken Access Control",
|
|
89
|
+
description: "Server-Sent Events endpoint streams data without authentication check. Anyone can subscribe and receive real-time updates.",
|
|
90
|
+
pattern: /["']text\/event-stream["'][\s\S]{0,500}?(?:export\s+(?:async\s+)?function\s+GET|new\s+Response\s*\()(?:(?!auth\s*\(|getServerSession|currentUser|verifyToken|session|protect)[\s\S]){5,}/gi,
|
|
91
|
+
languages: ["javascript", "typescript"],
|
|
92
|
+
fix: "Add authentication check before establishing SSE connection.",
|
|
93
|
+
compliance: ["SOC2:CC6.6"],
|
|
94
|
+
},
|
|
95
|
+
// ── postMessage Without Origin Check ──────────────────────────────
|
|
96
|
+
{
|
|
97
|
+
id: "VG136",
|
|
98
|
+
name: "postMessage Handler Without Origin Validation",
|
|
99
|
+
severity: "high",
|
|
100
|
+
owasp: "A07:2025 Cross-Site Scripting",
|
|
101
|
+
description: "Window message event handler processes data without checking event.origin. Any page (including malicious iframes) can send messages to this handler.",
|
|
102
|
+
pattern: /addEventListener\s*\(\s*["']message["']\s*,\s*(?:async\s+)?(?:\([^)]*\)|(?:event|e|evt|msg))\s*(?:=>|{)(?:(?!\.origin|event\.source)[\s\S]){5,}?(?:JSON\.parse|\.data|innerHTML|setState|dispatch|update|execute)/gi,
|
|
103
|
+
languages: ["javascript", "typescript"],
|
|
104
|
+
fix: "Always check event.origin against trusted origins before processing message data.",
|
|
105
|
+
fixCode: '// Always validate origin\nwindow.addEventListener("message", (event) => {\n if (event.origin !== "https://trusted.example.com") return;\n const data = JSON.parse(event.data);\n processData(data);\n});',
|
|
106
|
+
compliance: ["SOC2:CC7.1"],
|
|
107
|
+
},
|
|
108
|
+
// ── Debug/Internal Endpoint Exposed ───────────────────────────────
|
|
109
|
+
{
|
|
110
|
+
id: "VG137",
|
|
111
|
+
name: "Debug Endpoint Exposes System Information",
|
|
112
|
+
severity: "critical",
|
|
113
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
114
|
+
description: "Route exposes system internals (process.env, process.memoryUsage, os.cpus, debug data) that help attackers map the infrastructure.",
|
|
115
|
+
pattern: /(?:\/debug|\/internal|\/_internal|\/test|\/dev)\b[\s\S]{0,300}?(?:process\.env|process\.memoryUsage|os\.cpus|os\.hostname|process\.uptime|process\.version)/gi,
|
|
116
|
+
languages: ["javascript", "typescript"],
|
|
117
|
+
fix: "Remove debug endpoints from production code, or protect them with authentication and restrict to internal networks.",
|
|
118
|
+
compliance: ["SOC2:CC6.1"],
|
|
119
|
+
},
|
|
120
|
+
// ── Plaintext Password Comparison ─────────────────────────────────
|
|
121
|
+
{
|
|
122
|
+
id: "VG138",
|
|
123
|
+
name: "Plaintext Password Comparison",
|
|
124
|
+
severity: "critical",
|
|
125
|
+
owasp: "A02:2025 Cryptographic Failures",
|
|
126
|
+
description: "Password is compared using direct string equality (=== or ==) instead of a hashing function. This means passwords are stored or transmitted in plaintext.",
|
|
127
|
+
pattern: /(?<!typeof\s)(?:password|passwd|pwd)\s*(?:===|!==|==|!=)\s*(?:(?:req|request|body|input|data|form|user)[\.\[]|["'](?!(?:string|number|boolean|undefined|object|function|symbol|bigint)["']))/gi,
|
|
128
|
+
languages: ["javascript", "typescript", "python"],
|
|
129
|
+
fix: "Never compare passwords directly. Use bcrypt.compare() or argon2.verify() to compare against hashed passwords.",
|
|
130
|
+
fixCode: '// BAD: plaintext comparison\nif (user.password === inputPassword) { ... }\n\n// GOOD: hash comparison\nimport bcrypt from "bcrypt";\nconst valid = await bcrypt.compare(inputPassword, user.passwordHash);\nif (!valid) return new Response("Invalid", { status: 401 });',
|
|
131
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req8"],
|
|
132
|
+
},
|
|
133
|
+
// ── TLS Certificate Verification Disabled ─────────────────────────
|
|
134
|
+
{
|
|
135
|
+
id: "VG139",
|
|
136
|
+
name: "TLS Certificate Verification Disabled",
|
|
137
|
+
severity: "critical",
|
|
138
|
+
owasp: "A02:2025 Cryptographic Failures",
|
|
139
|
+
description: "TLS certificate verification is disabled, allowing man-in-the-middle attacks. All HTTPS connections become insecure.",
|
|
140
|
+
pattern: /(?:NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*["']0["']|rejectUnauthorized\s*:\s*false|(?<![\w$])verify\s*=\s*False|InsecureSkipVerify\s*:\s*true)/gi,
|
|
141
|
+
languages: ["javascript", "typescript", "python", "go"],
|
|
142
|
+
fix: "Never disable TLS verification in production. Fix certificate issues instead.",
|
|
143
|
+
fixCode: '// BAD: disables all TLS verification\n// process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";\n// const agent = new https.Agent({ rejectUnauthorized: false });\n\n// GOOD: fix the certificate issue\n// - Use valid certificates (Let\'s Encrypt)\n// - Add CA certificate to Node: --use-openssl-ca\n// - For self-signed dev certs: only disable in NODE_ENV=development',
|
|
144
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req4.1"],
|
|
145
|
+
},
|
|
146
|
+
// ── XXE (XML External Entity) ─────────────────────────────────────
|
|
147
|
+
{
|
|
148
|
+
id: "VG140",
|
|
149
|
+
name: "XML Parsing Without Disabling External Entities",
|
|
150
|
+
severity: "high",
|
|
151
|
+
owasp: "A02:2025 Injection",
|
|
152
|
+
description: "XML is parsed without disabling external entity resolution. Attackers can use XXE to read local files, perform SSRF, or cause denial of service.",
|
|
153
|
+
pattern: /(?:parseStringPromise|parseString|DOMParser|xml2js|xmldom|libxmljs|(?:fast-xml-parser|XMLParser))\s*[\.(](?:(?!processExternalEntities\s*:\s*false|noent\s*:\s*false|resolveExternals\s*:\s*false|entityMode|FORBID_DTD)[\s\S]){5,}?(?:req\.|request\.|body|input|data|text)/gi,
|
|
154
|
+
languages: ["javascript", "typescript"],
|
|
155
|
+
fix: "Disable external entity processing in your XML parser configuration.",
|
|
156
|
+
fixCode: '// xml2js: external entities disabled by default (safe)\nimport { parseStringPromise } from "xml2js";\nconst result = await parseStringPromise(xmlInput);\n\n// fast-xml-parser: safe by default\nimport { XMLParser } from "fast-xml-parser";\nconst parser = new XMLParser({ processEntities: false });\nconst result = parser.parse(xmlInput);',
|
|
157
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
158
|
+
},
|
|
159
|
+
// ── YAML Unsafe Load ──────────────────────────────────────────────
|
|
160
|
+
{
|
|
161
|
+
id: "VG141",
|
|
162
|
+
name: "YAML Parsed with Unsafe Loader",
|
|
163
|
+
severity: "high",
|
|
164
|
+
owasp: "A08:2025 Software and Data Integrity Failures",
|
|
165
|
+
description: "YAML is parsed using an unsafe loader that can execute arbitrary code via !!js/function, !!python/object, or other language-specific tags.",
|
|
166
|
+
pattern: /yaml\.(?:load|unsafeLoad)\s*\(\s*(?![\s\S]{0,30}?(?:JSON_SCHEMA|FAILSAFE_SCHEMA|CORE_SCHEMA|safeLoad))/gi,
|
|
167
|
+
languages: ["javascript", "typescript", "python"],
|
|
168
|
+
fix: "Use yaml.safeLoad() or yaml.load(input, { schema: yaml.JSON_SCHEMA }).",
|
|
169
|
+
fixCode: '// BAD: allows code execution\n// yaml.load(userInput)\n\n// GOOD: safe schema\nimport yaml from "js-yaml";\nconst config = yaml.load(userInput, { schema: yaml.JSON_SCHEMA });',
|
|
170
|
+
compliance: ["SOC2:CC7.1"],
|
|
171
|
+
},
|
|
172
|
+
// ── Missing Subresource Integrity ─────────────────────────────────
|
|
173
|
+
{
|
|
174
|
+
id: "VG142",
|
|
175
|
+
name: "External Script Without Subresource Integrity",
|
|
176
|
+
severity: "medium",
|
|
177
|
+
owasp: "A08:2025 Software and Data Integrity Failures",
|
|
178
|
+
description: "External script loaded from CDN without integrity attribute. If the CDN is compromised, malicious code executes in your users' browsers.",
|
|
179
|
+
pattern: /<script\s+[^>]*src\s*=\s*["']https?:\/\/(?:(?!integrity)[\s\S])*?>/gi,
|
|
180
|
+
languages: ["html", "javascript", "typescript"],
|
|
181
|
+
fix: "Add integrity and crossorigin attributes to external script tags.",
|
|
182
|
+
fixCode: '<!-- Add SRI hash -->\n<script\n src="https://cdn.example.com/lib.js"\n integrity="sha384-HASH_HERE"\n crossorigin="anonymous"\n></script>\n\n<!-- Generate hash: openssl dgst -sha384 -binary lib.js | base64 -->',
|
|
183
|
+
compliance: ["SOC2:CC7.1"],
|
|
184
|
+
},
|
|
185
|
+
// ── CSP Missing frame-ancestors ───────────────────────────────────
|
|
186
|
+
{
|
|
187
|
+
id: "VG143",
|
|
188
|
+
name: "CSP Missing frame-ancestors Directive",
|
|
189
|
+
severity: "medium",
|
|
190
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
191
|
+
description: "Content-Security-Policy is set but lacks frame-ancestors directive. Without it, the page can be embedded in malicious iframes for clickjacking attacks. frame-ancestors supersedes X-Frame-Options.",
|
|
192
|
+
pattern: /Content-Security-Policy["']\s*[,:]\s*["'][^"']*(?:default-src|script-src)(?:(?!frame-ancestors)[^"'])*["']/gi,
|
|
193
|
+
languages: ["javascript", "typescript"],
|
|
194
|
+
fix: "Add frame-ancestors 'self' to your Content-Security-Policy header.",
|
|
195
|
+
fixCode: '// Add frame-ancestors to CSP\nheaders: [\n {\n key: "Content-Security-Policy",\n value: "default-src \'self\'; frame-ancestors \'self\'; script-src \'self\'"\n }\n]',
|
|
196
|
+
compliance: ["SOC2:CC6.1"],
|
|
197
|
+
},
|
|
198
|
+
// ── Missing Referrer-Policy ───────────────────────────────────────
|
|
199
|
+
{
|
|
200
|
+
id: "VG144",
|
|
201
|
+
name: "Missing Referrer-Policy Header",
|
|
202
|
+
severity: "medium",
|
|
203
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
204
|
+
description: "Security headers are configured but Referrer-Policy is missing. Without it, the full URL (including query parameters with tokens/IDs) is sent to external sites in the Referer header.",
|
|
205
|
+
pattern: /(?:async\s+)?headers\s*\(\s*\)(?=[\s\S]*(?:X-Frame-Options|Strict-Transport-Security|Content-Security-Policy))(?![\s\S]*Referrer-Policy)/g,
|
|
206
|
+
languages: ["javascript", "typescript"],
|
|
207
|
+
fix: "Add Referrer-Policy: strict-origin-when-cross-origin to your security headers.",
|
|
208
|
+
fixCode: '{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }',
|
|
209
|
+
compliance: ["SOC2:CC6.1"],
|
|
210
|
+
},
|
|
211
|
+
// ── Missing Permissions-Policy ────────────────────────────────────
|
|
212
|
+
{
|
|
213
|
+
id: "VG145",
|
|
214
|
+
name: "Missing Permissions-Policy Header",
|
|
215
|
+
severity: "medium",
|
|
216
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
217
|
+
description: "Security headers are configured but Permissions-Policy is missing. Without it, embedded iframes and scripts can access camera, microphone, geolocation, and other sensitive browser APIs.",
|
|
218
|
+
pattern: /(?:async\s+)?headers\s*\(\s*\)(?=[\s\S]*(?:X-Frame-Options|Strict-Transport-Security|Content-Security-Policy))(?![\s\S]*Permissions-Policy)/g,
|
|
219
|
+
languages: ["javascript", "typescript"],
|
|
220
|
+
fix: "Add Permissions-Policy header to restrict browser API access.",
|
|
221
|
+
fixCode: '{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" }',
|
|
222
|
+
compliance: ["SOC2:CC6.1"],
|
|
223
|
+
},
|
|
224
|
+
// ── Unquoted .env Value ───────────────────────────────────────────
|
|
225
|
+
{
|
|
226
|
+
id: "VG146",
|
|
227
|
+
name: "Unquoted .env Value with Special Characters",
|
|
228
|
+
severity: "medium",
|
|
229
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
230
|
+
description: "Environment variable value contains special characters but is not quoted. This can cause parsing errors or shell injection when the .env file is sourced.",
|
|
231
|
+
pattern: /^[A-Z_]+=(?:[^"'\s]*[@#$&|;`\\(){}[\]!<>^~*?])/gm,
|
|
232
|
+
languages: ["shell"],
|
|
233
|
+
fix: "Quote .env values that contain special characters.",
|
|
234
|
+
fixCode: '# BAD: unquoted special characters\nDATABASE_URL=postgres://user:p@ss@host/db\n\n# GOOD: quoted\nDATABASE_URL="postgres://user:p@ss@host/db"',
|
|
235
|
+
compliance: ["SOC2:CC6.1"],
|
|
236
|
+
},
|
|
237
|
+
// ── Audit Logging Missing for Critical Operations ─────────────────
|
|
238
|
+
{
|
|
239
|
+
id: "VG147",
|
|
240
|
+
name: "Critical Operation Without Audit Logging",
|
|
241
|
+
severity: "medium",
|
|
242
|
+
owasp: "A09:2025 Security Logging and Monitoring Failures",
|
|
243
|
+
description: "Destructive database operation (delete user, change role, update payment) has no audit logging. Without audit trails, security incidents cannot be investigated.",
|
|
244
|
+
pattern: /(?:deleteUser|deleteAccount|updateRole|changeRole|cancelSubscription|refund|transferFunds|resetPassword|changePassword|deleteOrg|removeUser|banUser|suspendUser|updatePermission)\s*(?:=\s*async|\([\s\S]*?\)\s*(?:=>|{))[\s\S]{0,500}?(?:\.delete\s*\(|\.update\s*\(|\.destroy\s*\()(?:(?!console\.log|logger\.|audit\.|log\.|createAuditLog|logAction|trackEvent|analytics)[\s\S]){0,300}?(?:return|Response)/gi,
|
|
245
|
+
languages: ["javascript", "typescript"],
|
|
246
|
+
fix: "Add audit logging for all critical operations: who did what, when, and to whom.",
|
|
247
|
+
fixCode: 'async function deleteUser(targetId: string) {\n const { userId } = await auth();\n await db.user.delete({ where: { id: targetId } });\n // Audit log\n await db.auditLog.create({\n data: { action: "DELETE_USER", actorId: userId, targetId, timestamp: new Date() }\n });\n}',
|
|
248
|
+
compliance: ["SOC2:CC7.2", "HIPAA:§164.312(b)", "GDPR:Art30"],
|
|
249
|
+
},
|
|
250
|
+
// ── Login Without Brute Force Protection ──────────────────────────
|
|
251
|
+
{
|
|
252
|
+
id: "VG148",
|
|
253
|
+
name: "Login Endpoint Without Brute Force Protection",
|
|
254
|
+
severity: "high",
|
|
255
|
+
owasp: "A07:2025 Identification and Authentication Failures",
|
|
256
|
+
description: "Login/authentication endpoint compares passwords without rate limiting or account lockout. Attackers can try unlimited password combinations.",
|
|
257
|
+
pattern: /(?:signIn|login|authenticate|logIn)\b(?:(?!rateLimit|limiter|throttle|lockout|maxAttempts|failedAttempts|loginAttempts|Ratelimit|brute)[\s\S]){0,500}?(?:bcrypt\.compare|argon2\.verify|compare|verify)(?:(?!rateLimit|limiter|throttle|lockout|maxAttempts|failedAttempts|loginAttempts|Ratelimit|brute)[\s\S]){5,300}?(?:return|Response|res\.)/gi,
|
|
258
|
+
languages: ["javascript", "typescript"],
|
|
259
|
+
fix: "Add rate limiting and account lockout to login endpoints.",
|
|
260
|
+
fixCode: '// Add rate limiting to login\nimport { Ratelimit } from "@upstash/ratelimit";\nconst loginLimiter = new Ratelimit({\n redis: Redis.fromEnv(),\n limiter: Ratelimit.slidingWindow(5, "15 m"), // 5 attempts per 15 min\n});\n\nconst { success } = await loginLimiter.limit(email);\nif (!success) return new Response("Too many attempts", { status: 429 });',
|
|
261
|
+
compliance: ["SOC2:CC6.6", "PCI-DSS:Req8"],
|
|
262
|
+
},
|
|
263
|
+
// ── Multi-Tenant Data Leak ────────────────────────────────────────
|
|
264
|
+
{
|
|
265
|
+
id: "VG149",
|
|
266
|
+
name: "Multi-Tenant Query Without Tenant Scoping",
|
|
267
|
+
severity: "high",
|
|
268
|
+
owasp: "A01:2025 Broken Access Control",
|
|
269
|
+
description: "Endpoint authenticates a user/org but queries the database without filtering by tenant ID. Returns data from ALL tenants instead of only the authenticated tenant's data.",
|
|
270
|
+
pattern: /(?:orgId|tenantId|organizationId|org_id|tenant_id)\s*[\s\S]{0,200}?(?:findMany|findAll|getAll|fetchAll)\s*\(\s*(?:\{(?:(?!orgId|tenantId|organizationId|org_id|tenant_id)[\s\S]){5,}?\}|\s*\))/gi,
|
|
271
|
+
languages: ["javascript", "typescript"],
|
|
272
|
+
fix: "Always include the tenant/org ID in the WHERE clause of every query.",
|
|
273
|
+
fixCode: '// BAD: returns all tenants\' data\nconst { orgId } = await auth();\nconst items = await db.item.findMany(); // missing orgId filter!\n\n// GOOD: scoped to tenant\nconst items = await db.item.findMany({ where: { orgId } });',
|
|
274
|
+
compliance: ["SOC2:CC6.1", "HIPAA:§164.312(a)"],
|
|
275
|
+
},
|
|
276
|
+
// ── Lockfile in .gitignore ────────────────────────────────────────
|
|
277
|
+
{
|
|
278
|
+
id: "VG150",
|
|
279
|
+
name: "Package Lockfile Excluded from Git",
|
|
280
|
+
severity: "high",
|
|
281
|
+
owasp: "A08:2025 Software and Data Integrity Failures",
|
|
282
|
+
description: "package-lock.json, yarn.lock, or pnpm-lock.yaml is in .gitignore. Without a committed lockfile, builds are non-reproducible and vulnerable to dependency confusion and supply chain attacks.",
|
|
283
|
+
pattern: /^(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml)\s*$/gm,
|
|
284
|
+
languages: ["shell"],
|
|
285
|
+
fix: "Remove the lockfile from .gitignore and commit it to the repository.",
|
|
286
|
+
compliance: ["SOC2:CC7.1"],
|
|
287
|
+
},
|
|
288
|
+
// ── Error Message Exposure ───────────────────────────────────────
|
|
289
|
+
{
|
|
290
|
+
id: "VG151",
|
|
291
|
+
name: "Internal Error Message Exposed to Client",
|
|
292
|
+
severity: "medium",
|
|
293
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
294
|
+
description: "Caught error's .message property is returned directly in the API response. This can leak internal details (stack traces, DB errors, file paths) that help attackers understand the system architecture.",
|
|
295
|
+
pattern: /catch\s*\(\s*(?:error|err|e)\s*\)\s*\{[\s\S]{0,300}?(?:Response\.json|NextResponse\.json|res\.(?:json|send|status))\s*\([\s\S]{0,100}?(?:error|err|e)\.message/gi,
|
|
296
|
+
languages: ["javascript", "typescript"],
|
|
297
|
+
fix: "Log the real error server-side, return a generic message to the client.",
|
|
298
|
+
fixCode: '// BAD: leaks internal details\ncatch (error) {\n return Response.json({ error: error.message }, { status: 500 });\n}\n\n// GOOD: generic response, detailed server log\ncatch (error) {\n console.error("Route error:", error);\n return Response.json({ error: "Internal server error" }, { status: 500 });\n}',
|
|
299
|
+
compliance: ["SOC2:CC7.2"],
|
|
300
|
+
},
|
|
301
|
+
// ── Object Injection / Prototype Pollution ───────────────────────
|
|
302
|
+
{
|
|
303
|
+
id: "VG152",
|
|
304
|
+
name: "Object Injection via Dynamic Property Access",
|
|
305
|
+
severity: "high",
|
|
306
|
+
owasp: "A03:2025 Injection",
|
|
307
|
+
description: "User-controlled input is used as an object property key via bracket notation (obj[userInput]). Attackers can access __proto__, constructor, or prototype to pollute object prototypes and bypass security checks.",
|
|
308
|
+
pattern: /(?:(?:req|request|body|query|params)\.\w+|(?:const|let|var)\s+(?:\{[^}]*\}|\w+)\s*=\s*(?:await\s+)?(?:req|request)[\s\S]{0,50}?)[\s\S]{0,100}?\w+\s*\[\s*(?:key|field|prop|name|column|attr|param)\s*\]/gi,
|
|
309
|
+
languages: ["javascript", "typescript"],
|
|
310
|
+
fix: "Validate property names against an allowlist, or use Map instead of plain objects.",
|
|
311
|
+
fixCode: '// BAD: prototype pollution\nconst key = req.query.field;\nconst value = config[key];\n\n// GOOD: allowlist validation\nconst ALLOWED_FIELDS = new Set(["name", "email", "role"]);\nif (!ALLOWED_FIELDS.has(key)) return new Response("Invalid field", { status: 400 });\nconst value = config[key];\n\n// GOOD: use Map\nconst config = new Map([["name", "..."], ["email", "..."]]);\nconst value = config.get(key);',
|
|
312
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
313
|
+
},
|
|
314
|
+
// ── ReDoS — Unsafe Regular Expression ────────────────────────────
|
|
315
|
+
{
|
|
316
|
+
id: "VG153",
|
|
317
|
+
name: "Regular Expression Vulnerable to ReDoS",
|
|
318
|
+
severity: "medium",
|
|
319
|
+
owasp: "A04:2025 Insecure Design",
|
|
320
|
+
description: "Regular expression contains nested quantifiers ((a+)+), overlapping alternation with quantifiers (([a-z]+)*), or other patterns that cause catastrophic backtracking. Attackers can send crafted input to freeze the event loop.",
|
|
321
|
+
pattern: /\/(?:[^/\\\n]|\\.)*(?:\([^)\n]*[+*][^)\n]*\)[+*]|\(\?:[^)\n]*[+*][^)\n]*\)[+*])(?:[^/\\\n]|\\.)*\//g,
|
|
322
|
+
languages: ["javascript", "typescript"],
|
|
323
|
+
fix: "Rewrite the regex to avoid nested quantifiers. Use atomic groups or possessive quantifiers if available, or use the 'safe-regex' library to validate patterns.",
|
|
324
|
+
fixCode: '// BAD: catastrophic backtracking\nconst re = /(a+)+$/;\n\n// GOOD: no nested quantifiers\nconst re = /a+$/;\n\n// GOOD: validate with safe-regex\nimport safe from "safe-regex";\nif (!safe(pattern)) throw new Error("Unsafe regex");',
|
|
325
|
+
compliance: ["SOC2:CC7.1"],
|
|
326
|
+
},
|
|
327
|
+
// ── Supabase Race Condition: Check-Then-Act ──────────────────────
|
|
328
|
+
{
|
|
329
|
+
id: "VG154",
|
|
330
|
+
name: "Supabase Race Condition: Check-Then-Act Without Transaction",
|
|
331
|
+
severity: "critical",
|
|
332
|
+
owasp: "A04:2025 Insecure Design",
|
|
333
|
+
description: "Code reads a Supabase value (select/count), checks a condition, then updates — without an atomic transaction or RPC function. Two concurrent requests can both pass the check before either writes, enabling double-spending, quota bypass, or duplicate operations.",
|
|
334
|
+
pattern: /\.from\s*\(\s*["'][^"']+["']\s*\)\s*\.select\s*\([\s\S]{0,300}?(?:\.single\s*\(|count:\s*["']exact["'])[\s\S]{0,500}?if\s*\([\s\S]{0,500}?\.(?:update|insert|delete|upsert)\s*\((?:(?!\.rpc\s*\(|BEGIN|SERIALIZABLE)[\s\S]){0,300}/g,
|
|
335
|
+
languages: ["javascript", "typescript"],
|
|
336
|
+
fix: "Use a Supabase RPC function with a PostgreSQL transaction, or use atomic UPDATE with WHERE conditions.",
|
|
337
|
+
fixCode: '// BAD: race condition\nconst { data } = await supabase.from("users").select("credits").eq("id", id).single();\nif (data.credits >= cost) {\n await supabase.from("users").update({ credits: data.credits - cost }).eq("id", id);\n}\n\n// GOOD: atomic RPC function\nawait supabase.rpc("spend_credits", { user_id: id, amount: cost });',
|
|
338
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
339
|
+
},
|
|
340
|
+
// ── Missing CSRF Protection ──────────────────────────────────────
|
|
341
|
+
{
|
|
342
|
+
id: "VG155",
|
|
343
|
+
name: "State-Changing Endpoint Missing CSRF Protection",
|
|
344
|
+
severity: "medium",
|
|
345
|
+
owasp: "A01:2025 Broken Access Control",
|
|
346
|
+
description: "POST/PUT/PATCH/DELETE route handler performs database mutations without CSRF token verification. Cross-site requests from malicious pages can trick authenticated users into performing unwanted actions.",
|
|
347
|
+
pattern: /export\s+(?:async\s+)?function\s+(?:POST|PUT|PATCH|DELETE)\s*\([^)]*\)\s*\{(?:(?!csrf|csrfToken|CSRF|x-csrf|verifyCsrf|validateCsrf|anti.?forgery|requireAdmin|requireAuth|checkAuth|withAuth|protectRoute|authenticate|x-csrf-protection|getAuth|currentUser|clerkClient|createServerClient|createServerSupabaseClient|getServerSession|getSession|auth\(\)|getToken|verifyToken|clerkMiddleware)[\s\S]){10,}?(?:\.create\s*\(|\.update\s*\(|\.delete\s*\(|\.insert\s*\(|\.upsert\s*\()/g,
|
|
348
|
+
languages: ["javascript", "typescript"],
|
|
349
|
+
fix: "Add CSRF token verification to state-changing endpoints.",
|
|
350
|
+
fixCode: '// Verify CSRF token from header\nexport async function POST(req: Request) {\n const csrfToken = req.headers.get("x-csrf-token");\n if (!verifyCsrfToken(csrfToken)) {\n return new Response("CSRF validation failed", { status: 403 });\n }\n}',
|
|
351
|
+
compliance: ["SOC2:CC6.6", "PCI-DSS:Req6.5.1"],
|
|
352
|
+
},
|
|
353
|
+
// ── In-Memory State in Serverless ────────────────────────────────
|
|
354
|
+
{
|
|
355
|
+
id: "VG156",
|
|
356
|
+
name: "In-Memory State in Serverless Environment",
|
|
357
|
+
severity: "medium",
|
|
358
|
+
owasp: "A04:2025 Insecure Design",
|
|
359
|
+
description: "Module-level Map, object, or array is used for rate limiting, caching, or session storage. In serverless environments, each function instance has its own memory that resets on cold starts.",
|
|
360
|
+
pattern: /(?:const|let|var)\s+\w+[\s\S]{0,80}?=\s*(?:new\s+Map|new\s+Set|\{\s*\}|\[\s*\])[\s\S]{0,500}?export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|DELETE|PATCH)/g,
|
|
361
|
+
languages: ["javascript", "typescript"],
|
|
362
|
+
fix: "Use Redis (Upstash), Vercel KV, or another external store for state that must persist across requests in serverless.",
|
|
363
|
+
fixCode: '// BAD: resets on cold start\nconst rateMap = new Map();\nexport async function POST(req: Request) { ... }\n\n// GOOD: use Redis\nimport { Ratelimit } from "@upstash/ratelimit";\nimport { Redis } from "@upstash/redis";\nconst ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, "10s") });',
|
|
364
|
+
compliance: ["SOC2:CC7.1"],
|
|
365
|
+
},
|
|
366
|
+
// ── Rate Limit Fail-Open ─────────────────────────────────────────
|
|
367
|
+
{
|
|
368
|
+
id: "VG157",
|
|
369
|
+
name: "Rate Limiter Fails Open on Error",
|
|
370
|
+
severity: "high",
|
|
371
|
+
owasp: "A04:2025 Insecure Design",
|
|
372
|
+
description: "Rate limiting catch block returns a permissive result (limited: false, success: true) when the rate limit backend (Redis) fails. If Redis goes down, all rate limits are disabled.",
|
|
373
|
+
pattern: /(?:rateLimit|rateLimiter|limiter|Ratelimit)[\s\S]{0,500}?catch\s*\([^)]*\)\s*\{[\s\S]{0,200}?(?:limited\s*:\s*false|success\s*:\s*true|allowed\s*:\s*true)/g,
|
|
374
|
+
languages: ["javascript", "typescript"],
|
|
375
|
+
fix: "Fail closed: when the rate limiter backend is unavailable, deny the request.",
|
|
376
|
+
fixCode: '// BAD: fail-open\ncatch (error) { return { limited: false }; }\n\n// GOOD: fail-closed\ncatch (error) {\n console.error("Rate limiter unavailable:", error);\n return { limited: true };\n}',
|
|
377
|
+
compliance: ["SOC2:CC7.1"],
|
|
378
|
+
},
|
|
379
|
+
// ── Fail-Open Authorization Default ──────────────────────────────
|
|
380
|
+
{
|
|
381
|
+
id: "VG158",
|
|
382
|
+
name: "Authorization Defaults to Permissive Role",
|
|
383
|
+
severity: "medium",
|
|
384
|
+
owasp: "A01:2025 Broken Access Control",
|
|
385
|
+
description: "Role normalization or validation defaults to a permissive role (member, user, editor) for unknown values. If an unrecognized role string is passed, the user gets member-level access instead of being denied.",
|
|
386
|
+
pattern: /(?:default\s*:\s*(?:return\s+)?["'](?:member|user|editor|writer)["']|:\s*["'](?:member|user|editor|writer)["']\s*;?\s*\}(?:\s*$|\s*\/\/))/gm,
|
|
387
|
+
languages: ["javascript", "typescript"],
|
|
388
|
+
fix: "Default unknown roles to the most restrictive level (guest, none, or throw an error).",
|
|
389
|
+
fixCode: '// BAD: unknown role gets member access\ndefault: return "member";\n\n// GOOD: fail closed\ndefault: return "guest";\n\n// BEST: reject unknown roles\ndefault: throw new Error(`Unknown role: ${role}`);',
|
|
390
|
+
compliance: ["SOC2:CC6.6"],
|
|
391
|
+
},
|
|
392
|
+
// ── Timing Side-Channel in Secret Comparison ─────────────────────
|
|
393
|
+
{
|
|
394
|
+
id: "VG159",
|
|
395
|
+
name: "Timing-Unsafe Secret Comparison",
|
|
396
|
+
severity: "medium",
|
|
397
|
+
owasp: "A02:2025 Cryptographic Failures",
|
|
398
|
+
description: "Secret, token, or API key is compared using === or !== which leaks timing information. Attackers can determine how many characters match by measuring response times.",
|
|
399
|
+
pattern: /(?:secret|token|apiKey|api_key|cron_secret|CRON_SECRET|webhook_secret|WEBHOOK_SECRET|hmac|signature)\b[\s\S]{0,60}?(?:===|!==)\s*(?:process\.env\.|req\.|request\.|headers)/gi,
|
|
400
|
+
languages: ["javascript", "typescript"],
|
|
401
|
+
fix: "Use crypto.timingSafeEqual() for all secret comparisons.",
|
|
402
|
+
fixCode: '// BAD: timing leak\nif (secret !== process.env.CRON_SECRET) return false;\n\n// GOOD: constant-time comparison\nimport { timingSafeEqual } from "crypto";\nfunction safeCompare(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a), Buffer.from(b));\n}',
|
|
403
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.5.1"],
|
|
404
|
+
},
|
|
405
|
+
// ── User-Controlled href Without Protocol Check ──────────────────
|
|
406
|
+
{
|
|
407
|
+
id: "VG160",
|
|
408
|
+
name: "User-Controlled URL in href Without Protocol Validation",
|
|
409
|
+
severity: "medium",
|
|
410
|
+
owasp: "A07:2025 Cross-Site Scripting",
|
|
411
|
+
description: "User-provided URL is used directly in an href attribute without checking for dangerous protocols. Attackers can inject javascript:alert(document.cookie) to execute XSS when the link is clicked.",
|
|
412
|
+
pattern: /href\s*=\s*\{(?:user|profile|author|post|comment|record|input|formData|payload)[\w.]*\.(?:url|website|link|href|homepage)\}/gi,
|
|
413
|
+
languages: ["javascript", "typescript"],
|
|
414
|
+
fix: "Validate that URLs start with https:// or http:// before using in href.",
|
|
415
|
+
fixCode: '// BAD: XSS via javascript: protocol\n<a href={user.website}>{user.name}</a>\n\n// GOOD: protocol validation\nfunction safeHref(url: string): string {\n try {\n const parsed = new URL(url);\n if (["http:", "https:"].includes(parsed.protocol)) return url;\n } catch {}\n return "#";\n}\n<a href={safeHref(user.website)}>{user.name}</a>',
|
|
416
|
+
compliance: ["SOC2:CC7.1"],
|
|
417
|
+
},
|
|
418
|
+
];
|