Server Actionsのセキュリティ

Server Actionsは、Next.js App Routerにおけるサーバーサイドのデータ変更手段です。 "use server"ディレクティブを付与した関数は、クライアントから呼び出し可能な公開HTTPエンドポイントとして公開されます。 つまり、フォームから呼び出されるかどうかに関わらず、任意のHTTPリクエストで実行できてしまうのです。

入力バリデーション with Zod

Server Actionsに渡されるデータは、クライアントから送信されるため一切信頼できません。 Zodスキーマによる厳密なバリデーションが推奨されます。

// app/actions/update-profile.ts
'use server'

import { z } from 'zod'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'

// Zodスキーマで入力を厳密に定義
const UpdateProfileSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  bio: z.string().max(500).optional(),
})

export async function updateProfile(formData: FormData) {
  // 1. 認証チェック
  const session = await auth()
  if (!session?.user?.id) {
    throw new Error('認証が必要です')
  }

  // 2. 入力バリデーション(Zod)
  const parsed = UpdateProfileSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    bio: formData.get('bio'),
  })

  if (!parsed.success) {
    return { error: parsed.error.flatten().fieldErrors }
  }

  // 3. 認可チェック(自分自身のプロフィールのみ更新可能)
  const userId = session.user.id

  // 4. データ更新
  await db.user.update({
    where: { id: userId },
    data: parsed.data,
  })

  return { success: true }
}

クロージャの暗号化

Server Actionsでは、外部スコープの変数をクロージャとしてキャプチャできます。 Next.jsはこれらのクロージャ変数を自動的に暗号化してクライアントに送信します。 ビルドごとにプライベートキーが生成され、Server Actionが呼び出される際にサーバー側で復号されます。

// クロージャで機密データをキャプチャする例
export default async function Page() {
  // この変数はクロージャとして暗号化される
  const secretConfig = await getSecretConfig()

  async function submitAction() {
    'use server'
    // secretConfigはクライアントに暗号化された状態で渡される
    // サーバー実行時に復号される
    await processWithConfig(secretConfig)
  }

  return <form action={submitAction}>...</form>
}

CVE-2025-29927 — Middleware Bypass(CVSS 9.1)

2025年3月に公開されたCVE-2025-29927は、Next.jsのセキュリティ史上もっとも衝撃的な脆弱性の一つです。 攻撃者がx-middleware-subrequestヘッダーを偽装することで、 middlewareの認証チェックを完全にバイパスできるという致命的な問題でした。

項目 詳細
CVE ID CVE-2025-29927
CVSS スコア 9.1(Critical)
影響範囲 Next.js 11.1.4 〜 14.2.24, 15.0.0 〜 15.2.2
攻撃ベクトル x-middleware-subrequestヘッダーの偽装
影響 middleware認証の完全バイパス、管理画面への不正アクセス
修正バージョン v14.2.25, v15.2.3

この脆弱性の本質的な教訓は、middlewareを唯一のセキュリティ境界にしてはならないということです。 middlewareはパフォーマンスの最適化やリダイレクトには有効ですが、認証・認可の最終防衛線としては不十分です。

CVE-2025-55182 — React2Shell(CVSS 10.0)

2025年6月に公開されたCVE-2025-55182は、CVSS満点の10.0を記録した、 React Server Components(RSC)のFlightプロトコルに存在する安全でないデシリアライゼーション脆弱性です。 セキュリティ研究コミュニティでは「React2Shell」と呼ばれています。

項目 詳細
CVE ID CVE-2025-55182
CVSS スコア 10.0(Critical — 最高深刻度)
通称 React2Shell
攻撃メカニズム Flightプロトコルの安全でないデシリアライゼーション
影響 リモートコード実行(RCE)、サーバー完全掌握
悪用速度 公開から数時間以内にAPTグループが悪用
対策 即座のアップデート、WAFルールの適用

