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]*\)
|
|
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>',
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
650
|
+
break; // closed a previous block — not in exempt
|
|
643
651
|
}
|
|
644
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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.
|
|
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",
|