guardvibe 3.26.0 → 3.27.0
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 +9 -0
- package/build/tools/ast-engine.d.ts +12 -0
- package/build/tools/ast-engine.js +39 -0
- package/build/tools/taint-analysis.js +39 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ 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.27.0] - 2026-06-25
|
|
9
|
+
|
|
10
|
+
### Improved — AST engine: multi-hop SQL-injection taint (no rule/tool count change: 450 rules / 39 tools)
|
|
11
|
+
- **Multi-hop bare-variable SQL sinks.** Dataflow analysis now catches the case where a user-tainted SQL string is built into a *variable* and that bare variable is passed to a query sink (`const q = "SELECT ... " + req.body.x; db.sequelize.query(q)`). The inline taint patterns only match the dangerous string when it appears literally in the sink call, so they missed the variable-indirection (multi-hop) shape; the AST locates sinks whose first argument is a bare identifier and confirms it is a tainted SQL string before reporting.
|
|
12
|
+
- **High precision / zero-FP guarding:** reports only when the variable is user-tainted *and* its definition is provably a SQL string (carries SQL keywords) — a parameterized query (`db.query(q, [userVal])`) stays silent (the SQL string has no tainted source; the user value rides the bind array), as does a non-SQL `.query(opts)` or a sanitizer-wrapped service-layer build. Deterministic (bundled TypeScript parser).
|
|
13
|
+
- Corpus delta: 1 real SQL-injection caught that the inline patterns missed, zero false positives, zero drift on other rules. 7 new tests.
|
|
14
|
+
|
|
15
|
+
Gate green (build / lint / test / self-audit PASS / A / 0).
|
|
16
|
+
|
|
8
17
|
## [3.26.0] - 2026-06-25
|
|
9
18
|
|
|
10
19
|
### Improved — AST engine: inter-procedural & nested ownership for BOLA/IDOR (no rule/tool count change: 450 rules / 39 tools)
|
|
@@ -26,6 +26,18 @@ export declare function bolaOwnershipGuarded(code: string, filePath: string | un
|
|
|
26
26
|
* false on uncertainty so a genuinely unguarded mutation keeps firing.
|
|
27
27
|
*/
|
|
28
28
|
export declare function bolaMutationGuarded(code: string, filePath: string | undefined, line: number): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Find SQL-sink calls whose first argument is a BARE identifier (the multi-hop shape
|
|
31
|
+
* the inline regex can't see). Returns the 1-based sink line and the variable name so
|
|
32
|
+
* the taint engine can confirm the variable is a user-tainted SQL string before
|
|
33
|
+
* reporting. Empty (no suppression of other paths) when TypeScript is unavailable or
|
|
34
|
+
* the parse fails. The first argument must be a plain identifier — an inline
|
|
35
|
+
* string/template/concat is already covered by the regex sinks and is skipped here.
|
|
36
|
+
*/
|
|
37
|
+
export declare function bareVarSqlSinks(code: string, filePath?: string): Array<{
|
|
38
|
+
line: number;
|
|
39
|
+
varName: string;
|
|
40
|
+
}>;
|
|
29
41
|
/**
|
|
30
42
|
* True when the argument to a `new RegExp(...)` at `line` is PROVABLY a constant
|
|
31
43
|
* (a string literal, a variable assigned from a string literal, or the callback
|
|
@@ -348,6 +348,45 @@ export function bolaMutationGuarded(code, filePath, line) {
|
|
|
348
348
|
return true;
|
|
349
349
|
return hasInterProceduralOwnershipGuard(ts, sf, target);
|
|
350
350
|
}
|
|
351
|
+
// SQL sink methods whose FIRST argument is the query string. The inline taint regex
|
|
352
|
+
// only fires when that string is written literally in the sink call (backtick / `+`),
|
|
353
|
+
// so it misses the case where the SQL string was built into a VARIABLE and the bare
|
|
354
|
+
// variable is passed in (`db.sequelize.query(query)`). `.raw`/`$…Unsafe` are always
|
|
355
|
+
// raw SQL; `query`/`execute` are overloaded, so taint.ts gates them on the variable
|
|
356
|
+
// actually being a user-tainted SQL string.
|
|
357
|
+
const SQL_RAW_SINK_METHODS = new Set(["query", "execute", "raw", "$queryRawUnsafe", "$executeRawUnsafe"]);
|
|
358
|
+
/**
|
|
359
|
+
* Find SQL-sink calls whose first argument is a BARE identifier (the multi-hop shape
|
|
360
|
+
* the inline regex can't see). Returns the 1-based sink line and the variable name so
|
|
361
|
+
* the taint engine can confirm the variable is a user-tainted SQL string before
|
|
362
|
+
* reporting. Empty (no suppression of other paths) when TypeScript is unavailable or
|
|
363
|
+
* the parse fails. The first argument must be a plain identifier — an inline
|
|
364
|
+
* string/template/concat is already covered by the regex sinks and is skipped here.
|
|
365
|
+
*/
|
|
366
|
+
export function bareVarSqlSinks(code, filePath) {
|
|
367
|
+
const ts = getTs();
|
|
368
|
+
if (!ts)
|
|
369
|
+
return [];
|
|
370
|
+
let sf;
|
|
371
|
+
try {
|
|
372
|
+
sf = ts.createSourceFile(filePath ?? "file.ts", code, ts.ScriptTarget.Latest, true, scriptKindFor(ts, filePath));
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
const sinks = [];
|
|
378
|
+
const visit = (node) => {
|
|
379
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)
|
|
380
|
+
&& SQL_RAW_SINK_METHODS.has(node.expression.name.text)
|
|
381
|
+
&& node.arguments.length > 0 && ts.isIdentifier(node.arguments[0])) {
|
|
382
|
+
const line = sf.getLineAndCharacterOfPosition(node.arguments[0].getStart(sf)).line + 1;
|
|
383
|
+
sinks.push({ line, varName: node.arguments[0].text });
|
|
384
|
+
}
|
|
385
|
+
ts.forEachChild(node, visit);
|
|
386
|
+
};
|
|
387
|
+
visit(sf);
|
|
388
|
+
return sinks;
|
|
389
|
+
}
|
|
351
390
|
const ITER_METHODS = new Set(["map", "forEach", "some", "every", "filter", "find", "findIndex", "reduce", "flatMap"]);
|
|
352
391
|
/** First `const NAME = <initializer>` for NAME anywhere in the file (file-scope-ish). */
|
|
353
392
|
function findVarInit(ts, sf, name) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { isRuleDefinitionFile } from "./check-code.js";
|
|
7
7
|
import { looksMinified } from "../utils/constants.js";
|
|
8
|
+
import { bareVarSqlSinks } from "./ast-engine.js";
|
|
8
9
|
// User input sources (tainted data entry points)
|
|
9
10
|
const TAINT_SOURCES = [
|
|
10
11
|
{ pattern: /(?:req|request)\.(?:body|query|params|headers|cookies)\b/g, type: "http-input" },
|
|
@@ -300,6 +301,44 @@ export function analyzeTaint(code, language, filePath) {
|
|
|
300
301
|
});
|
|
301
302
|
}
|
|
302
303
|
}
|
|
304
|
+
// Multi-hop SQL injection: a user-tainted SQL string built into a VARIABLE and then
|
|
305
|
+
// passed BARE to a SQL sink (`const q = "SELECT ... " + req.body.x; db.query(q)`).
|
|
306
|
+
// The inline sink regexes only match the dangerous string in the sink call itself, so
|
|
307
|
+
// they miss the variable-indirection case. The AST locates sinks whose first argument
|
|
308
|
+
// is a bare identifier; we report only when that identifier is a tainted variable
|
|
309
|
+
// whose definition is provably a SQL string (contains SQL keywords) — high precision,
|
|
310
|
+
// and a parameterized query (`db.query(q, [userVal])`) stays silent because the SQL
|
|
311
|
+
// string `q` has no tainted source and the user value rides the bind array.
|
|
312
|
+
const SQL_KEYWORDS = /\b(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|UNION|DROP|INTO|JOIN)\b/i;
|
|
313
|
+
const hasSqlSinkCandidate = /\.\s*(?:query|execute|raw|\$queryRawUnsafe|\$executeRawUnsafe)\s*\(\s*[A-Za-z_$]/.test(code);
|
|
314
|
+
if (hasSqlSinkCandidate && SQL_KEYWORDS.test(code)) {
|
|
315
|
+
for (const site of bareVarSqlSinks(code, filePath)) {
|
|
316
|
+
const tv = taintedVars.find(v => v.name === site.varName);
|
|
317
|
+
if (!tv)
|
|
318
|
+
continue;
|
|
319
|
+
// The variable must provably hold a SQL string built from user input — its
|
|
320
|
+
// defining assignment line carries SQL keywords (so a non-SQL `.query(opts)` or a
|
|
321
|
+
// bind-parameter value never qualifies).
|
|
322
|
+
const def = lines[tv.line - 1] ?? "";
|
|
323
|
+
if (!SQL_KEYWORDS.test(def))
|
|
324
|
+
continue;
|
|
325
|
+
if (SANITIZERS.some(s => s.test(def)))
|
|
326
|
+
continue;
|
|
327
|
+
if (findings.some(f => f.sink.line === site.line && f.sink.type === "sql-injection"))
|
|
328
|
+
continue;
|
|
329
|
+
findings.push({
|
|
330
|
+
source: { type: tv.sourceType ?? "propagated", line: tv.line, variable: tv.name },
|
|
331
|
+
sink: { type: "sql-injection", line: site.line, code: (lines[site.line - 1] ?? "").trim().substring(0, 100) },
|
|
332
|
+
chain: [
|
|
333
|
+
`[SOURCE] ${tv.sourceType ?? "propagated"} -> ${tv.name} (line ${tv.line})`,
|
|
334
|
+
`[SINK] sql-injection (line ${site.line})`,
|
|
335
|
+
],
|
|
336
|
+
severity: "critical",
|
|
337
|
+
description: "A user-tainted SQL string is built into a variable and passed to a query sink, enabling SQL injection.",
|
|
338
|
+
fix: "Use parameterized queries with placeholder values (bind parameters); never concatenate user input into the SQL string.",
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
303
342
|
return findings;
|
|
304
343
|
}
|
|
305
344
|
export function formatTaintFindings(findings, format) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.27.0",
|
|
4
4
|
"mcpName": "io.github.goklab/guardvibe",
|
|
5
5
|
"description": "Security infrastructure your AI can't be — deterministic, current past your model's training cutoff, whole-repo-aware, author-independent. Security MCP for vibe coding. 450 rules, 39 tools, CLI + doctor. Prompt-level shift-left security (secure_prompt — embed security requirements BEFORE code generation), host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. 77 CVE rules refreshed daily from GHSA/OSV/CISA KEV — js-cookie cookie-attribute injection, PostCSS </style> stringify XSS, Axios proxy prototype-pollution gadget, Vite dev-server RCE, React Router 7 cluster, DOMPurify XSS, Better Auth bypass, 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",
|