Flightプロトコルは、Server ComponentsのレンダリングResultをクライアントに送信するための React独自のシリアライゼーション形式です。React2Shellでは、このプロトコルのデシリアライゼーション処理に 型チェックの不備があり、攻撃者が細工したペイロードを送信することで、 サーバー側で任意のJavaScriptオブジェクトを生成・実行できました。

多層防御アーキテクチャ

CVE-2025-29927とCVE-2025-55182の教訓を踏まえ、Next.jsアプリケーションでは 多層防御(Defense in Depth)アーキテクチャの構築が不可欠です。 単一のセキュリティ層に依存するのではなく、複数の独立した防御層を重ねることで、 いずれかの層が突破されても被害を最小限に抑えます。

graph TB
    A[クライアント] --> B[Proxy層 / WAF]
    B --> C[Next.js Middleware]
    C --> D[Server Actions / Route Handlers]
    D --> E[DAL層\nData Access Layer]
    E --> F[データベース]

    B -.->|"レート制限\nIPフィルタリング\nWAFルール"| B
    C -.->|"リダイレクト\nヘッダー操作\n※認証の最終防衛線にしない"| C
    D -.->|"入力バリデーション(Zod)\n認証チェック"| D
    E -.->|"認可チェック\nクエリスコーピング\nデータサニタイズ"| E

    style B fill:#ef4444,stroke:#dc2626,color:#fff
    style C fill:#f97316,stroke:#ea580c,color:#fff
    style D fill:#eab308,stroke:#ca8a04,color:#fff
    style E fill:#22c55e,stroke:#16a34a,color:#fff
    style F fill:#3b82f6,stroke:#2563eb,color:#fff
多層防御アーキテクチャ: 各層が独立したセキュリティチェックを実施

DAL(Data Access Layer)パターン

Next.jsの公式ドキュメントが推奨するDALパターンは、 データベースアクセスを専用のレイヤーに集約し、すべてのデータ操作に対して 認証・認可チェックを強制する設計パターンです。

// lib/dal.ts — Data Access Layer
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
import { cache } from 'react'

// リクエスト単位でキャッシュされる認証チェック
export const verifySession = cache(async () => {
  const session = await auth()
  if (!session?.user) {
    redirect('/login')
  }
  return { userId: session.user.id, role: session.user.role }
})

// DAL関数: 認可チェック込みのデータ取得
export async function getUserPosts(targetUserId: string) {
  const session = await verifySession()

  // 認可: 自分の投稿のみ、またはadminロール
  if (session.userId !== targetUserId && session.role !== 'admin') {
    throw new Error('権限がありません')
  }

  return db.post.findMany({
    where: { authorId: targetUserId },
    orderBy: { createdAt: 'desc' },
  })
}

環境変数の安全な管理

Next.jsの環境変数には、クライアントに公開されるものとサーバー専用のものがあります。 この区別を正しく理解しないと、機密情報が漏洩する危険があります。

種別 プレフィックス アクセス可能な場所 用途
公開変数 NEXT_PUBLIC_ サーバー + クライアント(バンドルに含まれる) API URL、Analytics ID等
サーバー専用 プレフィックスなし サーバーのみ DBパスワード、APIシークレット等

server-onlyパッケージ

サーバー専用のコードが誤ってClient Componentにインポートされることを防ぐため、 server-onlyパッケージの使用が推奨されます。

// lib/secrets.ts
import 'server-only' // Client Componentからインポートするとビルドエラー

export function getSecretKey() {
  return process.env.SECRET_API_KEY
}

// ↓ Client Componentでインポートすると...
// Error: This module cannot be imported from a Client Component module.

Content Security Policy(CSP)

Content Security Policy(CSP)は、XSS(クロスサイトスクリプティング)攻撃を緩和するための HTTPレスポンスヘッダーです。Next.jsではNonceベースのCSPが推奨されています。

Nonceベース CSP

リクエストごとにランダムなNonce(一度限りの値)を生成し、 そのNonceが付与されたスクリプトのみ実行を許可する方式です。

