Next.js: セキュリティ強化とバージョン移行
最終更新:2026-08-26
セキュリティとバージョン移行は本番レベルのアプリケーションにおける 2 つの防御線です — 前者は外部攻撃を防ぎ、後者は技術スタックの負債をなくします。
1. 学習内容
- XSS 保護、CSRF トークン、HTTP セキュリティヘッダー(next.config.js headers)の実装
- 環境変数の漏洩防止(NEXT_PUBLIC_* 規約 / .gitignore での .env*.local)
- npm audit と Snyk を使用した依存関係のセキュリティ監査
- v14→v15 移行の習得:params、cookies、headers の非同期処理
- v15→v16 移行の理解:proxy.ts による Middleware の置き換え、Turbopack デフォルト、"use cache" 移行
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 段階でアプローチします:
# ステップ 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.js の headers 関数は HTTP レスポンスヘッダーを注入し、CSP、HSTS、X-Frame-Options などのセキュリティポリシーを上書きします。
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 セキュリティヘッダー設定
// 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 トークン保護
// 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>
)
}
▶ サンプル: ヘッダーの検証
# セキュリティヘッダーの確認
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=()
{"status":"ok","data":{}}
4. 環境変数の漏洩防止
NEXT_PUBLIC_ プレフィックスを持つ変数はクライアントサイドの JS バンドルにコンパイルされます — このプレフィックスでシークレットを保存するとブラウザに露出します。
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) 環境変数セキュリティチェックリスト
# ✅ 安全: プレフィックスなしの変数を使用
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 — 環境変数セキュリティ
.env
.env*.local
.env.development.local
.env.test.local
.env.production.local
# .env.example は見落とさないでください(テンプレートファイルです)
!.env.example
(3) 環境変数検証スクリプト
// 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()
▶ サンプル: バンドル内の露出変数のチェック
# ビルド後のクライアント 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 はありません"
▲ 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
# 基本監査
npm audit
# 深刻度レベルでフィルタリング
npm audit --audit-level=high
# 自動修復(互換性を壊す可能性あり)
npm audit fix
# パッチバージョンのみ修復
npm audit fix --target=patch
# JSON レポートの生成
npm audit --json > security-report.json
(2) Snyk 統合
# Snyk CLI のインストール
npm install -g snyk
# 認証
snyk auth
# プロジェクトのテスト
snyk test
# 継続的監視
snyk monitor
# Docker イメージスキャン
snyk container test taskflow:latest --file=Dockerfile
# .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 の非同期処理です — これは最も破壊的な変更です。
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.tsx、layout.tsx |
| searchParams | searchParams.page(同期) |
await searchParams(非同期) |
page.tsx |
| cookies | cookies().get()(同期) |
await cookies()(非同期) |
layout.tsx、middleware.ts |
| headers | headers().get()(同期) |
await headers()(非同期) |
layout.tsx、page.tsx |
| generateMetadata | params 同期 |
params 非同期 |
page.tsx |
| React | React 18 | React 19 | グローバル |
| useFormState | react-dom |
useActionState に名称変更 |
フォームコンポーネント |
(2) 同期から非同期への移行例
// ❌ 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>
}
// ❌ 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 の非同期処理
generateMetadata コンポーネントの UI をレンダリングします。
// ❌ 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>
}
Layout コンポーネントの UI をレンダリングします。
7. v15 から v16 への移行ガイド
Next.js 16 は 3 つの主要な変更を導入します:proxy.ts による middleware の置き換え、Turbopack のデフォルト化、use cache の移行です。
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)
// 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 移行
// ❌ 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 デフォルト設定
// 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
▶ サンプル: 完全移行前後のシステム比較
Next.js の設定が適用されました。変更はサーバー再起動時に有効になります。
// === 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
}
})
セクションに記載されている ▶ サンプル: 完全移行前後のシステム比較 コンポーネントの UI をレンダリングします。
8. 完全な例: セキュリティ強化 + バージョン移行ワークフロー
# ============================================
# 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 "セキュリティ: ✅"
// 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>
)
}
❓ よくある質問
next.config.js で Content-Security-Policy-Report-Only ヘッダーを report-uri または report-to と組み合わせて使用し、Sentry またはカスタムエンドポイントにレポートを送信します。params の非同期化で最も影響を受けるのはどの部分ですか?generateMetadata、layout.tsx、page.tsx です。プロジェクトに params.id を使用するページが多数含まれている場合は、移行時に 1 つずつ確認する必要があります。まず npx @next/codemod@latest async-request-params で自動移行を実行し、その後カバーされていないエッジケースを手動でチェックすることをお勧めします。proxy.ts と従来の middleware.ts は機能的に同等ですか?proxy.ts は宣言的設定(「どのようなルールか」に焦点)を使用し、middleware.ts は命令的関数(「どのように実行するか」に焦点)を使用します。proxy.ts は middleware のシナリオの 90%(認証、リダイレクト、ヘッダー注入)をカバーしますが、複雑なリクエスト書き換えロジック(i18n ルート書き換えなど)では、引き続き middleware.ts を使用する必要があります(v16 でもサポートされています)。next.config.js で experimental.turbo.rules を使用してオーバーライドできます。Vercel チームは v16.3 で Webpack 設定との完全互換性を実装する予定です。use cache キャッシュと従来の fetch キャッシュの違いは何ですか?fetch キャッシュは自動的で暗黙的(デフォルト force-cache)であり、細かく制御することはできません。use cache は明示的なコンポーネントレベルのキャッシュディレクティブで、cacheTag(ラベルによる無効化)と cacheLife(ライフサイクルポリシー)をサポートします。移行パス:(1) シンプルな fetch キャッシュはそのまま維持できます。(2) オンデマンド無効化が必要なシナリオは use cache + cacheTag に移行します。(3) 新しいコードでは直接 use cache を使用します。git log -p -S 'sk_live_')。(3) シークレットが NEXT_PUBLIC_ として露出していた場合、過去の全訪問者がアクセスした可能性があると想定し、ローテーションします。(4) .gitignore に github-secret-scanning と .env* を追加して、再発を防止します。📖 まとめ
- HTTP セキュリティヘッダー(CSP、HSTS、X-Frame-Options など)は
next.config.jsのheaders関数で設定し、XSS とクリックジャッキングに対する第一防衛線となります。 NEXT_PUBLIC_プレフィックスを持つ変数はクライアントサイド JavaScript に露出します。キーの保存には絶対に使用しないでください。.env*.localは必ず.gitignoreに含める必要があります。npm auditと Snyk は依存関係の脆弱性を定期的にスキャンし、セキュリティ監査チェックを CI に統合します。- v14→v15 移行のハイライト:
params/cookies/headersがすべて非同期に変換されました。useFormStateはuseActionStateに名称変更されました。 - v15→v16 移行の主要変更点:
proxy.ts宣言的設定がmiddleware.ts関数的ミドルウェアを置き換え、Turbopack がデフォルトビルドツールになり、use cacheディレクティブが暗黙的なfetchキャッシュを置き換えます。 - セキュリティ強化とバージョン移行は段階的に実施すべきです:まず高リスクの脆弱性を修正し、次にバージョンをアップグレードし、各ステップで徹底的なテストと検証を行う必要があります。
📝 練習問題
-
基本問題 (⭐):
next.config.jsに 6 つのセキュリティヘッダー(CSP、HSTS、X-Frame-Options、X-Content-Type-Options、Referrer-Policy、Permissions-Policy)を設定し、curl -Iまたは Mozilla Observatory を使用して設定が有効であることを確認します。 -
応用問題 (⭐⭐): Next.js 14 を使用しているプロジェクトを v16 に移行します:(1)
npx @next/codemod async-request-paramsを実行して自動移行を行う。(2) カバーされていないcookies()とheaders()の同期呼び出しを手動で修正する。(3)middleware.tsをproxy.tsに変換する(認証ルート保護)。(4)webpack設定を削除し、Turbopack ビルドが成功することを確認する。 -
発展問題 (⭐⭐⭐): 完全なセキュア CI/CD パイプラインを実装します:(1) GitHub Actions に
npm auditアクセス制御を追加する(深刻度「高」以上でマージをブロック)。(2) Snyk スキャンを統合し、プルリクエストに脆弱性レポートを自動コメントする。(3) ビルド後にセキュリティヘッダー検証スクリプトを実行し、ビルド成果物からNEXT_PUBLIC_漏洩を検索する。(4) 毎週自動的に完全なセキュリティ監査を実行し HTML レポートを生成する。