security-nextjs by igorwarzocha/opencode-workflows
npx skills add https://github.com/igorwarzocha/opencode-workflows --skill security-nextjsNext.js 应用程序安全审计模式,涵盖环境变量暴露、Server Actions、中间件身份验证、API 路由和 App Router 安全性。
NEXT_PUBLIC_* → 打包进客户端 JavaScript → 对所有人可见
No prefix → 仅限服务器端 → 对密钥安全
审计步骤:
grep -r "NEXT_PUBLIC_" . -g "*.env*"NEXT_PUBLIC_API_KEY (应该仅限服务器端)NEXT_PUBLIC_DATABASE_URL (绝对不要使用)NEXT_PUBLIC_STRIPE_SECRET_KEY (使用 STRIPE_SECRET_KEY)安全模式:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// 仅限服务器端 (API 路由, Server Component, Server Action)
const apiKey = process.env.API_KEY; // ✓ 无 NEXT_PUBLIC_ 前缀
// 客户端安全 (真正公开)
const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY; // ✓ 可公开的密钥
env 总是会被打包在 next.config.js 的 env 下设置的值会被内联到客户端打包文件中,即使没有 NEXT_PUBLIC_ 前缀。请将它们视为公开的。
// ❌ 这里的敏感值会暴露给浏览器
module.exports = {
env: {
DATABASE_URL: process.env.DATABASE_URL,
},
};
// ❌ 存在漏洞:无身份验证检查
"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 } });
}
// ❌ 信任客户端输入
"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 });
}
// ❌ 无身份验证
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 });
// ...
}
// 检查所有处理程序是否缺少身份验证
// 常见问题:GET 是公开的,但 POST 有身份验证 (不一致)
// 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*"],
};
// ❌ 忘记了 API 路由
matcher: ["/dashboard/:path*"]
// 位于 /api/admin/* 的管理员 API 未受保护!
// ✓ 包含 API 路由
matcher: ["/dashboard/:path*", "/api/admin/:path*"]
// 检查安全响应头
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 很复杂 - 检查是否存在且不过于宽松
],
},
];
},
};
<severity_table>
| 问题 | 检查位置 | 严重性 |
|---|---|---|
| NEXT_PUBLIC_ 密钥 | .env* 文件 | 严重 |
| 未经验证的 Server Actions | app/**/actions.ts | 高 |
| 未经验证的 API 路由 | app/api/**/route.ts, pages/api/** | 高 |
| 中间件匹配器漏洞 | middleware.ts | 高 |
| 缺少输入验证 | Server Actions, API 路由 | 高 |
| 动态路由中的 IDOR | [id] 参数缺少所有权检查 | 高 |
| dangerouslySetInnerHTML | 组件 | 中 |
| 缺少安全响应头 | next.config.js | 低 |
</severity_table>
# 查找 NEXT_PUBLIC_ 使用情况
grep -r "NEXT_PUBLIC_" . -g "*.env*" -g "*.ts" -g "*.tsx"
# 查找 next.config env 使用情况 (总是会被打包)
rg -n 'env\s*:' next.config.*
# 查找没有身份验证的 Server Actions
rg -l '"use server"' . | xargs rg -L '(getServerSession|auth\(|getSession|currentUser)'
# 查找 API 路由
fd 'route\.(ts|js)' app/api/
# 查找 dangerouslySetInnerHTML
rg 'dangerouslySetInnerHTML' . -g "*.tsx" -g "*.jsx"
每周安装量
99
代码仓库
GitHub 星标数
95
首次出现
2026年1月24日
安全审计
安装于
opencode93
gemini-cli92
codex88
github-copilot87
cursor84
kimi-cli75
Security audit patterns for Next.js applications covering environment variable exposure, Server Actions, middleware auth, API routes, and App Router security.
NEXT_PUBLIC_* → Bundled into client JavaScript → Visible to everyone
No prefix → Server-only → Safe for secrets
Audit steps:
grep -r "NEXT_PUBLIC_" . -g "*.env*"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
env Is Always BundledValues 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,
},
};
// ❌ 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 });
}
// ❌ 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 });
// ...
}
// Check for missing auth on all handlers
// Common issue: GET is public but POST has auth (inconsistent)
// 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*"],
};
// ❌ Forgot API routes
matcher: ["/dashboard/:path*"]
// Admin API at /api/admin/* is unprotected!
// ✓ Include API routes
matcher: ["/dashboard/:path*", "/api/admin/:path*"]
// 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>
| Issue | Where to Look | Severity |
|---|---|---|
| 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 validation | Server Actions, API routes |
</severity_table>
# 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"
Weekly Installs
99
Repository
GitHub Stars
95
First Seen
Jan 24, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode93
gemini-cli92
codex88
github-copilot87
cursor84
kimi-cli75
xdrop 文件传输脚本:Bun 环境下安全上传下载工具,支持加密分享
37,500 周安装
长任务协调器:实现可恢复、有状态的长时间运行任务管理 | Agent Playbook
98 周安装
Prompt Agent - AI代理安全审计工具,自动化安全扫描与健康验证
135 周安装
ClawSec ClawHub 检查器 - 增强技能安装安全,集成 VirusTotal 信誉检查
139 周安装
Sanity Agent Context 智能体系统提示优化指南 - 塑造AI助手行为与边界
207 周安装
Astro框架专家:精通JavaScript/TypeScript,构建高性能静态网站与Web应用
144 周安装
React Native 高性能模式指南:列表、动画与导航优化技巧
305 周安装
| HIGH |
| IDOR in dynamic routes | [id] params without ownership check | HIGH |
| dangerouslySetInnerHTML | Components | MEDIUM |
| Missing security headers | next.config.js | LOW |