Next.js: セキュリティ強化とバージョン移行

最終更新:2026-08-26

セキュリティとバージョン移行は本番レベルのアプリケーションにおける 2 つの防御線です — 前者は外部攻撃を防ぎ、後者は技術スタックの負債をなくします。

1. 学習内容



2. あるセキュリティエンジニアの実話

(1) 課題:XSS 攻撃によるユーザーデータ漏洩

Bob のチームの TaskFlow SaaS プラットフォームがブラジルでローンチして 3 ヶ月後、セキュリティレポートを受け取りました:

"攻撃者が「プロジェクト名」フィールドに <script> タグを注入し、200 人以上のユーザーのセッション Cookie を盗むことに成功しました。調査の結果、ダッシュボードページの「プロジェクト名」フィールドが出力時に適切にエスケープされていなかったことが判明しました。"

セキュリティ監査では一連の問題が特定されました:

脆弱性 影響 深刻度
XSS(反射型) セッションハイジャック、データ盗難 🔴 重大
CSRF トークンなし クロスサイトリクエストフォージェリ 🟠 高リスク
HTTP セキュリティヘッダーなし クリックジャッキング、MIME スニッフィング 🟡 中程度
NEXT_PUBLIC_ の乱用 JS バンドルに API キーが露出 🔴 重大
依存関係の脆弱性 lodash — 既知の CVE、未パッチ 🟠 高リスク

同時に、技術的負債も蓄積しています — プロジェクトは Next.js 14 ベースですが、Next.js 16 は 6 ヶ月前にリリースされており、チームは古いバージョンの実行に伴うセキュリティリスクとパフォーマンスロスを懸念しています。

(2) セキュリティ強化 + バージョン移行のソリューション

Bob はセキュリティに 2 段階でアプローチします:

BASH
# ステップ 1: セキュリティ脆弱性の即時修正
npm audit fix
# next.config.js にセキュリティヘッダーを追加
# 全フォームに CSRF トークンを追加

# ステップ 2: バージョン移行(14→15→16)
npm install next@16 react@19 react-dom@19

(3) 成果

項目 強化前 強化後
セキュリティスコア(Mozilla Observatory) D (45/100) A+ (100/100)
既知の CVE 12 0
バージョン Next.js 14.2 Next.js 16.2 LTS
ビルド時間 45s 12s (Turbopack)
パフォーマンス(Lighthouse) 72 92


3. HTTP セキュリティヘッダーの設定

next.config.jsheaders 関数は HTTP レスポンスヘッダーを注入し、CSP、HSTS、X-Frame-Options などのセキュリティポリシーを上書きします。

100%
graph TB
    A[ユーザーリクエスト] --> B[Next.js アプリケーション]
    B --> C[headers 関数の処理]
    C --> D[セキュリティヘッダーの注入]
    D --> E[ブラウザセキュリティポリシー有効化]
    E --> F[CSP が XSS をブロック]
    E --> G[HSTS が HTTPS を強制]
    E --> H[X-Frame-Options がクリックジャッキングを防止]
    
    style C fill:#cce5ff
    style E fill:#d4edda
セキュリティヘッダー 目的 例の値
Content-Security-Policy XSS とデータ注入の防止 default-src 'self'
Strict-Transport-Security HTTPS の強制 max-age=31536000; includeSubDomains
X-Frame-Options クリックジャッキング防止 SAMEORIGIN
X-Content-Type-Options MIME スニッフィング防止 nosniff
Referrer-Policy リファラー情報の制御 strict-origin-when-cross-origin
Permissions-Policy ブラウザ API 権限の制御 camera=(), microphone=()

(1) next.config.js セキュリティヘッダー設定

