mcpField
S
security-nextjs

security-nextjs

github.com/igorwarzochaセキュリティ122368 回インストール38 週間前に更新
リポジトリ

security-nextjs について

Next.js アプリを点検する開発者向けのセキュリティ監査スキルです。環境変数、Server Actions、ミドルウェア、API ルート、App Router のリスクを、検索コマンドと対策パターンで確認します。

主な機能

  1. 01環境変数の露出確認
  2. 02Server Actions の認証と検証の監査
  3. 03API ルートとミドルウェアの保護監査
  4. 04IDOR と危険な HTML の検出
  5. 05セキュリティヘッダーと設定の確認

活用シーン

  1. 01リリース前の Next.js アプリ監査
  2. 02クライアントバンドルに露出した秘密情報の特定
  3. 03ルートとアクションの認可確認
  4. 04ミドルウェアのマッチャー漏れ確認

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/security-nextjsを入力して読み込まれたか確認します。

同梱ファイルscripts/scan.sh

詳細

環境変数の露出、Server Actions、ミドルウェア認証、API ルート、App Router のセキュリティを対象とする、Next.js アプリケーションのセキュリティ監査パターンです。

環境変数の露出

NEXT_PUBLIC_ の落とし穴
NEXT_PUBLIC_* → Bundled into client JavaScript → Visible to everyone
No prefix     → Server-only → Safe for secrets

監査手順:

  1. grep -r "NEXT_PUBLIC_" . -g "*.env*"
  2. 各変数について、「これが view-source に表示されても問題ないか?」と確認します。
  3. よくある間違い:
    • 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* ファイルCRITICAL
認証のない Server Actionsapp/**/actions.tsHIGH
認証のない API ルートapp/api/**/route.ts、pages/api/**HIGH
middleware の matcher 対象漏れmiddleware.tsHIGH
入力値検証の欠如Server Actions、API ルートHIGH
動的ルートでの IDOR所有権チェックのない [id] パラメーターHIGH
dangerouslySetInnerHTMLコンポーネントMEDIUM
セキュリティヘッダーの欠如next.config.jsLOW

</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"

コメント