Nextjs security scan
Skill Dolphinllc/claude-security-skills/skills/defensive/web/nextjs-security-scan
Defensive security scan for Next.js App Router projects. Detects environment-variable leakage to the browser bundle, unauthenticated server actions, missing middleware auth, unsafe CORS on route handlers, and trust-boundary violations in revalidatePath/revalidateTag. Invoke when the user asks to "review", "audit", or "scan" a Next.js codebase, or when editing files under app/ or middleware.ts.From its SKILL.md
npx -y skills add Dolphinllc/claude-security-skills --skill nextjs-security-scanAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
5.8 KB, ~1.4k tokens by cl100k_base, as published. Nobody here has run it
Next.js Security Scan
Performs a defensive security review of a Next.js App Router project (Next.js 13+). Reports findings using the shared scoring schema.
Scope
Detects only — does not modify code. Targets:
app/**— Server Components, Client Components, server actions, route handlersmiddleware.ts/middleware.jsnext.config.{js,ts,mjs}.env*
Out of scope: dependency CVEs (use npm audit / Snyk), runtime DAST.
Procedure
- Enumerate target files via
Glob/Grep. Read each one once withRead. - Apply each rule in the table below. For every match, record a finding with severity, location, evidence, and fix.
- Emit the final report in the schema from
SCORING.md.
Rules
| ID | Severity | Detection | Fix |
|---|---|---|---|
| NEXTJS-ENV-001 | critical | process.env.NEXT_PUBLIC_* referencing a secret-shaped name (*_KEY, *_SECRET, *_TOKEN, *_PASSWORD, *_DSN) | Remove NEXT_PUBLIC_ prefix; access only in Server Components / route handlers |
| NEXTJS-ENV-002 | high | Secret-shaped env var read inside a file containing "use client" | Move read to a Server Component or route handler; pass derived non-secret value via props |
| NEXTJS-SA-001 | high | Exported async function in a "use server" file with no auth check (no call to auth(), getServerSession, cookies()-based check, or equivalent) before mutation | Add session check at the top of the action; return early on unauthenticated |
| NEXTJS-SA-002 | high | Server action accepts unvalidated FormData/object and passes fields directly to DB / fetch | Validate with zod/valibot schema before use |
| NEXTJS-MW-001 | high | middleware.ts has a matcher that excludes auth-sensitive routes (e.g., /api/admin, /dashboard) but no per-route check exists | Tighten matcher or add per-route guards |
| NEXTJS-MW-002 | medium | Middleware reads JWT but does not verify signature (jwt.decode instead of jwt.verify / jose.jwtVerify) | Use jose.jwtVerify with explicit issuer/audience |
| NEXTJS-RH-001 | high | route.ts returns response with Access-Control-Allow-Origin: * together with Access-Control-Allow-Credentials: true | Pin origin to an allowlist; never combine wildcard with credentials |
| NEXTJS-RH-002 | medium | route.ts POST/PUT/DELETE handler reads request.json() without schema validation before persisting | Validate with zod before use |
| NEXTJS-RH-003 | medium | route.ts does not set runtime and uses Node-only crypto/secrets (risk of accidental Edge migration losing protections) | Add export const runtime = 'nodejs' explicitly |
| NEXTJS-REVAL-001 | medium | revalidatePath / revalidateTag called with a path/tag built from request input | Whitelist allowed paths; never interpolate user input |
| NEXTJS-IMG-001 | medium | next.config images.remotePatterns uses ** host or omits pathname | Pin host and path prefix |
| NEXTJS-CSP-001 | medium | No Content-Security-Policy set in middleware.ts / next.config headers | Add CSP with nonce-based script-src |
| NEXTJS-DSI-001 | high | dangerouslySetInnerHTML with value not provably static (template literal, prop, state, fetched data) | Render as text, or sanitize with DOMPurify (server-side) |
| NEXTJS-LOG-001 | medium | console.log of request.headers, cookies(), session, or full request.body | Redact before logging; log IDs not payloads |
Wrong vs. right
NEXTJS-ENV-001 (env leak)
// ❌ Exposes the API key to every browser bundle
const key = process.env.NEXT_PUBLIC_ANTHROPIC_API_KEY;
// ✅ Server-only access
// app/api/chat/route.ts
export async function POST(req: Request) {
const key = process.env.ANTHROPIC_API_KEY;
// ...
}
NEXTJS-SA-001 (unauth server action)
// ❌ Anyone who can hit the form can call this
"use server";
export async function deleteUser(id: string) {
await db.user.delete({ where: { id } });
}
// ✅ Session-checked, schema-validated
"use server";
import { z } from "zod";
import { auth } from "@/auth";
const schema = z.object({ id: z.string().uuid() });
export async function deleteUser(input: unknown) {
const session = await auth();
if (!session?.user || session.user.role !== "admin") {
throw new Error("Unauthorized");
}
const { id } = schema.parse(input);
await db.user.delete({ where: { id } });
}
NEXTJS-RH-001 (CORS + credentials)
// ❌ Browsers will reject this in modern versions, but older clients won't
return new Response(data, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true",
},
});
// ✅ Origin allowlist
const ALLOWED = new Set(["https://app.example.com"]);
const origin = req.headers.get("origin") ?? "";
const allow = ALLOWED.has(origin) ? origin : "";
return new Response(data, {
headers: {
"Access-Control-Allow-Origin": allow,
"Access-Control-Allow-Credentials": "true",
"Vary": "Origin",
},
});
References
- Next.js Server Actions: https://nextjs.org/docs/app/api-reference/functions/server-actions
- Next.js Middleware: https://nextjs.org/docs/app/building-your-application/routing/middleware
- Next.js Environment Variables: https://nextjs.org/docs/app/building-your-application/configuring/environment-variables
- OWASP CORS: https://owasp.org/www-community/attacks/CORS_OriginHeaderScrutiny
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.