JS
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',

  async headers() {
    return [
      {
        // 全ルートに適用
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: [
              "default-src 'self'",
              "script-src 'self' 'unsafe-eval' 'unsafe-inline' https://www.googletagmanager.com https://app.posthog.com",
              "style-src 'self' 'unsafe-inline'",
              "img-src 'self' blob: data: https://*.taskflow.io https://avatars.githubusercontent.com",
              "font-src 'self' data:",
              "connect-src 'self' https://api.taskflow.io https://app.posthog.com https://o450000000.ingest.sentry.io",
              "frame-ancestors 'none'",
              "form-action 'self'",
              "base-uri 'self'",
              "object-src 'none'"
            ].join('; ')
          },
          {
            key: 'Strict-Transport-Security',
            value: 'max-age=31536000; includeSubDomains; preload'
          },
          {
            key: 'X-Frame-Options',
            value: 'DENY'
          },
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff'
          },
          {
            key: 'Referrer-Policy',
            value: 'strict-origin-when-cross-origin'
          },
          {
            key: 'Permissions-Policy',
            value: [
              'camera=()',
              'microphone=()',
              'geolocation=()',
              'interest-cohort=()'
            ].join(', ')
          },
          {
            key: 'X-XSS-Protection',
            value: '1; mode=block'
          }
        ]
      },
      {
        // API ルートの追加セキュリティ制限
        source: '/api/(.*)',
        headers: [
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff'
          },
          {
            key: 'Cache-Control',
            value: 'no-store, no-cache, must-revalidate'
          }
        ]
      }
    ]
  }
}

module.exports = nextConfig

(2) CSRF トークン保護

TSX
// src/components/Form.tsx
'use client'

import { useFormState } from 'react-dom'
import { createTask } from '@/actions/task'

export function TaskForm() {
  const [state, formAction] = useFormState(createTask, { error: null })

  return (
    <form action={formAction}>
      {/* CSRF トークン隠しフィールド */}
      <input type="hidden" name="csrf_token" value={getCsrfToken()} />
      
      <input
        type="text"
        name="title"
        placeholder="タスク名"
        className="border p-2 rounded"
      />
      
      <button
        type="submit"
        className="bg-blue-600 text-white px-4 py-2 rounded"
      >
        タスクを作成
      </button>
      
      {state.error && (
        <p className="text-red-600 mt-2">{state.error}</p>
      )}
    </form>
  )
}

▶ サンプル: ヘッダーの検証

BASH
# セキュリティヘッダーの確認
curl -I https://taskflow.io

# 期待される出力
HTTP/2 200
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-eval'...
strict-transport-security: max-age=31536000; includeSubDomains
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
💻 出力:

TEXT 📖 参照専用
{"status":"ok","data":{}}


4. 環境変数の漏洩防止

NEXT_PUBLIC_ プレフィックスを持つ変数はクライアントサイドの JS バンドルにコンパイルされます — このプレフィックスでシークレットを保存するとブラウザに露出します。

100%
graph TB
    A[.env.local] --> B{環境変数プレフィックス}
    B -->|NEXT_PUBLIC_*| C[JS バンドルにインライン化]
    B -->|その他| D[サーバーサイドのみ]
    C --> E[ブラウザ DevTools で表示可能]
    D --> F[安全]
    
    style C fill:#f8d7da
    style D fill:#d4edda
    style E fill:#f8d7da
変数プレフィックス 使用範囲 セキュリティリスク 適用シナリオ
NEXT_PUBLIC_ クライアント + サーバー 🔴 高い(JS に露出) API 公開キー、プロジェクト名
プレフィックスなし サーバーのみ 🟢 安全 データベースパスワード、API キー
process.env.* ビルド時に注入 🟡 中程度(ビルド環境に依存) ビルド時に設定

(1) 環境変数セキュリティチェックリスト

BASH
# ✅ 安全: プレフィックスなしの変数を使用
DATABASE_URL=postgresql://user:pass@host:5432/db
AUTH_SECRET=super-secret-key-here
STRIPE_SECRET_KEY=sk_live_xxx
SENTRY_DSN=https://xxx@sentry.io/123
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE

# ✅ 正しい: NEXT_PUBLIC_ は公開キーに使用
NEXT_PUBLIC_POSTHOG_KEY=phc_publicKey
NEXT_PUBLIC_SENTRY_DSN=https://public@sentry.io/123
NEXT_PUBLIC_APP_URL=https://taskflow.io

