security-nextjs

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
<overview>
Security audit patterns for Next.js applications covering environment variable exposure, Server Actions, middleware auth, API routes, and App Router security.
</overview> <rules>
<overview>
针对Next.js应用的安全审计模式,涵盖环境变量暴露、Server Actions、中间件认证、API路由以及App Router安全等内容。
</overview> <rules>

Environment Variable Exposure

环境变量暴露

The NEXT_PUBLIC_ Footgun

NEXT_PUBLIC_ 陷阱

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:
typescript
// 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_PUBLIC_* → 会被打包到客户端JavaScript中 → 对所有人可见
无前缀 → 仅服务端可用 → 适合存储密钥
审计步骤:
  1. grep -r "NEXT_PUBLIC_" . -g "*.env*"
  2. 对每个变量,思考:“如果这个内容出现在查看源代码中,我是否能接受?”
  3. 常见错误:
    • NEXT_PUBLIC_API_KEY
      (应该仅在服务端使用)
    • NEXT_PUBLIC_DATABASE_URL
      (绝对不能使用)
    • NEXT_PUBLIC_STRIPE_SECRET_KEY
      (应使用
      STRIPE_SECRET_KEY
安全模式:
typescript
// 仅服务端可用(API路由、Server Component、Server Action)
const apiKey = process.env.API_KEY; // ✓ 无NEXT_PUBLIC_前缀

// 客户端安全(真正公开的内容)
const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY; // ✓ 可公开的密钥

next.config.js
env
Is Always Bundled

next.config.js 中的
env
配置始终会被打包

Values set in
next.config.js
under
env
are inlined into the client bundle, even without
NEXT_PUBLIC_
. Treat them as public.
javascript
// ❌ Sensitive values here are exposed to the browser
module.exports = {
  env: {
    DATABASE_URL: process.env.DATABASE_URL,
  },
};
</rules> <vulnerabilities>
next.config.js
env
字段中设置的值会被内联到客户端包中,即使没有
NEXT_PUBLIC_
前缀也要将其视为公开内容。
javascript
// ❌ 此处的敏感值会暴露给浏览器
module.exports = {
  env: {
    DATABASE_URL: process.env.DATABASE_URL,
  },
};
</rules> <vulnerabilities>

Server Actions Security

Server Actions 安全

Missing Auth (Most Common Issue)

缺失认证(最常见问题)

typescript
// ❌ 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 } });
}
typescript
// ❌ 存在漏洞:无认证检查
"use server"
export async function deleteUser(userId: string) {
  await db.user.delete({ where: { id: userId } });
}

// ✓ 安全:认证 + 授权
"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

输入验证

typescript
// ❌ 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 });
}
typescript
// ❌ 信任客户端输入
"use server"
export async function updateProfile(data: any) {
  await db.user.update({ data });
}

// ✓ 使用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

API路由安全

App Router (app/api/*/route.ts)

App Router(app/api/*/route.ts)

typescript
// ❌ 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 });
  // ...
}
typescript
// ❌ 无认证
export async function GET(request: Request) {
  return Response.json(await db.users.findMany());
}

// ✓ 认证中间件
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)

Pages Router(pages/api/*.ts)

typescript
// Check for missing auth on all handlers
// Common issue: GET is public but POST has auth (inconsistent)
typescript
// 检查所有处理程序是否缺失认证
// 常见问题:GET接口公开但POST接口有认证(不一致)

Middleware Security

中间件安全

Auth in middleware.ts

middleware.ts中的认证

typescript
// 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*"],
};
typescript
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const token = request.cookies.get("session");
  
  // ❌ 仅检查是否存在
  if (!token) return NextResponse.redirect("/login");
  
  // ✓ 应该验证令牌
  // 但中间件难以轻松执行异步数据库调用!
  // 解决方案:使用next-auth中间件或验证JWT
}

// 关键:检查匹配器是否覆盖所有受保护路由
export const config = {
  matcher: ["/dashboard/:path*", "/admin/:path*", "/api/admin/:path*"],
};

Matcher Gaps

匹配器漏洞

typescript
// ❌ Forgot API routes
matcher: ["/dashboard/:path*"]
// Admin API at /api/admin/* is unprotected!

// ✓ Include API routes
matcher: ["/dashboard/:path*", "/api/admin/:path*"]
typescript
// ❌ 遗漏了API路由
matcher: ["/dashboard/:path*"]
// /api/admin/* 下的管理API未受保护!

// ✓ 包含API路由
matcher: ["/dashboard/:path*", "/api/admin/:path*"]

Headers & Security Config

请求头与安全配置

next.config.js

next.config.js

javascript
// 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
        ],
      },
    ];
  },
};
</vulnerabilities>
<severity_table>
javascript
// 检查安全请求头
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配置复杂 - 检查是否存在且不过于宽松
        ],
      },
    ];
  },
};
</vulnerabilities>
<severity_table>

Common Vulnerabilities

常见漏洞

IssueWhere to LookSeverity
NEXT_PUBLIC_ secrets
.env*
files
CRITICAL
Unauth'd Server Actions
app/**/actions.ts
HIGH
Unauth'd API routes
app/api/**/route.ts
,
pages/api/**
HIGH
Middleware matcher gaps
middleware.ts
HIGH
Missing input validationServer Actions, API routesHIGH
IDOR in dynamic routes
[id]
params without ownership check
HIGH
dangerouslySetInnerHTMLComponentsMEDIUM
Missing security headers
next.config.js
LOW
</severity_table>
<commands>
问题检查位置严重程度
NEXT_PUBLIC_前缀的密钥
.env*
文件
严重(CRITICAL)
未认证的Server Actions
app/**/actions.ts
高(HIGH)
未认证的API路由
app/api/**/route.ts
,
pages/api/**
高(HIGH)
中间件匹配器漏洞
middleware.ts
高(HIGH)
缺失输入验证Server Actions、API路由高(HIGH)
动态路由中的IDOR(不安全的直接对象引用)
[id]
参数未做归属检查
高(HIGH)
dangerouslySetInnerHTML组件中(MEDIUM)
缺失安全请求头
next.config.js
低(LOW)
</severity_table>
<commands>

Quick Grep Commands

快速Grep命令

bash
undefined
bash
undefined

Find NEXT_PUBLIC_ usage

查找NEXT_PUBLIC_的使用情况

grep -r "NEXT_PUBLIC_" . -g ".env" -g ".ts" -g ".tsx"
grep -r "NEXT_PUBLIC_" . -g ".env" -g ".ts" -g ".tsx"

Find next.config env usage (always bundled)

查找next.config中的env配置(始终会被打包)

rg -n 'env\s*:' next.config.*
rg -n 'env\s*:' next.config.*

Find Server Actions without auth

查找未添加认证的Server Actions

rg -l '"use server"' . | xargs rg -L '(getServerSession|auth(|getSession|currentUser)'
rg -l '"use server"' . | xargs rg -L '(getServerSession|auth(|getSession|currentUser)'

Find API routes

查找API路由

fd 'route.(ts|js)' app/api/
fd 'route.(ts|js)' app/api/

Find dangerouslySetInnerHTML

查找dangerouslySetInnerHTML的使用

rg 'dangerouslySetInnerHTML' . -g ".tsx" -g ".jsx"

</commands>
rg 'dangerouslySetInnerHTML' . -g ".tsx" -g ".jsx"

</commands>