// middleware.ts — NonceベースCSPの実装
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // リクエストごとにランダムなNonceを生成
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')

  const cspHeader = [
    "default-src 'self'",
    "script-src 'self' 'nonce-" + nonce + "' 'strict-dynamic'",
    "style-src 'self' 'nonce-" + nonce + "'",
    "img-src 'self' blob: data:",
    "font-src 'self'",
    "object-src 'none'",
    "base-uri 'self'",
    "form-action 'self'",
    "frame-ancestors 'none'",
    "upgrade-insecure-requests",
  ].join('; ')

  const response = NextResponse.next()
  response.headers.set('Content-Security-Policy', cspHeader)
  response.headers.set('x-nonce', nonce)

  return response
}

SRI(Subresource Integrity)は、外部リソースの整合性を検証するもう一つのアプローチですが、 Next.jsでは現時点で実験的サポートの段階です。 本番環境ではNonceベースのCSPを優先し、SRIは補助的に使用することを推奨します。

認証ベストプラクティス

CVE-2025-29927の教訓を踏まえ、Next.jsにおける認証の設計原則を整理します。

原則 説明 実装方法
Middlewareは補助的に使う リダイレクトやヘッダー操作には有効だが、唯一の認証層にしない Middleware + DAL層の二重チェック
DAL層で認可を強制 すべてのデータアクセスに認可チェックを含める verifySession()をDAL関数の先頭で呼び出し
Server Actionsで認証チェック すべてのServer Actionの先頭で認証を確認 auth()の結果を検証
proxy.tsパターンの限界を理解 proxy.tsは認証プロキシとして便利だが唯一のセキュリティ境界にしない 多層防御の一部として使用
セッション管理の一元化 セッション検証をキャッシュして重複リクエストを防止 cache()でリクエスト単位のキャッシュ

セルフホスティング

Next.jsはVercel以外の環境でもセルフホスティングが可能です。 ただし、Vercelが提供する最適化(Edge Network、ISRの分散キャッシュ等)は 自前で構築する必要があります。

リバースプロキシの必須性

セルフホスティングでは、Next.jsの前段にリバースプロキシ(Nginx、Caddy等)を配置することが 強く推奨されます。Next.jsの開発サーバーおよびプロダクションサーバーは、 DDoS対策やTLS終端などのインフラレベルのセキュリティ機能を持ちません。