# ❌ 危険: NEXT_PUBLIC_ でキーを保存しないでください
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_xxx  # ブラウザに露出!
NEXT_PUBLIC_DATABASE_URL=postgresql://...    # データベース接続が露出!

(2) .gitignore 設定

GITIGNORE
# .gitignore — 環境変数セキュリティ
.env
.env*.local
.env.development.local
.env.test.local
.env.production.local

# .env.example は見落とさないでください(テンプレートファイルです)
!.env.example

(3) 環境変数検証スクリプト

TS
// src/lib/env-validation.ts
// 機密環境変数がクライアントサイドでアクセス不可であることを確認
function validateEnv() {
  if (typeof window !== 'undefined') {
    const nextPublicVars = Object.keys(process.env)
      .filter(key => key.startsWith('NEXT_PUBLIC_'))
    
    // プライベート変数の誤った露出をチェック
    const sensitiveSuffixes = ['SECRET', 'KEY', 'PASSWORD', 'TOKEN', 'PRIVATE']
    
    for (const key of nextPublicVars) {
      const suffix = key.replace('NEXT_PUBLIC_', '')
      if (sensitiveSuffixes.some(s => suffix.toUpperCase().includes(s))) {
        console.warn(
          `[セキュリティ警告] ${key} は NEXT_PUBLIC_ で始まっていますが ` +
          `機密キーワードを含んでいます: ${suffix}。` +
          `これはクライアントバンドルに露出します!`
        )
      }
    }
  }
}

validateEnv()

▶ サンプル: バンドル内の露出変数のチェック

BASH
# ビルド後のクライアント JS にキーが含まれているか確認
npm run build

# ビルド成果物内の機密キーワードを検索
grep -r "sk_live_" .next/static/ 2>/dev/null || echo "✅ クライアントバンドルに Stripe シークレットはありません"
grep -r "DATABASE_URL" .next/static/ 2>/dev/null || echo "✅ クライアントバンドルに DATABASE_URL はありません"
grep -r "AUTH_SECRET" .next/static/ 2>/dev/null || echo "✅ クライアントバンドルに AUTH_SECRET はありません"
💻 出力:

TEXT 📖 参照専用
  ▲ Next.js 16.0.0

   最適化された本番ビルドを作成中 ...
 ✓ コンパイル成功
 ✓ 型のリントと検証
 ✓ ページデータの収集
 ✓ 静的ページの生成 (5/5)
 ✓ ページ最適化の最終処理

ルート (app)              サイズ     初回ロード JS
┌ ○ /                    5.1 kB       89 kB
├ ○ /about               2.3 kB       86 kB
└ λ /api/items           0 B          84 kB

○  (静的)   静的コンテンツとしてプリレンダリング
λ  (動的)   オンデマンドでサーバーレンダリング
✅ クライアントバンドルに Stripe シークレットはありません
✅ クライアントバンドルに DATABASE_URL はありません
✅ クライアントバンドルに AUTH_SECRET はありません


5. 依存関係のセキュリティ監査

(1) npm audit

BASH
# 基本監査
npm audit

# 深刻度レベルでフィルタリング
npm audit --audit-level=high

# 自動修復(互換性を壊す可能性あり)
npm audit fix

# パッチバージョンのみ修復
npm audit fix --target=patch

# JSON レポートの生成
npm audit --json > security-report.json

(2) Snyk 統合

BASH
# Snyk CLI のインストール
npm install -g snyk

# 認証
snyk auth

# プロジェクトのテスト
snyk test

# 継続的監視
snyk monitor

# Docker イメージスキャン
snyk container test taskflow:latest --file=Dockerfile
YAML
# .github/workflows/security.yml
name: Security Audit

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'  # 毎週月曜日午前 6 時

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: 依存関係のインストール
        run: npm ci
      
      - name: npm audit の実行
        run: npx audit-ci --high --report-type summary
        continue-on-error: true
      
      - name: Snyk セキュリティスキャン
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high
      
      - name: SARIF のアップロード
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: snyk.sarif
ツール 検出範囲 CI 統合 推奨される修正
npm audit Node.js 依存関係 ✅ 組み込み npm audit fix
Snyk 依存関係 + Docker + IaC ✅ Action 自動 PR 修正
GitHub Dependabot 依存関係バージョン ✅ 組み込み 自動 PR アップグレード
CodeQL ソースコードの脆弱性 ✅ Action コードレベル修正


