guardvibe 3.1.31 → 3.1.33
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/CHANGELOG.md +26 -0
- package/README.md +8 -8
- package/build/data/rules/api-security.js +1 -1
- package/build/data/rules/core.js +4 -4
- package/build/data/rules/cve-versions.js +48 -0
- package/build/tools/check-code.js +152 -1
- package/build/tools/taint-analysis.js +23 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,32 @@ All notable changes to GuardVibe are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.1.33] - 2026-06-07
|
|
9
|
+
|
|
10
|
+
### Fixed — false-positive precision (no rule-count change, stays 433 / 36)
|
|
11
|
+
Surfaced by an end-to-end accuracy sweep across the labeled fixture set and the real-world corpus; each change has positive + negative tests and was cross-checked against an uncapped before/after diff (removal-only, zero real findings lost).
|
|
12
|
+
- **Engine:** multi-line `/* */` block comments are now stripped before matching (string-aware, scoped to C-style languages) so rules no longer fire on commented-out code; YAML/Python/shell/Dockerfile (which use `#`) are unaffected.
|
|
13
|
+
- **VG060** no longer flags MD5/SHA-1 used for file/build-artifact checksums (keeps real password-hashing).
|
|
14
|
+
- **VG1002** only flags query operators whose value is attacker-controlled (a static `{ $ne: true }` literal is skipped; `$where` built from a variable/concat/interpolation still fires).
|
|
15
|
+
- **VG123 / VG010 / taint** skip queries that are parameterized (`bind`/`replacements`/`$1`/`:name`) and whose only interpolation is a hash/encode helper.
|
|
16
|
+
- **VG951** recognizes ownership fields (`author`, `email`, `accountId`, …) in the where-clause.
|
|
17
|
+
- **VG138** ignores confirm-password (`password === cpassword`) and emptiness checks.
|
|
18
|
+
- **VG001** ignores UI/error-message string variables; **VG148**/**VG424** skip test (`.spec`) files.
|
|
19
|
+
- **VG013** renamed to "ORM/NoSQL query injection risk" with stack-aware remediation (Sequelize/TypeORM operator injection, not Mongo-only).
|
|
20
|
+
|
|
21
|
+
Tests 1820 → 1848. Self-audit PASS / A / 0. Determinism unchanged across the corpus.
|
|
22
|
+
|
|
23
|
+
## [3.1.32] - 2026-06-06
|
|
24
|
+
|
|
25
|
+
### Added — 4 new CVE rules (429 → 433), sourced via `npm run intel`
|
|
26
|
+
First rules added through the new intel-gap workflow: the daily check surfaced these as uncovered HIGH/CRITICAL npm advisories, each was written + tested + passed `npm run gate`.
|
|
27
|
+
- **VG1076** vitest < 4.1.0 — UI/API server arbitrary file read & execute (CVE-2026-47429, GHSA-5xrq-8626-4rwp, critical)
|
|
28
|
+
- **VG1077** @vitest/browser 4.0.17–4.1.5 + 5.0.0-beta.0→beta.2 — inline-script XSS via unsanitized `otelCarrier` query param (CVE-2026-47428, GHSA-2h32-95rg-cppp, critical)
|
|
29
|
+
- **VG1078** liquidjs < 10.26.0 — remote code execution via attacker-influenced templates (CVE-2026-45618, GHSA-gf2q-c269-pqgc, critical)
|
|
30
|
+
- **VG1079** tinymce < 5.11.1 / 6.0.0→7.9.2 / 8.0.0→8.5.0 — stored/DOM XSS cluster incl. media-plugin `data-mce-object` injection (CVE-2026-47759/47760/47761/47762, high)
|
|
31
|
+
|
|
32
|
+
CVE-version intelligence count 63 → 67. Tests +24. Self-audit PASS A 100.
|
|
33
|
+
|
|
8
34
|
## [3.1.31] - 2026-06-06
|
|
9
35
|
|
|
10
36
|
### Added — daily intel-gap triage
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/guardvibe)
|
|
7
7
|
[](https://codecov.io/gh/goklab/guardvibe)
|
|
8
8
|
|
|
9
|
-
**The security MCP built for vibe coding.**
|
|
9
|
+
**The security MCP built for vibe coding.** 433 security rules, 36 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,11 +14,11 @@ 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
|
-
- **
|
|
17
|
+
- **433 security rules, 36 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
|
|
21
|
-
- **CVE version intelligence** — detects
|
|
21
|
+
- **CVE version intelligence** — detects 67 known vulnerable package versions in package.json, refreshed every day from GHSA / OSV.dev / CISA KEV
|
|
22
22
|
- **AI agent & MCP security** — detects MCP server vulnerabilities, tool-description prompt injection (OWASP MCP Top 10), model-controlled sandbox-disable flags, excessive AI permissions, indirect prompt injection
|
|
23
23
|
- **Auto-fix suggestions** — `fix_code` tool returns concrete patches and structured edits the AI agent can apply mechanically. Coverage: hardcoded credentials → env-var migration; public-prefix LLM keys (`NEXT_PUBLIC_/VITE_/EXPO_PUBLIC_/REACT_APP_`) → prefix removal; CORS wildcards → env allowlist; `dangerouslyAllowBrowser` flags → drop; sandbox bypass flags (`unsafe`/`noSandbox`/`allowEval`) → drop; agent loops → add `maxSteps`; raw-HTML React props → `<ReactMarkdown>`; missing auth checks → insert auth guard; SQL injection → parameterized queries; missing rate limiters / CSRF / security headers → snippet templates.
|
|
24
24
|
- **Pre-commit hook** — block insecure code before it reaches your repo
|
|
@@ -49,10 +49,10 @@ GuardVibe is purpose-built for the AI coding workflow. Traditional tools are exc
|
|
|
49
49
|
| AI/LLM security (prompt injection, MCP, tool abuse) | 68 rules | Experimental/None | None |
|
|
50
50
|
| AI host security (CVE-2025-59536, CVE-2026-21852) | `guardvibe doctor` | Not supported | Not supported |
|
|
51
51
|
| Auto-fix suggestions for AI agents | `fix_code` tool | CLI autofix | Not supported |
|
|
52
|
-
| CVE version detection |
|
|
52
|
+
| CVE version detection | 67 packages, refreshed daily | Extensive | Extensive |
|
|
53
53
|
| Compliance mapping (SOC2, PCI-DSS, HIPAA) | Built-in | Paid tier | None |
|
|
54
54
|
| SARIF CI/CD export | Yes | Yes | Limited |
|
|
55
|
-
| Rule count |
|
|
55
|
+
| Rule count | 433 (focused, 68 AI-native) | 5000+ (broad) | N/A |
|
|
56
56
|
|
|
57
57
|
**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.
|
|
58
58
|
|
|
@@ -176,7 +176,7 @@ React Native, Expo — AsyncStorage secrets, deep link token exposure, hardcoded
|
|
|
176
176
|
### Firebase
|
|
177
177
|
Firestore security rules, Firebase Admin SDK exposure, storage rules, custom token validation
|
|
178
178
|
|
|
179
|
-
### CVE Version Intelligence (
|
|
179
|
+
### CVE Version Intelligence (67 CVEs, refreshed daily)
|
|
180
180
|
**Frameworks:** Next.js (CVE-2024-34351, CVE-2024-46982, CVE-2025-29927, CVE-2026-23869, CVE-2026-44573 / 44574 / 44575 / 44578 / 44579 / 45109 May 2026 cluster), React + react-server-dom-* (CVE-2025-55182, CVE-2026-23870), Express, Hono pre-4.12.18 cluster, @vitejs/plugin-rsc, Strapi content-type-builder (CVE-2026-22599)
|
|
181
181
|
**Auth:** Clerk middleware bypass (GHSA-vqx2), Clerk `has()` org/billing/reverification bypass (GHSA-w24r), Clerk `clerkFrontendApiProxy` SSRF (CVE-2026-34076), NextAuth.js (2 CVEs), jsonwebtoken
|
|
182
182
|
**ORMs / SQL:** Drizzle SQL identifier injection (CVE-2026-39356) + Drizzle `sql.raw` interpolation (VG1073), MikroORM SQL injection (CVE-2026-44680), Prisma raw-query call-form, Kysely JSON-path traversal (CVE-2026-44635)
|
|
@@ -242,7 +242,7 @@ Malicious postinstall scripts, unpinned GitHub Actions, CI `npm` provenance / `-
|
|
|
242
242
|
|
|
243
243
|
All scanning tools support `format: "json"` for machine-readable output.
|
|
244
244
|
|
|
245
|
-
## Security Rules (
|
|
245
|
+
## Security Rules (433 rules across 25 modules)
|
|
246
246
|
|
|
247
247
|
| Category | Rules | Coverage |
|
|
248
248
|
|----------|-------|----------|
|
|
@@ -457,7 +457,7 @@ If your AI agent cannot connect to GuardVibe:
|
|
|
457
457
|
|
|
458
458
|
1. **Restart your IDE/agent.** MCP servers are started by the host application. After running `npx guardvibe init`, restart Claude Code, Cursor, or Gemini CLI for the config to take effect.
|
|
459
459
|
2. **Check the config path.** Run `npx guardvibe init claude` again and verify the output shows the correct config file location (`.mcp.json` in your project root for Claude Code, `.cursor/mcp.json` for Cursor).
|
|
460
|
-
3. **Re-run `init` to upgrade.** When upgrading GuardVibe, re-run `npx guardvibe init claude` — `.mcp.json` is pinned to a specific version (e.g. `guardvibe@3.1.
|
|
460
|
+
3. **Re-run `init` to upgrade.** When upgrading GuardVibe, re-run `npx guardvibe init claude` — `.mcp.json` is pinned to a specific version (e.g. `guardvibe@3.1.33`) at init time for fast deterministic startup. As of v3.1.2 the re-run also rewrites stale pins automatically (`Upgraded GuardVibe pin (3.1.27 → 3.1.28)`); since v3.1.27 the PostToolUse hook command is pinned to the same version (was `@latest`) and re-run upgrades a stale hook too. The same applies to `npx guardvibe hook install` and `npx guardvibe ci github` (since v3.1.3) — both are version-pinned at install/generate time and re-run to upgrade.
|
|
461
461
|
4. **Pre-3.1.1 users won't see the auto-update banner.** GuardVibe started writing a once-per-day "newer version available" notice to stderr in v3.1.1. If your install predates that, you'll never see it — run `npx -y guardvibe@latest init <host>` once to bake in the latest pin and start receiving banners on subsequent sessions.
|
|
462
462
|
5. **Verify Node.js version.** GuardVibe requires Node.js >= 18.0.0. Check with `node --version`.
|
|
463
463
|
6. **Check npx cache.** If you upgraded GuardVibe and the old version is cached, run `npx -y guardvibe@latest` to force the latest version.
|
|
@@ -20,7 +20,7 @@ export const apiSecurityRules = [
|
|
|
20
20
|
severity: "critical",
|
|
21
21
|
owasp: "API1:2023 Broken Object Level Authorization",
|
|
22
22
|
description: "Delete or update operation uses user-supplied ID without verifying resource ownership. Any authenticated user can modify or delete other users' resources.",
|
|
23
|
-
pattern: /(?:delete|update|destroy|remove)\s*\(\s*\{?\s*(?:where\s*:\s*\{)?\s*(?:id|_id)\s*:\s*(?:req\.(?:params|query|body)|params\.|args\.|input\.)(?:(?!userId|user_id|ownerId|owner_id|createdBy|created_by)[\s\S]){0,200}?\}/gi,
|
|
23
|
+
pattern: /(?:delete|update|destroy|remove)\s*\(\s*\{?\s*(?:where\s*:\s*\{)?\s*(?:id|_id)\s*:\s*(?:req\.(?:params|query|body)|params\.|args\.|input\.)(?:(?!userId|user_id|ownerId|owner_id|createdBy|created_by|author|authorId|author_id|email|userEmail|accountId|account_id|tenantId|tenant_id|orgId|org_id|organizationId)[\s\S]){0,200}?\}/gi,
|
|
24
24
|
languages: ["javascript", "typescript"],
|
|
25
25
|
fix: "Include the authenticated user's ID in the where clause to prevent unauthorized modifications.",
|
|
26
26
|
fixCode: '// Scope mutations to the authenticated user\nconst { userId } = await auth();\nawait prisma.post.delete({\n where: { id: params.id, userId }, // ownership!\n});',
|
package/build/data/rules/core.js
CHANGED
|
@@ -100,14 +100,14 @@ export const coreRules = [
|
|
|
100
100
|
},
|
|
101
101
|
{
|
|
102
102
|
id: "VG013",
|
|
103
|
-
name: "NoSQL injection risk",
|
|
103
|
+
name: "ORM/NoSQL query injection risk",
|
|
104
104
|
severity: "high",
|
|
105
105
|
owasp: "A02:2025 Injection",
|
|
106
|
-
description: "User input passed directly
|
|
106
|
+
description: "User input passed directly into an ORM/NoSQL query filter object — a MongoDB/Mongoose .find()/.findOne() or a SQL-ORM where clause (Sequelize .find({where}), TypeORM). When the value is an object instead of a scalar, an attacker can inject query operators (MongoDB $ne/$gt/$where, or Sequelize string-operator aliases like $gt/$ne in v4) to bypass authentication or filters. Express parses req.query via qs into nested objects, so query-param values reach the filter as objects unless coerced.",
|
|
107
107
|
pattern: /(?:find|findOne|updateOne|deleteOne|aggregate)\s*\(\s*\{[^}]*(?:req\.|request\.|body\.|params\.)/gi,
|
|
108
108
|
languages: ["javascript", "typescript"],
|
|
109
|
-
fix: "
|
|
110
|
-
fixCode: "//
|
|
109
|
+
fix: "Coerce filter values to scalars before querying and reject objects where strings are expected: const id = typeof req.params.id === 'string' ? req.params.id : ''. For Mongoose use schema validation; for Sequelize wrap values (String(req.query.id)) and never spread raw req objects into a where clause.",
|
|
110
|
+
fixCode: "// Coerce to a scalar before using in any ORM/NoSQL query filter\nconst id = typeof req.params.id === 'string' ? req.params.id : '';\n// Mongoose: await collection.findOne({ _id: new ObjectId(id) });\n// Sequelize: await Model.findOne({ where: { id: String(req.query.id) } });",
|
|
111
111
|
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
112
112
|
},
|
|
113
113
|
{
|
|
@@ -733,4 +733,52 @@ export const cveVersionRules = [
|
|
|
733
733
|
fixCode: '// package.json\n"overrides": { "node-ipc": "^12.0.0" }\n\n// pnpm-only\n// "pnpm": { "overrides": { "node-ipc": "^12.0.0" } }\n\n// yarn classic / berry\n// "resolutions": { "node-ipc": "^12.0.0" }',
|
|
734
734
|
compliance: ["SOC2:CC7.1", "SOC2:CC8.1", "PCI-DSS:Req6.2"],
|
|
735
735
|
},
|
|
736
|
+
{
|
|
737
|
+
id: "VG1076",
|
|
738
|
+
name: "Vitest UI Server Arbitrary File Read/Execute (CVE-2026-47429 / GHSA-5xrq-8626-4rwp)",
|
|
739
|
+
severity: "critical",
|
|
740
|
+
owasp: "A01:2025 Broken Access Control",
|
|
741
|
+
description: "vitest before 4.1.0 ships a UI/API server that, when listening, lets an attacker on the same network (or via a browser request when the dev server is exposed) read and execute arbitrary files on the developer or CI host. Vitest is a near-ubiquitous test runner, so a vulnerable pin in devDependencies exposes any machine that runs `vitest --ui` or leaves the API server bound during CI.",
|
|
742
|
+
pattern: /["']vitest["']\s*:\s*["'](?:\^|~|>=?)?\s*(?:[0-3]\.\d+\.\d+|4\.0\.\d+)["']/g,
|
|
743
|
+
languages: ["json"],
|
|
744
|
+
fix: "Upgrade vitest to 4.1.0+: npm install -D vitest@latest. Never expose the Vitest UI/API server beyond localhost, and bind it to 127.0.0.1 in CI rather than 0.0.0.0.",
|
|
745
|
+
fixCode: '// package.json\n"vitest": "^4.1.0" // or latest\n\n// vitest.config — keep the API/UI local-only\nexport default { test: { api: { host: "127.0.0.1" } } };',
|
|
746
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.2"],
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
id: "VG1077",
|
|
750
|
+
name: "Vitest Browser Mode Inline-Script XSS via otelCarrier (CVE-2026-47428 / GHSA-2h32-95rg-cppp)",
|
|
751
|
+
severity: "critical",
|
|
752
|
+
owasp: "A02:2025 Injection",
|
|
753
|
+
description: "@vitest/browser 4.0.17 through 4.1.5 (and the 5.0.0-beta.0 -> 5.0.0-beta.2 line) serves the `otelCarrier` query parameter back as an unsanitized inline script in browser mode. An attacker who can influence the URL of the running browser-mode session achieves cross-site scripting / arbitrary script execution in the test browser context, which on CI can pivot to the runner.",
|
|
754
|
+
pattern: /["']@vitest\/browser["']\s*:\s*["'](?:\^|~|>=?)?\s*(?:4\.0\.(?:1[7-9]|[2-9]\d)|4\.1\.[0-5]|5\.0\.0-beta\.[0-2])["']/g,
|
|
755
|
+
languages: ["json"],
|
|
756
|
+
fix: "Upgrade @vitest/browser to 4.1.6+ (stable) or 5.0.0-beta.3+ (beta): npm install -D @vitest/browser@latest. Keep browser-mode sessions on localhost only.",
|
|
757
|
+
fixCode: '// package.json\n"@vitest/browser": "^4.1.6" // or latest',
|
|
758
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.7"],
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
id: "VG1078",
|
|
762
|
+
name: "LiquidJS Remote Code Execution (CVE-2026-45618 / GHSA-gf2q-c269-pqgc)",
|
|
763
|
+
severity: "critical",
|
|
764
|
+
owasp: "A03:2025 Injection",
|
|
765
|
+
description: "liquidjs before 10.26.0 is vulnerable to remote code execution when rendering templates whose content or context an attacker can influence. Apps that let users supply Liquid templates or template fragments (CMS themes, email builders, low-code field templating) can be driven to execute arbitrary code on the server.",
|
|
766
|
+
pattern: /["']liquidjs["']\s*:\s*["'](?:\^|~|>=?)?\s*(?:[0-9]\.\d+\.\d+|10\.(?:[01]?\d|2[0-5])\.\d+)["']/g,
|
|
767
|
+
languages: ["json"],
|
|
768
|
+
fix: "Upgrade liquidjs to 10.26.0+: npm install liquidjs@latest. Never render user-supplied Liquid templates without sandboxing; treat template source as untrusted input.",
|
|
769
|
+
fixCode: '// package.json\n"liquidjs": "^10.26.0" // or latest',
|
|
770
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
id: "VG1079",
|
|
774
|
+
name: "TinyMCE XSS Cluster — media/object & content injection (CVE-2026-47759/47760/47761/47762)",
|
|
775
|
+
severity: "high",
|
|
776
|
+
owasp: "A02:2025 Injection",
|
|
777
|
+
description: "TinyMCE before 5.11.1, 6.0.0 -> 7.9.2, and 8.0.0 -> 8.5.0 carries a cluster of stored/DOM XSS vulnerabilities (GHSA-vg35-5wq7-3x7w, GHSA-v98h-vmpc-fpqv, GHSA-q742-qvgc-gc2f, GHSA-mh5m-5hw4-5c69) including media-plugin `data-mce-object` injection. Any app embedding the TinyMCE editor and rendering its output can be driven to execute attacker-supplied script in another user's browser.",
|
|
778
|
+
pattern: /["']tinymce["']\s*:\s*["'](?:\^|~|>=?)?\s*(?:[1-4]\.\d+\.\d+|5\.(?:[0-9]|10)\.\d+|5\.11\.0|6\.\d+\.\d+|7\.[0-8]\.\d+|7\.9\.[0-2]|8\.[0-4]\.\d+|8\.5\.0)["']/g,
|
|
779
|
+
languages: ["json"],
|
|
780
|
+
fix: "Upgrade tinymce to 7.9.3+ (v7) or 8.5.1+ (v8): npm install tinymce@latest. The 5.x line has no fix — migrate off it. Always sanitize TinyMCE output server-side before rendering it to other users.",
|
|
781
|
+
fixCode: '// package.json\n"tinymce": "^8.5.1" // or "^7.9.3" for v7\n\n// Sanitize editor output before persisting/rendering\nimport DOMPurify from "isomorphic-dompurify";\n' + 'const clean = DOMPurify.sanitize(editorHtml);',
|
|
782
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.7"],
|
|
783
|
+
},
|
|
736
784
|
];
|
|
@@ -70,6 +70,77 @@ function isInComment(lines, lineNumber) {
|
|
|
70
70
|
trimmed.startsWith("<!--") ||
|
|
71
71
|
trimmed.startsWith("/*"));
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Compute the set of 1-based line numbers that fall inside a multi-line block
|
|
75
|
+
* comment (slash-star ... star-slash). `isInComment` only catches lines whose
|
|
76
|
+
* trimmed start is a comment marker, so a line like ` res.cookie(...)` sitting
|
|
77
|
+
* INSIDE a commented-out block (common in teaching repos that keep "Fix for X"
|
|
78
|
+
* demos inline) was scanned as live code — a false-positive class for VG100,
|
|
79
|
+
* VG042 and any other non-CVE rule. This is a string-aware lexer pass (skips
|
|
80
|
+
* markers that appear inside ' " ` strings and after a // line comment) so URLs
|
|
81
|
+
* (`http://`), division, and regex-ish literals don't spuriously open a block.
|
|
82
|
+
*/
|
|
83
|
+
function computeBlockCommentLines(code) {
|
|
84
|
+
const inBlock = new Set();
|
|
85
|
+
let line = 1;
|
|
86
|
+
let state = "code";
|
|
87
|
+
for (let i = 0; i < code.length; i++) {
|
|
88
|
+
const c = code[i];
|
|
89
|
+
const c2 = i + 1 < code.length ? code[i + 1] : "";
|
|
90
|
+
if (c === "\n") {
|
|
91
|
+
line++;
|
|
92
|
+
if (state === "line")
|
|
93
|
+
state = "code";
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
switch (state) {
|
|
97
|
+
case "code":
|
|
98
|
+
if (c === "/" && c2 === "/") {
|
|
99
|
+
state = "line";
|
|
100
|
+
i++;
|
|
101
|
+
}
|
|
102
|
+
else if (c === "/" && c2 === "*") {
|
|
103
|
+
state = "block";
|
|
104
|
+
inBlock.add(line);
|
|
105
|
+
i++;
|
|
106
|
+
}
|
|
107
|
+
else if (c === "'")
|
|
108
|
+
state = "sq";
|
|
109
|
+
else if (c === '"')
|
|
110
|
+
state = "dq";
|
|
111
|
+
else if (c === "`")
|
|
112
|
+
state = "tpl";
|
|
113
|
+
break;
|
|
114
|
+
case "block":
|
|
115
|
+
inBlock.add(line);
|
|
116
|
+
if (c === "*" && c2 === "/") {
|
|
117
|
+
state = "code";
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
case "sq":
|
|
122
|
+
if (c === "\\")
|
|
123
|
+
i++;
|
|
124
|
+
else if (c === "'")
|
|
125
|
+
state = "code";
|
|
126
|
+
break;
|
|
127
|
+
case "dq":
|
|
128
|
+
if (c === "\\")
|
|
129
|
+
i++;
|
|
130
|
+
else if (c === '"')
|
|
131
|
+
state = "code";
|
|
132
|
+
break;
|
|
133
|
+
case "tpl":
|
|
134
|
+
if (c === "\\")
|
|
135
|
+
i++;
|
|
136
|
+
else if (c === "`")
|
|
137
|
+
state = "code";
|
|
138
|
+
break;
|
|
139
|
+
// "line" state is exited at the newline handler above
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return inBlock;
|
|
143
|
+
}
|
|
73
144
|
/**
|
|
74
145
|
* Check if a match is inside a multi-line string literal (template literal,
|
|
75
146
|
* fixCode/description property, or string concatenation).
|
|
@@ -187,6 +258,7 @@ function hasAuthGuardPattern(code) {
|
|
|
187
258
|
}
|
|
188
259
|
// Pattern 3: function called with await that contains auth-like keywords in name
|
|
189
260
|
// Broad catch: any function name containing auth/session/permission/guard/verify/protect
|
|
261
|
+
// guardvibe-ignore VG153 — dotted-identifier path matcher; each `\w+\.` segment is dot-anchored, so backtracking is linear, not catastrophic
|
|
190
262
|
if (/await\s+(?:\w+\.)*\w*(?:auth|Auth|session|Session|permission|Permission|guard|Guard|verify|Verify|protect|Protect|check|Check|ensure|Ensure|require|Require|assert|Assert|authorize|Authorize)\w*\s*\(/i.test(code)) {
|
|
191
263
|
return true;
|
|
192
264
|
}
|
|
@@ -221,6 +293,7 @@ function hasRoleCheckPattern(code) {
|
|
|
221
293
|
/(?:requireAdmin|requireRole|checkAdmin|isAdmin|verifyAdmin|assertAdmin)\s*\(/i.test(code))
|
|
222
294
|
return true;
|
|
223
295
|
// await requireAdmin() with error check pattern (naming-agnostic admin guard)
|
|
296
|
+
// guardvibe-ignore VG153 — dotted-identifier path matcher; dot-anchored segments make backtracking linear
|
|
224
297
|
if (/await\s+(?:\w+\.)*\w*(?:Admin|admin)\w*\s*\([^)]*\)\s*;?\s*\n\s*if\s*\(/i.test(code))
|
|
225
298
|
return true;
|
|
226
299
|
return false;
|
|
@@ -346,6 +419,14 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
|
|
|
346
419
|
if (customPattern.test(code))
|
|
347
420
|
codeHasAuthGuard = true;
|
|
348
421
|
}
|
|
422
|
+
// Line numbers inside multi-line /* */ block comments — computed once per file
|
|
423
|
+
// (string-aware) so the per-match comment skip can drop matches on commented-out
|
|
424
|
+
// code whose own line doesn't start with a comment marker. Gated to languages that
|
|
425
|
+
// actually use C-style /* */ comments — YAML/Python/shell/Dockerfile/TOML use #, so
|
|
426
|
+
// a `/*` there (e.g. a `# .../health/*` path glob in a k8s manifest) is NOT a comment
|
|
427
|
+
// opener and must not suppress real findings.
|
|
428
|
+
const usesCStyleBlockComments = language === "javascript" || language === "typescript" || language === "go";
|
|
429
|
+
const blockCommentLines = usesCStyleBlockComments && code.includes("/*") ? computeBlockCommentLines(code) : null;
|
|
349
430
|
const effectiveRules = rules ?? owaspRules;
|
|
350
431
|
for (const rule of effectiveRules) {
|
|
351
432
|
if (!rule.languages.includes(language))
|
|
@@ -370,7 +451,7 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
|
|
|
370
451
|
// agent.get('/?q=' + sqlPayload) which match the regex but aren't database calls
|
|
371
452
|
// - VG042/VG678: HTTP-response/security-header rules (tests don't serve to real users)
|
|
372
453
|
const isTestFile = filePath && /(?:\.(?:[\w-]+-)?(?:spec|test|e2e|stories|cy)\.(?:ts|tsx|js|jsx|mjs|cjs)$|_test\.go$|\/__tests__\/|\/__mocks__\/|\/tests?\/|\/cypress\/|\/playwright\/|\/dockertest\/|\/testutil\/|\/testhelpers?\/|\/testfixtures?\/)/i.test(filePath);
|
|
373
|
-
if (isTestFile && ["VG001", "VG003", "VG062", "VG010", "VG011", "VG012", "VG013", "VG014", "VG042", "VG100", "VG130", "VG678", "VG955", "VG133", "VG1021", "VG409"].includes(rule.id))
|
|
454
|
+
if (isTestFile && ["VG001", "VG003", "VG062", "VG010", "VG011", "VG012", "VG013", "VG014", "VG042", "VG100", "VG130", "VG678", "VG955", "VG133", "VG1021", "VG409", "VG148", "VG424"].includes(rule.id))
|
|
374
455
|
continue;
|
|
375
456
|
// VG955 (Missing Pagination on List Endpoint): only fire on actual request-handling
|
|
376
457
|
// surfaces — API routes, App Router `route.{ts,tsx}`, pages/api, or Server Actions.
|
|
@@ -792,6 +873,10 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
|
|
|
792
873
|
const isMultiLineMatch = match[0].includes("\n");
|
|
793
874
|
if (!isMultiLineMatch && isInComment(lines, lineNumber))
|
|
794
875
|
continue;
|
|
876
|
+
// Single-line match sitting inside a /* ... */ block comment (its own line
|
|
877
|
+
// may not start with a comment marker) — commented-out dead code, skip.
|
|
878
|
+
if (!isMultiLineMatch && blockCommentLines?.has(lineNumber))
|
|
879
|
+
continue;
|
|
795
880
|
if (isInsideStringLiteral(lines, lineNumber, code, match.index))
|
|
796
881
|
continue;
|
|
797
882
|
}
|
|
@@ -853,6 +938,71 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
|
|
|
853
938
|
// e.g. `INVALID_PASSWORD = "5020"` — error code, not a credential.
|
|
854
939
|
if (/\b[A-Z][A-Z0-9_]*\s*=\s*["']\d+["']/.test(matchedLine))
|
|
855
940
|
continue;
|
|
941
|
+
// Skip UI/error message string variables: `invalidPasswordErrorMessage = "Invalid password"`.
|
|
942
|
+
// The identifier signals a user-facing message/label/error and the value is a prose phrase
|
|
943
|
+
// (letters + at least one space), not a credential. isHumanReadableString needs 4+ words;
|
|
944
|
+
// this catches shorter 2-3 word phrases when the name is clearly a message.
|
|
945
|
+
const msgPair = matchedLine.match(/\b([A-Za-z_][A-Za-z0-9_]*)\s*[:=]\s*["']([^"']{3,})["']/);
|
|
946
|
+
if (msgPair
|
|
947
|
+
&& /(?:message|msg|error|\berr\b|label|title|hint|text|placeholder|description|tooltip|notice|warning|caption|heading|prompt|copy)/i.test(msgPair[1])
|
|
948
|
+
&& /^[A-Za-z][A-Za-z .,!?'’()-]*\s[A-Za-z .,!?'’()-]+$/.test(msgPair[2]))
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
// VG138 (Plaintext Password Comparison): skip benign non-credential comparisons.
|
|
952
|
+
// (1) Confirm-password match: `req.body.password == req.body.cpassword` compares two
|
|
953
|
+
// user inputs from the same form, not a submission against a stored secret.
|
|
954
|
+
// (2) Emptiness/presence check: `password === ''` validates that a field was provided.
|
|
955
|
+
if (rule.id === "VG138") {
|
|
956
|
+
const matchedLine = lines[lineNumber - 1] ?? "";
|
|
957
|
+
if (/(?:cpassword|confirm[_]?password|password[_]?confirm(?:ation)?|password2|repeat[_]?password|retype[_]?password|verify[_]?password)/i.test(matchedLine))
|
|
958
|
+
continue;
|
|
959
|
+
if (/(?:password|passwd|pwd)\s*(?:===|!==|==|!=)\s*(['"])\1/i.test(matchedLine))
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
// VG1002 (MongoDB NoSQL Injection via Query Operators): a query operator only enables
|
|
963
|
+
// injection when its value is attacker-controlled. Skip ONLY when the operator's value is
|
|
964
|
+
// a pure literal (`{ $ne: true }`, `{ $gt: 5 }`, `{ $regex: "^a" }`) — a static internal
|
|
965
|
+
// filter. A value built from a variable, concatenation, or template interpolation
|
|
966
|
+
// (`$where: 'this.x == ' + id`, `$where: `...${id}``) is a real injection vector — keep it.
|
|
967
|
+
if (rule.id === "VG1002") {
|
|
968
|
+
const after = code.slice(match.index + match[0].length, match.index + match[0].length + 80);
|
|
969
|
+
const staticLiteral = /^\s*:\s*(?:true|false|null|-?\d+(?:\.\d+)?|'[^'`$+]*'|"[^"`$+]*")\s*[},\]]/.test(after);
|
|
970
|
+
if (staticLiteral)
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
// VG060 (Weak password hashing): MD5/SHA-1 have legitimate non-credential uses — file/
|
|
974
|
+
// build-artifact checksums, ETags, cache keys, content integrity. Skip when the context
|
|
975
|
+
// is clearly a checksum/digest-of-bytes (or a build-tool config) and not a password.
|
|
976
|
+
if (rule.id === "VG060") {
|
|
977
|
+
const isBuildConfig = filePath ? /(?:^|\/)(?:Gruntfile|gulpfile|webpack\.config|rollup\.config|vite\.config|esbuild|metro\.config)\.[cm]?[jt]s$/i.test(filePath) : false;
|
|
978
|
+
const start = Math.max(0, lineNumber - 5);
|
|
979
|
+
const window = lines.slice(start, lineNumber + 4).join("\n");
|
|
980
|
+
// NB: do NOT treat `.update(data)` / `.update(content)` as a checksum signal — `data`
|
|
981
|
+
// is too generic and `hash(data)` is exactly how weak password hashing looks. Require a
|
|
982
|
+
// file/byte-buffer or explicit checksum marker instead.
|
|
983
|
+
const looksLikeChecksum = /(?:readFileSync|createReadStream|\bBuffer\b|\.update\s*\(\s*(?:buffer|buf|fileBuffer)|fs\.read|\.md5\b|checksum|etag|integrity|cacheKey|cache[_-]?key|contentHash|fileHash|subresource)/i.test(window);
|
|
984
|
+
const looksLikePassword = /(?:password|passwd|\bpwd\b|credential|user\.pass|loginPass)/i.test(window);
|
|
985
|
+
if ((isBuildConfig || looksLikeChecksum) && !looksLikePassword)
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
// VG123 (SQL Injection via Template Literal) + VG010 (SQL injection): skip when the query
|
|
989
|
+
// is parameterized (sequelize bind/replacements or $1/:name placeholders) AND every ${...}
|
|
990
|
+
// interpolation is a safe transform (hash/encode/escape/number) — not raw user input. e.g.
|
|
991
|
+
// `query(`... email = $1 ... password = '${security.hash(req.body.password)}'`, { bind: [..] })`.
|
|
992
|
+
// VG010 is included because the VG010↔VG123 dedup makes VG010 take over the same line once
|
|
993
|
+
// VG123 is suppressed — without this the FP is just relabeled, not removed.
|
|
994
|
+
if (rule.id === "VG123" || rule.id === "VG010") {
|
|
995
|
+
const tplStart = code.indexOf("`", match.index);
|
|
996
|
+
if (tplStart !== -1) {
|
|
997
|
+
const tplEnd = code.indexOf("`", tplStart + 1);
|
|
998
|
+
const tpl = tplEnd !== -1 ? code.slice(tplStart + 1, tplEnd) : "";
|
|
999
|
+
const callCtx = code.slice(match.index, (tplEnd !== -1 ? tplEnd : match.index) + 200);
|
|
1000
|
+
const isParameterized = /\b(?:bind|replacements)\s*:/.test(callCtx) || /[=\s](?:\$\d+|:[a-zA-Z_]\w*)\b/.test(tpl);
|
|
1001
|
+
const interps = tpl.match(/\$\{[^}]*\}/g) || [];
|
|
1002
|
+
const allSafe = interps.length > 0 && interps.every(s => /\$\{\s*[\w$.]*(?:hash|sha\d*|md5|bcrypt|argon2?|hmac|digest|encode|escape|encodeURIComponent|toString|String|Number|parseInt|parseFloat)\b/i.test(s));
|
|
1003
|
+
if (isParameterized && allSafe)
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
856
1006
|
}
|
|
857
1007
|
// VG106 (Timing-Unsafe Secret Comparison): skip when one operand is a React useRef
|
|
858
1008
|
// pattern (`*Ref.current`). Refs hold local component state, not user-provided input,
|
|
@@ -884,6 +1034,7 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
|
|
|
884
1034
|
// - `z.enum(filterConfig.field.operators)` (TS `as const` config object) — when
|
|
885
1035
|
// the file has any `as const` cast, treat nested property access as static
|
|
886
1036
|
const matchedLine = lines[lineNumber - 1] ?? "";
|
|
1037
|
+
// guardvibe-ignore VG153 — dotted-identifier path matcher; dot-anchored segments make backtracking linear
|
|
887
1038
|
if (/z\.enum\s*\(\s*[\w$]+(?:\.[\w$]+)+/.test(matchedLine)) {
|
|
888
1039
|
if (/\.enumValues\b/.test(matchedLine))
|
|
889
1040
|
continue;
|
|
@@ -59,6 +59,25 @@ const SANITIZERS = [
|
|
|
59
59
|
/sanitizeHtml\s*\(/,
|
|
60
60
|
/xss\s*\(/,
|
|
61
61
|
];
|
|
62
|
+
/**
|
|
63
|
+
* A SQL sink is NOT injectable when the query is parameterized (sequelize
|
|
64
|
+
* bind/replacements, or $1 / :name placeholders) AND every ${...} interpolation
|
|
65
|
+
* in the template is a safe transform (hash/encode/escape/number) rather than raw
|
|
66
|
+
* user input. e.g. sequelize.query(`... email = $1 ... password = '${security.hash(pw)}'`,
|
|
67
|
+
* { bind: [req.body.email] }) — the only interpolation is a fixed-charset hash, and
|
|
68
|
+
* the user value is bound. Without this, the inline-source loop reports req.body.*
|
|
69
|
+
* appearing inside the hash() call as a SQLi flow (false positive).
|
|
70
|
+
*/
|
|
71
|
+
function isSafeParameterizedSqlSink(lines, sinkIdx) {
|
|
72
|
+
const ctx = lines.slice(sinkIdx, sinkIdx + 4).join("\n");
|
|
73
|
+
const parameterized = /\b(?:bind|replacements)\s*:/.test(ctx) || /[=\s](?:\$\d+|:[a-zA-Z_]\w*)\b/.test(ctx);
|
|
74
|
+
if (!parameterized)
|
|
75
|
+
return false;
|
|
76
|
+
const sinkLine = lines[sinkIdx] ?? "";
|
|
77
|
+
const tpl = (sinkLine.match(/`[^`]*`/) || [""])[0];
|
|
78
|
+
const interps = tpl.match(/\$\{[^}]*\}/g) || [];
|
|
79
|
+
return interps.every(s => /\$\{\s*[\w$.]*(?:hash|sha\d*|md5|bcrypt|argon2?|hmac|digest|encode|escape|encodeURIComponent|toString|String|Number|parseInt|parseFloat)\b/i.test(s));
|
|
80
|
+
}
|
|
62
81
|
function extractAssignments(lines) {
|
|
63
82
|
const assignments = [];
|
|
64
83
|
const assignPattern = /(?:const|let|var)\s+([\w]+)\s*=\s*(.*)/;
|
|
@@ -131,6 +150,8 @@ export function analyzeTaint(code, language, filePath) {
|
|
|
131
150
|
sink.pattern.lastIndex = 0;
|
|
132
151
|
if (!sink.pattern.test(line))
|
|
133
152
|
continue;
|
|
153
|
+
if (sink.type === "sql-injection" && isSafeParameterizedSqlSink(lines, i))
|
|
154
|
+
continue;
|
|
134
155
|
for (const tVar of taintedVars) {
|
|
135
156
|
if (line.includes(tVar.name)) {
|
|
136
157
|
const chain = [];
|
|
@@ -160,6 +181,8 @@ export function analyzeTaint(code, language, filePath) {
|
|
|
160
181
|
sink.pattern.lastIndex = 0;
|
|
161
182
|
if (!sink.pattern.test(line))
|
|
162
183
|
continue;
|
|
184
|
+
if (sink.type === "sql-injection" && isSafeParameterizedSqlSink(lines, i))
|
|
185
|
+
continue;
|
|
163
186
|
for (const source of TAINT_SOURCES) {
|
|
164
187
|
source.pattern.lastIndex = 0;
|
|
165
188
|
if (source.pattern.test(line)) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.33",
|
|
4
4
|
"mcpName": "io.github.goklab/guardvibe",
|
|
5
|
-
"description": "Security MCP for vibe coding.
|
|
5
|
+
"description": "Security MCP for vibe coding. 433 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. 67 CVE rules refreshed daily from GHSA/OSV/CISA KEV — Miasma @redhat-cloud-services compromise, Next.js May 2026 13-advisory cluster, Drizzle/MikroORM/Kysely SQL injection, Axios proxy-auth redirect leak, Hono setCookie attribute injection, Clerk SSRF, tRPC prototype pollution, @tanstack supply-chain, node-ipc protestware, OpenClaude sandbox bypass, plus the full AI-generated stack (Supabase, Stripe, Prisma, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK). 68 AI-native rules including OWASP MCP Top 10 tool-description prompt injection (VG1068), model-controlled sandbox-disable flag detection (VG1063), Session messenger exfil endpoint IOC (VG1075), and CI/CD supply-chain hardening (VG1070 npm --expect-provenance / --ignore-scripts enforcement).",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"guardvibe": "build/cli.js",
|