# nginx.conf — Next.jsのリバースプロキシ設定例
upstream nextjs {
    server 127.0.0.1:3000;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/ssl/certs/example.com.pem;
    ssl_certificate_key /etc/ssl/private/example.com.key;

    # セキュリティヘッダー
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # レート制限
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    location / {
        proxy_pass http://nextjs;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

Dockerデプロイ

Next.jsの公式ドキュメントが推奨するDockerfileは、マルチステージビルドで 最小限のプロダクションイメージを生成します。

# Dockerfile — Next.jsのマルチステージビルド
FROM node:20-alpine AS base

# 依存関係のインストール
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

# ビルド
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# プロダクション
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT=3000

CMD ["node", "server.js"]

マルチインスタンスの暗号化キー統一

ロードバランサーの背後で複数のNext.jsインスタンスを実行する場合、 Server Actionsのクロージャ暗号化キーをすべてのインスタンスで統一する必要があります。

# .env.production — マルチインスタンス環境の暗号化キー設定
# すべてのインスタンスで同一の値を設定すること
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=your-32-byte-secret-key-here

# セッション管理にも共有シークレットが必要
AUTH_SECRET=your-auth-secret-here

Deployment Adapters API

Next.js v15.3で導入されたDeployment Adapters APIは、 各デプロイプラットフォームの差異を吸収する抽象化レイヤーです。 これにより、Vercel以外のプラットフォームへのデプロイが大幅に簡素化されました。

アダプター プラットフォーム 特徴
@vercel/next Vercel 最も最適化、Edge Functions・ISR・画像最適化がフルサポート
@next/bun Bun Runtime Bunネイティブランタイムでの高速実行
@netlify/next Netlify Netlify Edge FunctionsでのSSR
@opennextjs/cloudflare Cloudflare Workers Workersランタイムでのエッジ実行
@opennextjs/aws AWS Lambda Lambda + CloudFrontでのサーバーレスデプロイ
graph LR
    A[Next.js Application] --> B[Deployment Adapters API]
    B --> C[Vercel]
    B --> D[Bun]
    B --> E[Netlify]
    B --> F[Cloudflare Workers]
    B --> G[AWS Lambda]
    B --> H[Docker / セルフホスト]

    style A fill:#3b82f6,stroke:#2563eb,color:#fff
    style B fill:#8b5cf6,stroke:#6d28d9,color:#fff
    style C fill:#000,stroke:#333,color:#fff
    style D fill:#fbbd23,stroke:#f59e0b,color:#000
    style E fill:#00c7b7,stroke:#059669,color:#fff
    style F fill:#f97316,stroke:#ea580c,color:#fff
    style G fill:#ff9900,stroke:#ea580c,color:#fff
    style H fill:#22c55e,stroke:#16a34a,color:#fff
Deployment Adapters API: プラットフォームの差異を吸収する抽象化レイヤー

テスト戦略

Server Componentsの登場により、Next.jsアプリケーションのテスト戦略にも変化が求められています。 従来のユニットテスト中心のアプローチでは、サーバーとクライアントの境界を跨ぐ動作を 十分に検証できません。

推奨テスト比率: E2E 70% / ユニット 30%

Next.jsの公式ドキュメントは、Server Componentsを多用するアプリケーションでは E2Eテストを重視し、70:30(E2E:ユニット)の比率を推奨しています。 Server Componentsはサーバー上でのみ実行されるため、jsdomベースのユニットテストでは 実際の動作を正確に再現できないからです。

テスト種別 ツール 対象 特徴
E2Eテスト Playwright Server Components、ページ遷移、フォーム送信 実ブラウザで実行、サーバー・クライアント統合テスト
ユニットテスト Vitest ユーティリティ関数、Client Components 高速、モック可能、jsdom環境
コンポーネントテスト Testing Library Client Componentsのインタラクション ユーザー視点でのテスト
// e2e/profile-update.spec.ts — Playwrightによるe2eテスト
import { test, expect } from '@playwright/test'

test('プロフィール更新が正しく動作する', async ({ page }) => {
  // 認証済みの状態でプロフィールページにアクセス
  await page.goto('/profile')

  // Server Componentでレンダリングされたフォームを確認
  await expect(page.getByRole('heading', { name: 'プロフィール' })).toBeVisible()

  // フォーム入力(Server Actionのテスト)
  await page.getByLabel('名前').fill('テスト太郎')
  await page.getByLabel('自己紹介').fill('こんにちは')
  await page.getByRole('button', { name: '保存' }).click()

  // Server Actionの結果を確認
  await expect(page.getByText('保存しました')).toBeVisible()

  // リロードしてもデータが永続化されていることを確認
  await page.reload()
  await expect(page.getByLabel('名前')).toHaveValue('テスト太郎')
})

まとめ

Next.jsアプリケーションのセキュリティは、単一の対策では不十分です。 CVE-2025-29927とCVE-2025-55182が示したように、フレームワーク自体にも脆弱性は存在します。 Proxy層→Middleware→Server Actions→DAL層という多層防御を構築し、 いずれかの層が突破されても被害を最小限に抑える設計が求められます。

セルフホスティングでは、リバースプロキシの配置、暗号化キーの統一、 Deployment Adapters APIの活用が成功の鍵です。 テストでは、Server Componentsの特性を踏まえてE2Eテストを重視し、 Playwright + Vitestの組み合わせが推奨されます。

次章では、Next.jsと他のフレームワーク(Remix、SvelteKit、Astro、Nuxt.js)を多角的に比較し、 2026年以降のフロントエンド開発のトレンドと将来展望を考察します。

理解度チェック

問題 0 / 50%
Q1

CVE-2025-29927(middleware bypass)の教訓として最も重要なものはどれですか?

キーボード: 1〜4 で選択、Enter で回答