6. v14 から v15 への移行ガイド

Next.js 15 の最大の変更点は params、cookies、headers の非同期処理です — これは最も破壊的な変更です。

100%
graph TB
    subgraph "v14 同期 API(廃止)"
        A[params.id]
        B[cookies().get()]
        C[headers().get()]
    end
    
    subgraph "v15 非同期 API(必須)"
        D[await params]
        E[await cookies()]
        F[await headers()]
    end
    
    G[generateMetadata も await が必要]
    H[全動的ルートを async に変更]
    
    D --> G
    E --> G
    F --> H
    
    style A fill:#f8d7da
    style B fill:#f8d7da
    style C fill:#f8d7da
    style D fill:#d4edda
    style E fill:#d4edda
    style F fill:#d4edda

(1) 移行チェックリスト

変更点 v14(旧) v15(新) 影響を受けるファイル
params params.id(同期) await params(非同期) page.tsxlayout.tsx
searchParams searchParams.page(同期) await searchParams(非同期) page.tsx
cookies cookies().get()(同期) await cookies()(非同期) layout.tsxmiddleware.ts
headers headers().get()(同期) await headers()(非同期) layout.tsxpage.tsx
generateMetadata params 同期 params 非同期 page.tsx
React React 18 React 19 グローバル
useFormState react-dom useActionState に名称変更 フォームコンポーネント

(2) 同期から非同期への移行例

TSX
// ❌ v14(旧) — 同期 params
export default function ProjectPage({
  params
}: {
  params: { id: string }
}) {
  return <div>プロジェクト {params.id}</div>
}

