guardvibe 1.9.2 → 1.9.3

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.
@@ -180,4 +180,28 @@ export const nextjsRules = [
180
180
  fixCode: '<!-- EJS: use escaped output -->\n<p><%= userInput %></p> <!-- SAFE: HTML-escaped -->\n<!-- NOT: <%- userInput %> DANGEROUS: raw HTML -->\n\n<!-- Handlebars: use double braces -->\n<p>{{userInput}}</p> <!-- SAFE: escaped -->\n<!-- NOT: {{{userInput}}} DANGEROUS: raw HTML -->\n\n<!-- Pug: use = not != -->\np= userInput //- SAFE: escaped\n//- NOT: p!= userInput DANGEROUS: raw HTML',
181
181
  compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
182
182
  },
183
+ {
184
+ id: "VG415",
185
+ name: "Cached Function Exposes User-Specific Data",
186
+ severity: "high",
187
+ owasp: "A01:2025 Broken Access Control",
188
+ description: "A function marked with 'use cache' accesses user-specific data (auth, session, cookies, headers) but caches the result. Cached data is shared across all users, leaking one user's data to others.",
189
+ pattern: /["']use cache["'][\s\S]{0,800}?(?:auth\s*\(|getServerSession|currentUser|getUser|cookies\s*\(|headers\s*\()/g,
190
+ languages: ["javascript", "typescript"],
191
+ fix: "Do not access user-specific data inside cached functions. Pass user-independent parameters only, or use cacheTag with user-specific tags.",
192
+ fixCode: '// BAD: caches user-specific data\n"use cache";\nasync function getData() {\n const { userId } = await auth(); // WRONG in cached fn!\n return db.items.findMany({ where: { userId } });\n}\n\n// GOOD: cache only shared data\n"use cache";\nasync function getPublicPosts() {\n return db.posts.findMany({ where: { published: true } });\n}',
193
+ compliance: ["SOC2:CC6.1"],
194
+ },
195
+ {
196
+ id: "VG416",
197
+ name: "Cached Function Without Revalidation Strategy",
198
+ severity: "medium",
199
+ owasp: "A05:2025 Security Misconfiguration",
200
+ description: "A function marked with 'use cache' does not specify a cacheLife or cacheTag for revalidation. Without explicit revalidation, stale data may be served indefinitely, including outdated security-sensitive information.",
201
+ pattern: /["']use cache["'](?:(?!cacheLife|cacheTag|unstable_cache)[\s\S]){10,}?(?:return|export)/g,
202
+ languages: ["javascript", "typescript"],
203
+ fix: "Add cacheLife() or cacheTag() inside cached functions to control revalidation.",
204
+ fixCode: '"use cache";\nimport { cacheLife, cacheTag } from "next/cache";\n\nasync function getCachedData() {\n cacheLife("hours");\n cacheTag("data-feed");\n return db.posts.findMany();\n}\n\n// Revalidate when data changes:\nimport { revalidateTag } from "next/cache";\nrevalidateTag("data-feed");',
205
+ compliance: ["SOC2:CC7.1"],
206
+ },
183
207
  ];
@@ -47,23 +47,38 @@ function isInsideStringLiteral(lines, lineNumber, code, matchIndex) {
47
47
  const line = lines[lineNumber - 1];
48
48
  if (!line)
49
49
  return false;
50
- const trimmed = line.trimStart();
51
- // Line is part of a template literal or multi-line string
52
- // Check if we're between backticks by counting unescaped backticks before this point
50
+ // 1. Template literal: count unescaped backticks before this point
53
51
  const before = code.substring(0, matchIndex);
54
52
  const backtickCount = (before.match(/(?<!\\)`/g) || []).length;
55
53
  if (backtickCount % 2 === 1)
56
- return true; // inside template literal
57
- // Line is a string property value (fixCode, description, fix, exploit, audit, fixCode)
58
- // Look backwards to find if this line is inside a property assignment string
54
+ return true;
55
+ // 2. The match line itself is a string continuation (starts with quote + or ends with +quote)
56
+ const trimmed = line.trimStart();
57
+ if (/^["']/.test(trimmed) && /\+\s*$/.test(line))
58
+ return true; // "string" +
59
+ if (/^\s*\+\s*["']/.test(line))
60
+ return true; // + "string continuation"
61
+ // 3. Line contains escaped newlines (\n) suggesting it's inside a string value
62
+ const quotesBefore = line.substring(0, line.indexOf(trimmed.charAt(0)));
63
+ if (/\\n/.test(line) && /["'`].*\\n/.test(line)) {
64
+ // Extra check: is the match portion inside quotes on this line?
65
+ const matchEnd = matchIndex + 20; // approximate
66
+ const lineStart = code.lastIndexOf("\n", matchIndex) + 1;
67
+ const col = matchIndex - lineStart;
68
+ const beforeCol = line.substring(0, col);
69
+ const singleQuotes = (beforeCol.match(/(?<!\\)'/g) || []).length;
70
+ const doubleQuotes = (beforeCol.match(/(?<!\\)"/g) || []).length;
71
+ if (singleQuotes % 2 === 1 || doubleQuotes % 2 === 1)
72
+ return true;
73
+ }
74
+ // 4. Look backwards for property assignment context (fixCode, description, etc.)
59
75
  for (let i = lineNumber - 1; i >= Math.max(0, lineNumber - 20); i--) {
60
76
  const prev = lines[i]?.trimStart() || "";
61
- // Property assignment with string value that likely spans lines
62
- if (/^(?:fixCode|fix|description|exploit|audit|fixCode)\s*[:=]/.test(prev))
77
+ if (/^(?:fixCode|fix|description|exploit|audit)\s*[:=]/.test(prev))
63
78
  return true;
64
79
  if (/^(?:fixCode|fix|description|exploit|audit)\s*:\s*$/.test(prev))
65
80
  return true;
66
- // Hit a rule boundary (id: "VG...) — stop looking
81
+ // Hit a rule boundary — stop looking
67
82
  if (/^\s*id\s*:\s*["']VG/.test(prev))
68
83
  break;
69
84
  if (/^\s*\{/.test(prev) && i < lineNumber - 2)
@@ -90,7 +105,32 @@ function isHumanReadableString(lines, lineNumber) {
90
105
  return true;
91
106
  return false;
92
107
  }
108
+ /**
109
+ * Detect if a file is a security rule definition file.
110
+ * These files intentionally contain vulnerable code patterns
111
+ * as regex matchers and fixCode examples — scanning them is meaningless.
112
+ */
113
+ function isRuleDefinitionFile(code, filePath) {
114
+ // Path-based: known rule definition directories
115
+ if (filePath && /(?:\/rules\/|\/data\/rules\/)/.test(filePath)) {
116
+ // Confirm it actually exports SecurityRule objects
117
+ if (/SecurityRule\s*\[\]/.test(code) && /id:\s*["']VG\d+["']/.test(code)) {
118
+ return true;
119
+ }
120
+ }
121
+ // Content-based: file defines multiple VG rules with pattern: regex
122
+ if (/id:\s*["']VG\d+["']/g.test(code) && /pattern:\s*\//.test(code)) {
123
+ const ruleCount = (code.match(/id:\s*["']VG\d+["']/g) || []).length;
124
+ if (ruleCount >= 3)
125
+ return true; // 3+ rule definitions = rule file
126
+ }
127
+ return false;
128
+ }
93
129
  export function analyzeCode(code, language, framework, filePath, configDir, rules) {
130
+ // Skip files that are security rule definitions (they intentionally contain
131
+ // vulnerable code patterns as regex matchers and fixCode examples)
132
+ if (isRuleDefinitionFile(code, filePath))
133
+ return [];
94
134
  const config = loadConfig(configDir);
95
135
  const ignoreEntries = loadIgnoreFile(configDir || process.cwd());
96
136
  const findings = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "1.9.2",
3
+ "version": "1.9.3",
4
4
  "description": "Security MCP for vibe coding. 277 rules, 24 tools for Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
5
5
  "type": "module",
6
6
  "bin": {