guardvibe 3.1.32 → 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 +15 -0
- package/README.md +1 -1
- package/build/data/rules/api-security.js +1 -1
- package/build/data/rules/core.js +4 -4
- package/build/tools/check-code.js +152 -1
- package/build/tools/taint-analysis.js +23 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ 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
|
+
|
|
8
23
|
## [3.1.32] - 2026-06-06
|
|
9
24
|
|
|
10
25
|
### Added — 4 new CVE rules (429 → 433), sourced via `npm run intel`
|
package/README.md
CHANGED
|
@@ -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
|
{
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.33",
|
|
4
4
|
"mcpName": "io.github.goklab/guardvibe",
|
|
5
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",
|