mcpField
S
security-nextjs

security-nextjs

github.com/igorwarzochaSecurity122368 installsUpdated 38w ago
Repository

About security-nextjs

A security-audit skill for developers reviewing Next.js applications. It highlights risks in environment variables, Server Actions, middleware, API routes, and App Router code, with grep-based checks and remediation patterns.

Key features

  1. 01Checks for exposed environment variables
  2. 02Reviews Server Actions for auth and validation
  3. 03Audits API route and middleware protection
  4. 04Flags IDOR and unsafe HTML patterns
  5. 05Checks security headers and config

Use cases

  1. 01Audit a Next.js app before release
  2. 02Find secrets exposed through client bundles
  3. 03Review route and action authorization
  4. 04Check middleware matchers for uncovered paths

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/igorwarzocha/opencode-workflows --skill security-nextjs

After installing, type/security-nextjsin Claude Code to confirm it loaded.

Bundled filesscripts/scan.sh

Details

From the project README

Security audit patterns for Next.js applications covering environment variable exposure, Server Actions, middleware auth, API routes, and App Router security.

Environment Variable Exposure

The NEXT_PUBLIC_ Footgun
NEXT_PUBLIC_* → Bundled into client JavaScript → Visible to everyone
No prefix     → Server-only → Safe for secrets

Audit steps:

  1. grep -r "NEXT_PUBLIC_" . -g "*.env*"
  2. For each var, ask: "Would I be OK if this was in view-source?"
  3. Common mistakes:
    • NEXT_PUBLIC_API_KEY (SHOULD be server-only)
    • NEXT_PUBLIC_DATABASE_URL (MUST NOT use)
    • NEXT_PUBLIC_STRIPE_SECRET_KEY (use STRIPE_SECRET_KEY)

Safe pattern:

// Server-only (API route, Server Component, Server Action)
const apiKey = process.env.API_KEY; // ✓ No NEXT_PUBLIC_

// Client-safe (truly public)
const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY; // ✓ Publishable
next.config.js env Is Always Bundled

Values set in next.config.js under env are inlined into the client bundle, even without NEXT_PUBLIC_. Treat them as public.

// ❌ Sensitive values here are exposed to the browser
module.exports = {
  env: {
    DATABASE_URL: process.env.DATABASE_URL,
  },
};

Server Actions Security

Missing Auth (Most Common Issue)
// ❌ VULNERABLE: No auth check
"use server"
export async function deleteUser(userId: string) {
  await db.user.delete({ where: { id: userId } });
}

// ✓ SECURE: Auth + authorization
"use server"
export async function deleteUser(userId: string) {
  const session = await getServerSession();
  if (!session) throw new Error("Unauthorized");
  if (session.user.id !== userId && !session.user.isAdmin) {
    throw new Error("Forbidden");
  }
  await db.user.delete({ where: { id: userId } });
}
Input Validation
// ❌ Trusts client input
"use server"
export async function updateProfile(data: any) {
  await db.user.update({ data });
}

// ✓ Validates with Zod
"use server"
import { z } from "zod";
const schema = z.object({ name: z.string().max(100), bio: z.string().max(500) });
export async function updateProfile(formData: FormData) {
  const data = schema.parse(Object.fromEntries(formData));
  await db.user.update({ data });
}

API Routes Security

App Router (app/api/*/route.ts)
// ❌ No auth
export async function GET(request: Request) {
  return Response.json(await db.users.findMany());
}

// ✓ Auth middleware
import { getServerSession } from "next-auth";
export async function GET(request: Request) {
  const session = await getServerSession();
  if (!session) return new Response("Unauthorized", { status: 401 });
  // ...
}
Pages Router (pages/api/*.ts)
// Check for missing auth on all handlers
// Common issue: GET is public but POST has auth (inconsistent)

Middleware Security

Auth in middleware.ts
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const token = request.cookies.get("session");
  
  // ❌ Just checking existence
  if (!token) return NextResponse.redirect("/login");
  
  // ✓ SHOULD verify token
  // But middleware can't do async DB calls easily!
  // Solution: Use next-auth middleware or verify JWT
}

// CRITICAL: Check matcher covers all protected routes
export const config = {
  matcher: ["/dashboard/:path*", "/admin/:path*", "/api/admin/:path*"],
};
Matcher Gaps
// ❌ Forgot API routes
matcher: ["/dashboard/:path*"]
// Admin API at /api/admin/* is unprotected!

// ✓ Include API routes
matcher: ["/dashboard/:path*", "/api/admin/:path*"]

Headers & Security Config

next.config.js
// Check for security headers
module.exports = {
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [
          { key: "X-Frame-Options", value: "DENY" },
          { key: "X-Content-Type-Options", value: "nosniff" },
          { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
          // CSP is complex - check if present and not too permissive
        ],
      },
    ];
  },
};

<severity_table>

Common Vulnerabilities

IssueWhere to LookSeverity
NEXT_PUBLIC_ secrets.env* filesCRITICAL
Unauth'd Server Actionsapp/**/actions.tsHIGH
Unauth'd API routesapp/api/**/route.ts, pages/api/**HIGH
Middleware matcher gapsmiddleware.tsHIGH
Missing input validationServer Actions, API routesHIGH
IDOR in dynamic routes[id] params without ownership checkHIGH
dangerouslySetInnerHTMLComponentsMEDIUM
Missing security headersnext.config.jsLOW

</severity_table>

Quick Grep Commands

# Find NEXT_PUBLIC_ usage
grep -r "NEXT_PUBLIC_" . -g "*.env*" -g "*.ts" -g "*.tsx"

# Find next.config env usage (always bundled)
rg -n 'env\s*:' next.config.*

# Find Server Actions without auth
rg -l '"use server"' . | xargs rg -L '(getServerSession|auth\(|getSession|currentUser)'

# Find API routes
fd 'route\.(ts|js)' app/api/

# Find dangerouslySetInnerHTML
rg 'dangerouslySetInnerHTML' . -g "*.tsx" -g "*.jsx"

Comments

Markdown supported