// ✅ v15(新) — 非同期 params
export default async function ProjectPage({
  params
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  return <div>プロジェクト {id}</div>
}
TSX
// ❌ v14 — 同期 generateMetadata
export function generateMetadata({ params }: { params: { id: string } }) {
  return { title: `プロジェクト ${params.id}` }
}

// ✅ v15 — 非同期 generateMetadata
export async function generateMetadata({
  params
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  return { title: `プロジェクト ${id}` }
}

▶ サンプル: cookies と headers の非同期処理

💻 出力:

TEXT 📖 参照専用
generateMetadata コンポーネントの UI をレンダリングします。
TSX
// ❌ v14 — 同期 cookies 取得
import { cookies } from 'next/headers'

export function Layout({ children }: { children: React.ReactNode }) {
  const token = cookies().get('session_token')
  return <div>{children}</div>
}

// ✅ v15 — 非同期 cookies 取得
import { cookies } from 'next/headers'

export default async function Layout({
  children
}: {
  children: React.ReactNode
}) {
  const cookieStore = await cookies()
  const token = cookieStore.get('session_token')
  return <div>{children}</div>
}
💻 出力:

TEXT 📖 参照専用
Layout コンポーネントの UI をレンダリングします。


7. v15 から v16 への移行ガイド

Next.js 16 は 3 つの主要な変更を導入します:proxy.ts による middleware の置き換えTurbopack のデフォルト化use cache の移行です。

100%
graph LR
    A[v15] --> B{v16 移行}
    B --> C[proxy.ts が middleware を置き換え<br/>設定可能なルーティングルール]
    B --> D[Turbopack デフォルト<br/>webpack 設定を削除]
    B --> E[use cache 移行<br/>fetch キャッシュ → cacheTag]
    C --> F[より宣言的な設定]
    D --> G[ビルドが 10 倍高速化]
    E --> H[より細かいキャッシュ制御]
    
    style A fill:#f8d7da
    style C fill:#d4edda
    style D fill:#d4edda
    style E fill:#d4edda
変更点 v15(旧) v16(新) 移行アクション
ルートインターセプト middleware.ts(関数的) proxy.ts(設定ベース) middleware のロジックを proxy.ts に抽出
ビルドツール webpack(デフォルト) Turbopack(デフォルト) カスタム webpack 設定を削除
キャッシュモデル fetch 自動キャッシュ use cache ディレクティブ データキャッシュを cacheTag に移行
React React 19 RC React 19 安定版 バージョンアップグレード
PPR experimental.ppr 安定版 experimental. プレフィックスを削除

(1) proxy.ts 設定(v16 の新しい API)

TS
// src/proxy.ts — v16 で middleware.ts を置き換え
// ============================================
// proxy.ts は宣言的なルーティングルール設定です
// ============================================
import { defineProxy } from 'next/proxy'

export default defineProxy({
  // ルートマッチングルール
  matcher: [
    // 認証が必要なパス
    { pathname: '/dashboard/:path*', requireAuth: true },
    { pathname: '/projects/:path*', requireAuth: true },
    { pathname: '/settings/:path*', requireAuth: true },
    
    // リダイレクトルール
    { pathname: '/old-blog/:slug*', redirect: '/blog/:slug*' },
    
    // 静的リソースをスキップ
    { pathname: '/_next/:path*', bypass: true },
    { pathname: '/favicon.ico', bypass: true }
  ],

  // 認証チェック
  async auth(request) {
    const session = await getSession(request)
    if (!session) {
      return new Response(null, {
        status: 307,
        headers: { Location: '/login' }
      })
    }
    return null  // 通過
  },

  // リクエストヘッダー注入
  async headers(request) {
    return {
      'x-request-id': crypto.randomUUID(),
      'x-user-locale': getPreferredLocale(request)
    }
  }
})

// ユーティリティ関数
async function getSession(request: Request) {
  const cookie = request.headers.get('cookie') || ''
  const token = cookie.split('session_token=')?.[1]?.split(';')?.[0]
  if (!token) return null
  return { userId: 'user_123', role: 'admin' }
}

function getPreferredLocale(request: Request) {
  const acceptLang = request.headers.get('accept-language') || 'en'
  return acceptLang.split(',')[0].split('-')[0]
}

(2) use cache 移行

TSX
// ❌ v15 — fetch キャッシュを使用
async function getProjects() {
  const res = await fetch('https://api.example.com/projects', {
    next: { revalidate: 300 }
  })
  return res.json()
}

// ✅ v16 — use cache + cacheTag を使用
import { cacheTag, cacheLife } from 'next/cache'

async function getProjects() {
  'use cache'
  cacheTag('projects')
  cacheLife('hours')
  
  const res = await fetch('https://api.example.com/projects')
  return res.json()
}

(3) Turbopack デフォルト設定

JS
// next.config.js — v16 簡略化バージョン
/** @type {import('next').NextConfig} */
const nextConfig = {
  // v16 では Turbopack がデフォルトです。設定不要
  // 以下の v15 設定を削除してください:
  // swcMinify: true,       // Turbopack デフォルト
  // compiler: { ... },     // すでに組み込み
  // webpack: (config) => config,  // カスタム webpack を削除
  
  output: 'standalone',
  
  // v16 PPR 安定版
  ppr: true,
  
  // 必要なカスタマイズを保持
  serverExternalPackages: ['@prisma/client'],
  images: {
    formats: ['image/avif', 'image/webp']
  }
}

module.exports = nextConfig

▶ サンプル: 完全移行前後のシステム比較

💻 出力:

TEXT 📖 参照専用
Next.js の設定が適用されました。変更はサーバー再起動時に有効になります。
TSX
// === v14 オリジナルコード ===
// page.tsx
export default function Page({ params }: { params: { id: string } }) {
  return <div>{params.id}</div>
}

// layout.tsx
import { cookies } from 'next/headers'
export default function Layout({ children }: { children: React.ReactNode }) {
  const theme = cookies().get('theme')
  return <div className={theme?.value}>{children}</div>
}

// middleware.ts
import { NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
  if (!request.cookies.has('token')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

// === v16 移行後 ===
// page.tsx
export default async function Page({
  params
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  return <div>{id}</div>
}

// layout.tsx
import { cookies } from 'next/headers'
export default async function Layout({
  children
}: {
  children: React.ReactNode
}) {
  const cookieStore = await cookies()
  const theme = cookieStore.get('theme')
  return <div className={theme?.value}>{children}</div>
}

// proxy.ts(middleware.ts を置き換え)
import { defineProxy } from 'next/proxy'
export default defineProxy({
  matcher: [{ pathname: '/:path*', requireAuth: true }],
  async auth(request) {
    const token = request.headers.get('cookie')?.includes('token')
    if (!token) return Response.redirect('/login')
    return null
  }
})
💻 出力:

TEXT 📖 参照専用
セクションに記載されている ▶ サンプル: 完全移行前後のシステム比較 コンポーネントの UI をレンダリングします。


8. 完全な例: セキュリティ強化 + バージョン移行ワークフロー

BASH
# ============================================
# security-migration.sh
# 機能: セキュリティ強化 + バージョン移行の完全スクリプト
# ============================================
#!/bin/bash
set -euo pipefail

echo "=== TaskFlow セキュリティ & 移行 ==="

# フェーズ 1: セキュリティ強化
echo ""
echo "--- フェーズ 1: セキュリティ強化 ---"

# 1.1 ランタイム依存関係の監査
echo "npm audit を実行中..."
npm audit --audit-level=high
npm audit fix --target=minor || true

# 1.2 ヘッダー検証
echo "セキュリティヘッダーレポートを生成中..."
curl -sI http://localhost:3000 | grep -E "^(content-security-policy|strict-transport-security|x-frame-options)" \
  || echo "警告: セキュリティヘッダーが不足しています!"

# 1.3 NEXT_PUBLIC 漏洩チェック
echo "漏洩したシークレットをチェック中..."
grep -r "NEXT_PUBLIC_.*SECRET\|NEXT_PUBLIC_.*KEY\|NEXT_PUBLIC_.*PASSWORD" \
  .env* 2>/dev/null && echo "警告: NEXT_PUBLIC にシークレットがあります!" || echo "✅ 漏洩したシークレットはありません"

# 1.4 XSS 保護の検証
echo "XSS 保護を検証中..."
# 入力エンコーディングのテスト
echo "<script>alert('xss')</script>" | node -e "
  const { escape } = require('querystring')
  process.stdin.on('data', d => console.log('エンコード:', escape(d.toString())))
"

# フェーズ 2: バージョン移行
echo ""
echo "--- フェーズ 2: バージョン移行 ---"

# 2.1 現在のバージョンを確認
CURRENT_VERSION=$(node -e "console.log(require('./node_modules/next/package.json').version)")
echo "現在の Next.js バージョン: v$CURRENT_VERSION"

# 2.2 バックアップ
echo "現在の node_modules をバックアップ中..."
mv node_modules node_modules_v${CURRENT_VERSION}.bak
mv package-lock.json package-lock_v${CURRENT_VERSION}.json.bak

# 2.3 アップグレード
echo "Next.js 16 にアップグレード中..."
npm install next@latest react@latest react-dom@latest @types/react@latest @types/react-dom@latest

# 2.4 codemod の実行
echo "codemod を実行中..."
npx @next/codemod@latest built-in-next-font .
npx @next/codemod@latest metadata-to-viewport-export .
npx @next/codemod@latest async-request-params .

# 2.5 middleware を proxy.ts に変換
if [ -f src/middleware.ts ]; then
  echo "middleware.ts を proxy.ts に変換中..."
  mv src/middleware.ts src/middleware_v15.bak.ts
fi

# 2.6 ビルドと検証
echo "Next.js 16 でビルド中..."
npm run build

# 2.7 完全テストの実行
echo "テストスイートを実行中..."
npm test
npm run test:e2e

echo ""
echo "=== 移行完了 ==="
echo "Next.js $(node -e "console.log(require('./node_modules/next/package.json').version)")"
echo "React $(node -e "console.log(require('./node_modules/react/package.json').version)")"
echo "ビルド: ✅"
echo "テスト: ✅"
echo "セキュリティ: ✅"
TSX
// src/app/security/report/page.tsx
// ============================================
// セキュリティレポートページ
// ============================================
import { headers } from 'next/headers'
import { execSync } from 'child_process'

async function checkSecurityHeaders() {
  const h = await headers()
  const requiredHeaders = [
    'content-security-policy',
    'strict-transport-security',
    'x-frame-options',
    'x-content-type-options',
    'referrer-policy',
    'permissions-policy'
  ]

  const results = requiredHeaders.map(name => ({
    name,
    present: h.has(name),
    value: h.get(name) || ''
  }))

  const score = results.filter(r => r.present).length / results.length * 100
  return { results, score }
}

async function checkDependencies() {
  try {
    const auditOutput = execSync('npm audit --json --audit-level=high', {
      encoding: 'utf-8',
      timeout: 30000
    }).toString()
    const audit = JSON.parse(auditOutput)
    return {
      vulnerabilities: audit.metadata?.vulnerabilities || { high: 0, critical: 0 },
      total: audit.metadata?.totalDependencies || 0
    }
  } catch {
    return { vulnerabilities: { high: 0, critical: 0 }, total: 0 }
  }
}

const gradeMap: Record<number, string> = {
  100: 'A+', 90: 'A', 80: 'B', 70: 'C', 60: 'D', 0: 'F'
}

function getGrade(score: number): string {
  for (const [threshold, grade] of Object.entries(gradeMap)) {
    if (score >= Number(threshold)) return grade
  }
  return 'F'
}

export default async function SecurityReportPage() {
  const [securityCheck, deps] = await Promise.all([
    checkSecurityHeaders(),
    checkDependencies()
  ])

  return (
    <div className="p-6 space-y-6">
      <h1 className="text-2xl font-bold">セキュリティレポート</h1>

      {/* HTTP セキュリティ評価 */}
      <div className="bg-white rounded-lg shadow p-4">
        <h2 className="text-lg font-semibold mb-4">
          HTTP セキュリティヘッダー
          <span className="ml-2 px-2 py-1 text-sm rounded bg-blue-100">
            グレード: {getGrade(securityCheck.score)}
          </span>
        </h2>
        <table className="w-full text-sm">
          <thead>
            <tr className="text-left text-gray-500">
              <th className="pb-2">ヘッダー</th>
              <th className="pb-2">ステータス</th>
              <th className="pb-2">値</th>
            </tr>
          </thead>
          <tbody>
            {securityCheck.results.map(r => (
              <tr key={r.name} className="border-t">
                <td className="py-2 font-mono">{r.name}</td>
                <td className="py-2">
                  {r.present ? '✅' : '❌'}
                </td>
                <td className="py-2 text-xs text-gray-500 truncate max-w-md">
                  {r.value || '—'}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* 依存関係セキュリティ */}
      <div className="bg-white rounded-lg shadow p-4">
        <h2 className="text-lg font-semibold mb-4">依存関係監査</h2>
        <p>合計パッケージ数: {deps.total}</p>
        <div className="flex gap-4 mt-2">
          <span className="text-red-600">
            重大: {deps.vulnerabilities.critical}
          </span>
          <span className="text-orange-600">
            高: {deps.vulnerabilities.high}
          </span>
        </div>
      </div>

      {/* 環境変数チェック */}
      <div className="bg-white rounded-lg shadow p-4">
        <h2 className="text-lg font-semibold mb-4">環境変数</h2>
        <p className="text-green-600">✅ NEXT_PUBLIC_ シークレットは検出されませんでした</p>
      </div>
    </div>
  )
}

❓ よくある質問

Q Content-Security-Policy(CSP)が厳しすぎてサードパーティスクリプトの読み込みがブロックされる場合はどうすればよいですか?
A 最初に CSP Report-Only モードを使用して違反レポートを収集し、問題がないことを確認してから強制モードに切り替えます。next.config.jsContent-Security-Policy-Report-Only ヘッダーを report-uri または report-to と組み合わせて使用し、Sentry またはカスタムエンドポイントにレポートを送信します。
Q v14→v15 移行時、params の非同期化で最も影響を受けるのはどの部分ですか?
A 最も影響を受けるのは generateMetadatalayout.tsxpage.tsx です。プロジェクトに params.id を使用するページが多数含まれている場合は、移行時に 1 つずつ確認する必要があります。まず npx @next/codemod@latest async-request-params で自動移行を実行し、その後カバーされていないエッジケースを手動でチェックすることをお勧めします。
Q proxy.ts と従来の middleware.ts は機能的に同等ですか?
A proxy.ts は宣言的設定(「どのようなルールか」に焦点)を使用し、middleware.ts は命令的関数(「どのように実行するか」に焦点)を使用します。proxy.ts は middleware のシナリオの 90%(認証、リダイレクト、ヘッダー注入)をカバーしますが、複雑なリクエスト書き換えロジック(i18n ルート書き換えなど)では、引き続き middleware.ts を使用する必要があります(v16 でもサポートされています)。
Q Turbopack のデフォルトに切り替えた後、Webpack の設定はどうすればよいですか?
A Turbopack はほとんどの Webpack ローダーとプラグインをサポートしています。カスタム Webpack 設定を使用している場合は、次の手順をお勧めします:(1) まず Webpack 設定を削除し、Turbopack のデフォルトでビルドします。(2) エラーが発生した場合、Turbopack 互換の代替手段があるか確認します。(3) 互換性のない設定が稀にある場合は、next.config.jsexperimental.turbo.rules を使用してオーバーライドできます。Vercel チームは v16.3 で Webpack 設定との完全互換性を実装する予定です。
Q use cache キャッシュと従来の fetch キャッシュの違いは何ですか?
A fetch キャッシュは自動的で暗黙的(デフォルト force-cache)であり、細かく制御することはできません。use cache は明示的なコンポーネントレベルのキャッシュディレクティブで、cacheTag(ラベルによる無効化)と cacheLife(ライフサイクルポリシー)をサポートします。移行パス:(1) シンプルな fetch キャッシュはそのまま維持できます。(2) オンデマンド無効化が必要なシナリオは use cache + cacheTag に移行します。(3) 新しいコードでは直接 use cache を使用します。
Q 環境変数が漏洩した場合の緊急対応として、何をすべきですか?
A (1) GitHub Secrets またはクラウドプラットフォーム上のすべてのシークレットを直ちにローテーションします。(2) Git 履歴で漏洩をチェックします(git log -p -S 'sk_live_')。(3) シークレットが NEXT_PUBLIC_ として露出していた場合、過去の全訪問者がアクセスした可能性があると想定し、ローテーションします。(4) .gitignoregithub-secret-scanning.env* を追加して、再発を防止します。

📖 まとめ


📝 練習問題

  1. 基本問題 (⭐): next.config.js に 6 つのセキュリティヘッダー(CSP、HSTS、X-Frame-Options、X-Content-Type-Options、Referrer-Policy、Permissions-Policy)を設定し、curl -I または Mozilla Observatory を使用して設定が有効であることを確認します。

  2. 応用問題 (⭐⭐): Next.js 14 を使用しているプロジェクトを v16 に移行します:(1) npx @next/codemod async-request-params を実行して自動移行を行う。(2) カバーされていない cookies()headers() の同期呼び出しを手動で修正する。(3) middleware.tsproxy.ts に変換する(認証ルート保護)。(4) webpack 設定を削除し、Turbopack ビルドが成功することを確認する。

  3. 発展問題 (⭐⭐⭐): 完全なセキュア CI/CD パイプラインを実装します:(1) GitHub Actions に npm audit アクセス制御を追加する(深刻度「高」以上でマージをブロック)。(2) Snyk スキャンを統合し、プルリクエストに脆弱性レポートを自動コメントする。(3) ビルド後にセキュリティヘッダー検証スクリプトを実行し、ビルド成果物から NEXT_PUBLIC_ 漏洩を検索する。(4) 毎週自動的に完全なセキュリティ監査を実行し HTML レポートを生成する。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%