guardvibe 3.0.17 → 3.0.18
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.
|
@@ -152,11 +152,22 @@ function runChecks(files, root) {
|
|
|
152
152
|
const apiRoutes = files.routeHandlers
|
|
153
153
|
.map(r => r.path.replace(resolve(root), "").replace(/\\/g, "/"))
|
|
154
154
|
.filter(p => p.includes("/api/"));
|
|
155
|
+
// Check which routes are NOT covered by middleware matcher
|
|
156
|
+
// But exclude routes that have in-handler auth (requireAdmin, requireAuth, etc.)
|
|
157
|
+
const authGuardPattern = /requireAdmin|requireAuth|checkAuth|withAuth|getServerSession|auth\(\)|clerkClient|currentUser/;
|
|
155
158
|
const unprotectedApiRoutes = apiRoutes.filter(route => {
|
|
156
|
-
|
|
159
|
+
// Check if middleware matcher covers this route
|
|
160
|
+
const coveredByMatcher = matcherPaths.some(pattern => {
|
|
157
161
|
const normalized = pattern.replace(/:path\*/, "").replace(/\(.*?\)/, "");
|
|
158
162
|
return route.startsWith(normalized) || route.includes(normalized);
|
|
159
163
|
});
|
|
164
|
+
if (coveredByMatcher)
|
|
165
|
+
return false;
|
|
166
|
+
// Check if the route handler has in-handler auth guard
|
|
167
|
+
const handler = files.routeHandlers.find(r => r.path.replace(resolve(root), "").replace(/\\/g, "/") === route);
|
|
168
|
+
if (handler && authGuardPattern.test(handler.content))
|
|
169
|
+
return false;
|
|
170
|
+
return true;
|
|
160
171
|
});
|
|
161
172
|
if (unprotectedApiRoutes.length > 0) {
|
|
162
173
|
issues.push({
|
|
@@ -193,11 +204,23 @@ function runChecks(files, root) {
|
|
|
193
204
|
});
|
|
194
205
|
}
|
|
195
206
|
// Check for secrets in .env files that are also in NEXT_PUBLIC_
|
|
207
|
+
// Known safe public keys that are DESIGNED to be in client bundles
|
|
208
|
+
const knownPublicKeys = new Set([
|
|
209
|
+
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
|
210
|
+
"NEXT_PUBLIC_SUPABASE_URL",
|
|
211
|
+
"NEXT_PUBLIC_TURNSTILE_SITE_KEY",
|
|
212
|
+
"NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY",
|
|
213
|
+
"NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
|
|
214
|
+
"NEXT_PUBLIC_RECAPTCHA_SITE_KEY",
|
|
215
|
+
"NEXT_PUBLIC_GA_MEASUREMENT_ID",
|
|
216
|
+
"NEXT_PUBLIC_SITE_URL",
|
|
217
|
+
"NEXT_PUBLIC_APP_URL",
|
|
218
|
+
]);
|
|
196
219
|
for (const envFile of files.envFiles) {
|
|
197
220
|
const lines = envFile.content.split("\n");
|
|
198
221
|
for (const line of lines) {
|
|
199
222
|
const match = /^(NEXT_PUBLIC_\w*(?:SECRET|KEY|PASSWORD|TOKEN|PRIVATE|CREDENTIAL)\w*)\s*=/.exec(line);
|
|
200
|
-
if (match && !/PUBLISHABLE/i.test(match[1])) {
|
|
223
|
+
if (match && !/PUBLISHABLE/i.test(match[1]) && !knownPublicKeys.has(match[1])) {
|
|
201
224
|
issues.push({
|
|
202
225
|
id: "AC021", severity: "critical", category: "secrets",
|
|
203
226
|
title: `NEXT_PUBLIC_ exposes secret: ${match[1]}`,
|
|
@@ -280,6 +280,18 @@ const SINK_PATTERNS = [
|
|
|
280
280
|
{ pattern: /writeFileSync?\s*\(/g, type: "path-traversal" },
|
|
281
281
|
{ pattern: /readFileSync?\s*\(/g, type: "path-traversal" },
|
|
282
282
|
];
|
|
283
|
+
// Patterns that break the taint chain (validation/sanitization)
|
|
284
|
+
const SANITIZER_PATTERNS = [
|
|
285
|
+
/validate\w*\s*\(/i,
|
|
286
|
+
/sanitize\w*\s*\(/i,
|
|
287
|
+
/safeParse\s*\(/i,
|
|
288
|
+
/parseBody\s*\(/i,
|
|
289
|
+
/DOMPurify/i,
|
|
290
|
+
/encodeURIComponent\s*\(/i,
|
|
291
|
+
/\.hostname\s*!==?\s*/i,
|
|
292
|
+
/\.origin\s*!==?\s*/i,
|
|
293
|
+
/allowlist|whitelist|allowedHosts/i,
|
|
294
|
+
];
|
|
283
295
|
function checkParamFlowsToSink(paramName, body, startLine) {
|
|
284
296
|
const lines = body.split("\n");
|
|
285
297
|
const taintedNames = new Set([paramName]);
|
|
@@ -289,11 +301,20 @@ function checkParamFlowsToSink(paramName, body, startLine) {
|
|
|
289
301
|
if (m) {
|
|
290
302
|
for (const t of taintedNames) {
|
|
291
303
|
if (m[2].includes(t)) {
|
|
292
|
-
|
|
304
|
+
const isSanitized = SANITIZER_PATTERNS.some(p => p.test(m[2]));
|
|
305
|
+
if (!isSanitized) {
|
|
306
|
+
taintedNames.add(m[1]);
|
|
307
|
+
}
|
|
293
308
|
break;
|
|
294
309
|
}
|
|
295
310
|
}
|
|
296
311
|
}
|
|
312
|
+
// Break taint if value passes through validation
|
|
313
|
+
for (const t of taintedNames) {
|
|
314
|
+
if (line.includes(t) && SANITIZER_PATTERNS.some(p => p.test(line))) {
|
|
315
|
+
taintedNames.delete(t);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
297
318
|
}
|
|
298
319
|
for (let i = 0; i < lines.length; i++) {
|
|
299
320
|
const line = lines[i];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.18",
|
|
4
4
|
"mcpName": "io.github.goklab/guardvibe",
|
|
5
5
|
"description": "Security MCP for vibe coding. 335 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. 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",
|