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]*\)
|
|
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|
|
|
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  — the browser automatically fetches the URL, silently exfiltrating data. This was exploited against Microsoft 365 Copilot in 2025.",
|
|
192
|
-
pattern: /(?:dangerouslySetInnerHTML|innerHTML|v-html
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
686
|
+
break; // closed a previous block — not in exempt
|
|
643
687
|
}
|
|
644
|
-
if (
|
|
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", "
|
|
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.
|
|
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",
|