guardvibe 3.1.17 → 3.1.19

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.
@@ -842,6 +842,63 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
842
842
  if (/\b\w*Ref\.current\b/.test(matchedLine))
843
843
  continue;
844
844
  }
845
+ // VG850 (AI Prompt Injection via User Input): only fire when at least one of the
846
+ // interpolations in the system-prompt template literal looks like user input. The
847
+ // pattern matches any `${...}` interpolation, but apps commonly compose system
848
+ // prompts from constants — `system: ` + "`${codePrompt}\\n...`" + ` — and these
849
+ // constants are not attacker-controlled. User-input shapes: `req.X`/`body.X`/
850
+ // `params.X`/`query.X`/`searchParams.X`, or bare identifiers named `userInput`,
851
+ // `userMessage`, `userPrompt`, `prompt`, `input`, `message`.
852
+ if (rule.id === "VG850") {
853
+ // Match starts at `system:` and ends at `${`. Look at the surrounding window
854
+ // for the rest of the template literal (it can span multiple lines).
855
+ const window = lines.slice(Math.max(0, lineNumber - 1), Math.min(lines.length, lineNumber + 6)).join("\n");
856
+ const interpolations = Array.from(window.matchAll(/\$\{([^}]+)\}/g)).map(m => m[1].trim());
857
+ if (interpolations.length > 0) {
858
+ const isUserInput = (expr) => /\b(?:req|request|body|query|params|searchParams)\.\w+/.test(expr) ||
859
+ /^(?:userInput|userMessage|userPrompt|prompt|input|message)\b/.test(expr) ||
860
+ /\buser(?:Input|Message|Prompt|Query|Text|Content|Data)\b/.test(expr);
861
+ if (!interpolations.some(isUserInput))
862
+ continue;
863
+ }
864
+ }
865
+ // VG999 (AI Request Without maxTokens): skip when the call uses structured output
866
+ // (`output: Output.array(...)` / `output: Output.object(...)`) — token usage is
867
+ // bounded by the schema, not free-form text. The match already excludes calls
868
+ // that explicitly set maxTokens; structured-output calls are similarly bounded.
869
+ // Need to look at a wider window than match[0] because the match terminates at
870
+ // `}` and `output:` may appear elsewhere in the call options.
871
+ if (rule.id === "VG999") {
872
+ if (/\boutput\s*:\s*Output\.(?:array|object|enum)\s*\(/i.test(match[0]))
873
+ continue;
874
+ }
875
+ // VG1027 (Conversation Messages Serialized to Client With System Role): skip when
876
+ // the response is built via a filter/conversion helper that strips the system role.
877
+ // Vercel AI SDK convention names: `convertToUIMessages` (recommended), `filterMessages`,
878
+ // `pickRole`, `sanitizeMessages`, `publicMessages`. Pattern matches up to the
879
+ // `messages` key but doesn't include the value, so check the surrounding lines.
880
+ if (rule.id === "VG1027") {
881
+ const surrounding = lines.slice(Math.max(0, lineNumber - 1), Math.min(lines.length, lineNumber + 3)).join("\n");
882
+ // No trailing \b — the helper names are CamelCase and continue with more word chars
883
+ // (convertToUIMessages, sanitizeMessagesForClient, etc.). Prefix match is intentional.
884
+ if (/\b(?:convertToUI|filterMessages|pickRole|sanitizeMessages|publicMessages|visibleMessages|userMessages|convertMessages)/i.test(surrounding))
885
+ continue;
886
+ }
887
+ // VG152 (Object Injection via Dynamic Property Access): only fire on bracket-key
888
+ // ASSIGNMENT (`obj[key] = ...`) — that's the prototype-pollution shape. Read-only
889
+ // bracket access (`obj[key]` on RHS, in conditional, in function arg) does not
890
+ // pollute prototypes; even with attacker-controlled key, you only get a read of
891
+ // an existing or undefined property. The rule's pattern triggers on `req.X` etc.
892
+ // upstream + `\w+[key]` somewhere within 100 chars, which fires on completely
893
+ // benign read patterns like `data[key]` inside `for (const key in data)` loops or
894
+ // `DEFAULT_REDIRECTS[key]` lookups against hardcoded constants. The match string
895
+ // ends at `]` (no trailing `=`), so look slightly past match end for the assignment.
896
+ if (rule.id === "VG152") {
897
+ const window = code.slice(match.index, match.index + match[0].length + 10);
898
+ const isAssignment = /\w+\s*\[\s*(?:key|field|prop|name|column|attr|param)\s*\]\s*=(?!=)/.test(window);
899
+ if (!isAssignment)
900
+ continue;
901
+ }
845
902
  // VG126 (Dynamic RegExp from User Input): skip when the variable name signals it has
846
903
  // already been escaped/sanitized (e.g. `escapedElement`, `safeQuery`, `sanitizedInput`).
847
904
  if (rule.id === "VG126") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.1.17",
3
+ "version": "3.1.19",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
5
  "description": "Security MCP for vibe coding. 390 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis, +25 AI-native rules (MCP supply-chain, RAG/vector poisoning, agent loop DoS, public-prefix LLM keys, sandbox bypass). 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",