关于 security-nextjs
面向 Next.js 开发者的安全审计技能,涵盖环境变量、Server Actions、中间件、API 路由和 App Router,并提供搜索检查与修复模式。
主要功能
- 01检查环境变量是否泄露
- 02审查 Server Actions 的认证与校验
- 03审计 API 路由和中间件保护
- 04查找 IDOR 与不安全 HTML
- 05检查安全标头和配置
适用场景
- 01发布前审计 Next.js 应用
- 02查找客户端代码包中暴露的密钥
- 03审查路由与操作的授权
- 04检查中间件匹配器遗漏的路径
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ igorwarzocha/ opencode-workflows --skill security-nextjs安装后在 Claude Code 中输入/security-nextjs确认已加载。
附带文件scripts/scan.sh
详细介绍
针对 Next.js 应用的安全审计模式,涵盖环境变量暴露、Server Actions、中间件身份验证、API 路由和 App Router 安全。
环境变量暴露
NEXT_PUBLIC_ 陷阱
NEXT_PUBLIC_* → Bundled into client JavaScript → Visible to everyone
No prefix → Server-only → Safe for secrets
审计步骤:
grep -r "NEXT_PUBLIC_" . -g "*.env*"- 对每个变量都要问:「如果它出现在 view-source 中,我能接受吗?」
- 常见错误:
NEXT_PUBLIC_API_KEY(应该仅限服务器端使用)NEXT_PUBLIC_DATABASE_URL(绝不能使用)NEXT_PUBLIC_STRIPE_SECRET_KEY(应使用STRIPE_SECRET_KEY)
安全模式:
// 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 始终会打包
在 next.config.js 的 env 下设置的值会内联到客户端包中,即使没有 NEXT_PUBLIC_ 前缀也是如此。应将它们视为公开信息。
// ❌ Sensitive values here are exposed to the browser
module.exports = {
env: {
DATABASE_URL: process.env.DATABASE_URL,
},
};
Server Actions 安全
缺少身份验证(最常见问题)
// ❌ 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 } });
}
输入验证
// ❌ 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 路由安全
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.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 遗漏
// ❌ Forgot API routes
matcher: ["/dashboard/:path*"]
// Admin API at /api/admin/* is unprotected!
// ✓ Include API routes
matcher: ["/dashboard/:path*", "/api/admin/:path*"]
标头与安全配置
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>
常见漏洞
| 问题 | 检查位置 | 严重程度 |
|---|---|---|
| NEXT_PUBLIC_ 密钥 | .env* 文件 | 严重 |
| 未经身份验证的 Server Actions | app/**/actions.ts | 高 |
| 未经身份验证的 API 路由 | app/api/**/route.ts、pages/api/** | 高 |
| 中间件 matcher 遗漏 | middleware.ts | 高 |
| 缺少输入验证 | Server Actions、API 路由 | 高 |
| 动态路由中的 IDOR | 未进行所有权检查的 [id] 参数 | 高 |
| dangerouslySetInnerHTML | 组件 | 中 |
| 缺少安全标头 | next.config.js | 低 |
</severity_table>
快速 Grep 命令
# 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"
评论
支持 Markdown