# Next.jsのセキュリティヘッダーとCSP(nonce) — middleware 1枚で多層防御を自動化する

> CSP・HSTS・X-Content-Type-Options・Referrer-Policy・frame-ancestors等のセキュリティヘッダーを、Next.js App Routerのmiddleware 1枚で一括導入する方法。nonce方式CSPがなぜ手書きで壊れ、どう自動化するかを実コードで解説し、CSPがXSS対策の補完であって出力設計の代わりではない限界も正直に示します。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: Next.js, セキュリティ, アーキテクチャ設計, TypeScript
- URL: https://tomodahinata.com/blog/nextjs-security-headers-csp-nonce-middleware-guide
- カテゴリ: アプリ層セキュリティ
- 総合ガイド: https://tomodahinata.com/blog/nextjs-supabase-application-security-guide

## 要点

- セキュリティヘッダーは『水平統制』——アプリ横断で一律に効き、middleware 1枚に集約して自動化できる。各ヘッダーは別々の脅威を塞ぐ（CSP→XSS被害縮小、HSTS→中間者、X-Content-Type-Options→MIMEスニッフィング、frame-ancestors→クリックジャッキング、Referrer-Policy→URL漏洩、Permissions-Policy→APIの濫用）
- nonceなしのCSPは `'unsafe-inline'` を許して実質XSS無防備になりがち。安全な nonce 方式は『リクエストごとに毎回生成』が必須で、これは静的設定では不可能。middlewareでnonceを生成し、ヘッダーとアプリの両方へ受け渡すのが唯一の正解
- 本番投入は Content-Security-Policy-Report-Only で段階導入する。いきなり強制すると正規のスクリプトまで壊れる。違反レポートを観測してから強制へ昇格させる
- 正直な限界：CSP/ヘッダーはリスクを減らし自動化もできるが、XSSの『最後の砦』であって出力サニタイズ＝設計の代わりにはならない。汚染データを安全に出す責任は依然としてコード側にある

---

最初に結論を述べます。**Next.js のセキュリティヘッダーは、`middleware.ts` 1枚に集約して自動化できる「水平統制」です。** ヘッダーはアプリ固有のデータモデルを一切知らなくても、全リクエストに一律で効きます。だからこそ「人間が毎回正しく書く」前提を捨て、設定で一度固めるのが正解です。唯一の例外が **nonce 方式の CSP** で、これだけは「リクエストごとに毎回生成」が必須なため、静的な設定ファイルでは表現できません——middleware で生成し、ヘッダーとアプリの両方へ受け渡す必要があります。

ただし、ここで一つ正直に線を引いておきます。**CSP は XSS の「最後の砦」であって、出力の安全設計の代わりにはなりません。** 汚染データを `dangerouslySetInnerHTML` に素通しすれば、CSP があっても被害は出ます。本記事は「ヘッダーで何を機械的に塞げるか」と「ヘッダーでは塞げない、設計が要る領域はどこか」をセットで、Next.js App Router の実コードと一次情報に基づいて整理します。これは [Next.js × Supabase アプリケーションセキュリティ](/blog/nextjs-supabase-application-security-guide)で描いた「水平統制 vs 垂直リスク」の地図の、ヘッダー領域を深掘りする記事です。

---

## 1. なぜヘッダーは「自動化できる水平統制」なのか

セキュリティ対策には、性質の異なる2種類があります。

| 種別 | 例 | 何に依存するか | 自動化 |
|---|---|---|---|
| **水平統制** | セキュリティヘッダー、CSP、レート制限、CSRF、入力検証 | アプリ横断で一律 | **設定で一律に肩代わりできる** |
| **垂直リスク** | 認可/IDOR、テナント分離、業務ロジック | 「誰が何を所有するか」 | 検出・警告まで。修正は人間の設計 |

セキュリティヘッダーは完全に前者です。`Strict-Transport-Security` や `X-Content-Type-Options: nosniff` は、あなたのテーブル構造もユーザーの所有関係も知りません。ただ「全レスポンスに同じ1行を足す」だけで効きます。**人間が画面ごとに考えるべきものではなく、1か所に集約して機械に番をさせるべき領域**です。

Next.js でこの「1か所」になるのが middleware です。`middleware.ts` はすべてのリクエストの前段で走るため、ヘッダー付与の単一の関所として理想的です（[Next.js docs](https://nextjs.org/docs)）。`next.config.js` の `headers()` でも静的ヘッダーは付けられますが、後述する nonce はリクエストごとに値が変わるため、`next.config.js` では表現できません。だからヘッダー戦略は middleware に寄せるのが筋が通ります。

OWASP の [Application Security Verification Standard（ASVS）](https://owasp.org/www-project-application-security-verification-standard/)も、セキュリティを「入れたか」ではなく「検証できるか」で測ります。本記事も各ヘッダーについて「何を塞ぐか」と「どう確かめるか」をセットで述べます。

---

## 2. 主要セキュリティヘッダーと、各々が塞ぐ脅威

まず地図を渡します。ヘッダーは「1個で全部を守る魔法」ではなく、**1個が1つの脅威を塞ぐ部品の集合**です。混同すると「CSPを入れたのにクリックジャッキングされた」のような事故になります。

| ヘッダー | 塞ぐ脅威 | 典型値 |
|---|---|---|
| `Content-Security-Policy` | XSS（被害の縮小）、データ持ち出し | `default-src 'self'; script-src 'self' 'nonce-…'` |
| `Strict-Transport-Security` | HTTPSダウングレード、中間者(MITM) | `max-age=63072000; includeSubDomains; preload` |
| `X-Content-Type-Options` | MIMEスニッフィングによる誤実行 | `nosniff` |
| `Referrer-Policy` | URL（トークン等）の外部漏洩 | `strict-origin-when-cross-origin` |
| `frame-ancestors`（CSP）/ `X-Frame-Options` | クリックジャッキング | `frame-ancestors 'none'` / `DENY` |
| `Permissions-Policy` | カメラ/マイク/位置情報等の濫用 | `camera=(), microphone=(), geolocation=()` |

順に、要点だけ正確に押さえます。

**`Strict-Transport-Security`（HSTS）** は「このサイトは今後 `max-age` 秒間、必ずHTTPSで来い」とブラウザに強制します。初回アクセスの平文リクエストを後続で消すのが狙いで、`preload` を付けてブラウザの先読みリストに載せると初回も保護されます。ただし `preload` は**一度載せると取り消しが難しい**ため、サブドメイン含め恒久的にHTTPS化できる確証がある場合だけにします（[MDN Strict-Transport-Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security)）。

**`X-Content-Type-Options: nosniff`** は、ブラウザがレスポンスのMIMEタイプを「推測」して実行する挙動を止めます。例えばユーザーがアップロードしたファイルを、ブラウザが勝手に `text/html` と解釈してスクリプト実行する、という事故を防ぎます。値は `nosniff` 一択です。

**`Referrer-Policy`** は、別サイトへ遷移する際に送る `Referer` ヘッダーの粒度を制御します。クエリ文字列にトークンやセッションIDを載せている場合、これが緩いと外部サイトのアクセスログに秘密が漏れます。`strict-origin-when-cross-origin`（同一オリジンはフルパス、クロスオリジンはオリジンのみ）が実用的な既定です。

**クリックジャッキング**対策は、CSP の `frame-ancestors` が現代の正解です。古い `X-Frame-Options` は後方互換のために併記する程度で、`frame-ancestors 'none'`（または許可するオリジンを列挙）があれば本質的に十分です。両者が矛盾した場合は CSP が優先されます。

**`Permissions-Policy`** は、カメラ・マイク・位置情報・全画面などのブラウザ機能を、自サイトと埋め込みiframeのどちらに許すかを宣言します。使わない機能は `camera=()` のように空で明示的に閉じておくと、万一のXSSやサードパーティスクリプトによる濫用面が減ります。

ここまでの5つ（HSTS / nosniff / Referrer-Policy / frame-ancestors / Permissions-Policy）は**値が固定**です。リクエストによって変わりません。問題は6つ目、CSP の `script-src` だけが「リクエストごとに変わる値（nonce）」を必要とする点です。次節で深掘りします。

---

## 3. CSPの基礎 — default-src / script-src と、なぜ `'unsafe-inline'` が危険か

CSP（Content Security Policy）は「このページが**どこからリソースを読み込み、何を実行してよいか**」をブラウザに宣言するヘッダーです。XSS が混入しても、攻撃者のスクリプトを「許可されていない」として実行させないのが狙いです（[MDN Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy)）。

ディレクティブは「リソース種別ごとの許可リスト」です。最重要な数個だけ押さえます。

```http
Content-Security-Policy:
  default-src 'self';                          # 既定: 同一オリジンのみ許可
  script-src 'self' 'nonce-rAndOm==';          # JS: 自オリジン + このnonce付きのみ
  style-src 'self';                            # CSS: 自オリジンのみ
  img-src 'self' data:;                        # 画像: 自オリジン + data URI
  object-src 'none';                           # <object>/<embed> 禁止（古い攻撃面を閉じる）
  base-uri 'none';                             # <base> 改ざんによる相対パス乗っ取りを防ぐ
  frame-ancestors 'none';                      # クリックジャッキング対策
```

`default-src` はフォールバックで、個別指定のないリソース種別すべてに適用されます。`script-src` がスクリプトの実行可否を握る最重要ディレクティブです。

### なぜ `'unsafe-inline'` が危険か

ここが CSP の核心です。多くの「とりあえずCSPを入れた」設定は、こう書かれています。

```http
# アンチパターン: これは「CSPを入れた」が「XSSをほぼ防げない」設定
script-src 'self' 'unsafe-inline';
```

`'unsafe-inline'` は「ページ内に直接書かれた `<script>…</script>` やインラインの `onclick=` を全部実行してよい」という許可です。しかし**XSS の本質は、攻撃者がページにインラインスクリプトを注入すること**です。つまり `'unsafe-inline'` を許した瞬間、CSP は XSS に対してほぼ無力になります。「鍵をかけたが、合鍵を玄関マットの下に置いた」状態です。

同様に `'unsafe-eval'`（`eval()` や文字列からのコード生成を許可）も避けます。

### 正解: nonce + strict-dynamic

ではどうやって「自分の正規のインラインスクリプトだけ」を許すか。答えが **nonce（number used once）** です。リクエストごとにランダムな使い捨ての値を生成し、(1) CSP ヘッダーに `'nonce-<値>'` として宣言し、(2) 自分が出力する `<script>` タグに `nonce="<値>"` 属性を付けます。**両者が一致したスクリプトだけ**が実行されます。攻撃者は、サーバーがそのリクエストで生成したランダム値を事前に知り得ないため、注入したスクリプトに正しい nonce を付けられません。

```http
# 厳格版: nonce で「正規のインラインだけ」を許可
script-src 'self' 'nonce-rAndOm==' 'strict-dynamic';
```

`'strict-dynamic'` は「nonce で信頼したスクリプトが、さらに動的に読み込むスクリプトも連鎖的に信頼する」指示です。これにより、ホスト名のallowlist（`https://cdn.example.com` のような列挙）に頼らずに済みます。allowlist 方式は、信頼したCDN上に1つでも悪用可能なスクリプトがあると破られるため、nonce + strict-dynamic の方が堅牢です。

**決定的な制約がここにあります。nonce は「リクエストごとに必ず新しく生成」しなければ意味がありません。** 全ユーザー・全リクエストで同じ固定値を使えば、攻撃者がその値を1度読み取るだけで自由にスクリプトを注入できてしまい、nonce を使う意味が消えます。だから nonce 付き CSP は、静的な `next.config.js` の `headers()` や、ビルド時に固定される設定では**原理的に実現できません**。リクエストのたびにコードが動いて値を作る場所——すなわち **middleware** が必要になります。

---

## 4. 実装 — middleware 1枚でnonceを生成し、ヘッダーとアプリへ受け渡す

ここが本記事の実装の核です。難しさは「生成した同じ nonce を、レスポンスヘッダー**と**レンダリングされる `<script>` の両方に行き渡らせる」という受け渡しにあります。Next.js App Router では、middleware からリクエストヘッダーに nonce を載せ、Server Component 側で `headers()` から読み取る、という経路が定石です。

### 4-1. middleware: nonce生成とヘッダー付与

```ts
// middleware.ts — リクエストごとに nonce を発行し、全ヘッダーを1か所で付与する
import { NextResponse, type NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // リクエストごとに必ず新規生成。crypto は Edge ランタイムでも利用可
  const nonce = crypto.randomUUID().replace(/-/g, "");
  const isDev = process.env.NODE_ENV !== "production";

  const csp = [
    `default-src 'self'`,
    // 開発時は HMR のため eval を許す。本番ビルドでは付けない（下の分岐参照）
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ""}`,
    // Next.js は styled-jsx 等でインラインstyleを使う。nonce が効かない環境向けの現実解は §6 で後述
    `style-src 'self' 'unsafe-inline'`,
    `img-src 'self' blob: data:`,
    `font-src 'self'`,
    `object-src 'none'`,
    `base-uri 'none'`,
    `form-action 'self'`,
    `frame-ancestors 'none'`,
    `upgrade-insecure-requests`,
  ].join("; ");

  // 1) 後段(Server Component)が同じ nonce を読めるよう、リクエストヘッダーに載せる
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-nonce", nonce);
  requestHeaders.set("Content-Security-Policy", csp);

  const response = NextResponse.next({ request: { headers: requestHeaders } });

  // 2) ブラウザに効かせる本番のヘッダー群（値が固定のものはここで一律付与）
  response.headers.set("Content-Security-Policy", csp);
  response.headers.set("X-Content-Type-Options", "nosniff");
  response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
  response.headers.set("X-Frame-Options", "DENY"); // frame-ancestors の後方互換
  response.headers.set(
    "Permissions-Policy",
    "camera=(), microphone=(), geolocation=(), interest-cohort=()",
  );
  // HSTS は HTTPS 環境でのみ意味を持つ。ローカルHTTPで付けると後で苦労するので本番だけ
  if (!isDev) {
    response.headers.set(
      "Strict-Transport-Security",
      "max-age=63072000; includeSubDomains; preload",
    );
  }

  return response;
}

// 静的アセットやプリフェッチには CSP を流さない（不要なオーバーヘッドと誤検知を避ける）
export const config = {
  matcher: [
    {
      source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
      missing: [
        { type: "header", key: "next-router-prefetch" },
        { type: "header", key: "purpose", value: "prefetch" },
      ],
    },
  ],
};
```

ポイントを3つ。**(1) `crypto.randomUUID()`** は Edge ランタイムでも使える標準APIで、外部依存なしに十分なランダム性が得られます（base64化したい場合は `btoa` だが、UUIDのハイフン除去でも実用上問題ありません）。**(2) リクエストヘッダーとレスポンスヘッダーの両方**に CSP を載せているのは、前者が「アプリへの受け渡し」、後者が「ブラウザへの適用」という別の役割だからです。**(3) `matcher`** で静的アセットを除外し、ヘッダー付与のコストとノイズを抑えます。

### 4-2. アプリ側: nonceを読んで `<script>` に付ける

middleware が `x-nonce` に載せた値を、Server Component で読み出します。

```tsx
// app/layout.tsx — middleware が生成した nonce を読み、自前のインラインスクリプトに付与
import { headers } from "next/headers";
import Script from "next/script";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const nonce = (await headers()).get("x-nonce") ?? "";

  return (
    <html lang="ja">
      <body>
        {children}
        {/* 例: アナリティクス等の自前インラインスクリプトには必ず nonce を付ける */}
        <Script
          id="analytics-init"
          nonce={nonce}
          strategy="afterInteractive"
          dangerouslySetInnerHTML={{
            __html: `window.dataLayer = window.dataLayer || [];`,
          }}
        />
      </body>
    </html>
  );
}
```

`headers()` を呼ぶとそのルートは動的レンダリングになります。これは nonce 方式の本質的なトレードオフです——**リクエストごとに値が変わる以上、そのページを静的にキャッシュすることはできません**。完全静的配信が最優先で、かつ自前のインラインスクリプトが皆無なページでは、nonce を諦めてハッシュ方式（`'sha256-…'`）を選ぶ判断もあります。設計上の天秤として認識しておくべき点です。

### 4-3. 動作確認

実装したら、必ず「効いているか」を確かめます。型チェックや見た目では分かりません。

```bash
# レスポンスヘッダーを直接観測する。nonce がリクエストごとに変わることも確認
curl -sI https://localhost:3000/ | grep -iE "content-security-policy|strict-transport|x-content-type|referrer-policy|permissions-policy"
```

```http
content-security-policy: default-src 'self'; script-src 'self' 'nonce-a1b2c3...' 'strict-dynamic'; ...
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=(), interest-cohort=()
```

ブラウザのDevTools Consoleに `Refused to execute inline script because it violates the following Content Security Policy directive` という違反が出れば、それは「nonce の付け忘れたスクリプトが弾かれている」証拠です。次節の Report-Only を使えば、本番を壊さずにこの違反を観測できます。

---

## 5. Report-Only で段階導入する — いきなり強制しない

CSP を最初から強制（enforce）モードで本番投入するのは危険です。サードパーティのウィジェット、計測タグ、`next dev` の挙動、見落としていた自前インラインスクリプト——これらが一斉に弾かれ、画面が壊れます。**正しい順序は「観測 → 修正 → 強制」です。**

ブラウザはこのために `Content-Security-Policy-Report-Only` ヘッダーを用意しています。これは「ポリシーに違反するが、**ブロックはせず違反だけを報告する**」モードです。

```ts
// 段階導入: まず Report-Only で「何が弾かれるはずか」を観測する
const cspHeaderName = ENFORCE_CSP
  ? "Content-Security-Policy"
  : "Content-Security-Policy-Report-Only";

response.headers.set(cspHeaderName, csp);
// 違反レポートの送信先（report-to/report-uri）。収集は Sentry 等のエンドポイントでも可
response.headers.set(
  "Reporting-Endpoints",
  `csp-endpoint="https://example.com/api/csp-report"`,
);
```

導入手順は以下です。

1. **Report-Only で本番にデプロイする。** 画面は一切壊れません。
2. **数日〜1〜2週間、違反レポートを集める。** 実トラフィックでどのスクリプト/スタイル/接続先が弾かれるかが分かります。
3. **正規のものをポリシーに足す。** 自前スクリプトには nonce を付け、必要なサードパーティだけを許可に加えます。違反が枯れるまで繰り返します。
4. **`Content-Security-Policy`（強制）へ昇格させる。** ここで初めてブロックが有効になります。

この「Report-Only で実トラフィックの違反を観測してから強制へ昇格」というプロセスを踏むかどうかが、CSP導入の成否を分けます。いきなり強制して障害を出し、CSP ごと外してしまう——これが最もよくある失敗です。

---

## 6. 正直な落とし穴 — Next.js特有の現実

実運用で必ずぶつかる点を、誇張せず挙げます。

**インラインstyleの扱い。** Next.js は `styled-jsx` やフレームワーク内部のインラインスタイルを使う場面があり、`style-src` に nonce を効かせきれないことがあります。現実解は2つ——`style-src 'self' 'unsafe-inline'` を許容する（styleのインライン許可は、scriptのそれと比べてリスクは限定的だが**ゼロではない**ため、データ持ち出し系の攻撃面が残る点は認識する）か、ハッシュで個別許可する。完璧を狙って画面を壊すより、`script-src` を厳格に保ちつつ `style-src` は現実的に運用する方が、トータルのリスクは下がります。

**サードパーティスクリプト。** 計測・チャット・広告タグは、それぞれが要求するオリジンや `'unsafe-inline'` 相当を持ち込みます。`'strict-dynamic'` を使えば多くは nonce 連鎖で動きますが、対応していないタグもあります。「CSPに合わないタグは入れない」という逆方向の意思決定も、セキュリティ上はしばしば正解です。

**`headers()` による動的化。** §4-2 で触れたとおり、nonce を読むページはISR/静的キャッシュの対象外になります。CDNフルキャッシュが事業上クリティカルなページでは、ハッシュ方式や、そのルートだけCSPを緩める判断を要します。

**middleware のランタイム。** middleware は Edge ランタイムで動くため、`crypto.randomUUID()` のような標準APIは使えても、Node.js固有のモジュールは使えません。nonce 生成にNode専用ライブラリを持ち込まないよう注意します。

これらはどれも「ヘッダーを入れれば終わり」ではなく、**アプリの構成に合わせた調整が要る**ことを示しています。とはいえ調整の範囲はアプリ横断の設定の中に閉じており、データモデルの設計判断は不要です。だから依然として「水平統制＝自動化できる層」です。

---

## 7. これは「水平統制」— Aegisのinitで自動化できる

ここまでの実装は、突き詰めれば**定型作業**です。nonce 生成、ヘッダー6種の付与、Report-Only からの段階導入、matcher の設定——どれもアプリ固有の知識を要さず、正しく一度書けば全プロジェクトで使い回せます。「人間が毎回 middleware を手書きする」より、**雛形を自動生成して必要な調整だけ加える**方が、ヒューマンエラー（`'unsafe-inline'` の付け忘れ消し忘れ、nonceの固定化、HSTSの取り違え）を構造的に減らせます。

私が公開している OSS のセキュリティツールキット Aegis は、この水平統制のランタイム強化——**ヘッダー/CSP・レート制限・CSRF・型付きenv境界**の導入——を1コマンドで雛形化します。

```bash
# ランタイム強化の雛形を導入（ヘッダー/CSP・レート制限・CSRF・型付きenv境界）
npx @aegiskit/cli init
```

過大に主張するつもりはありません。`init` が生成するのは**出発点となる設定**であって、§6 で見たサードパーティ調整や、ページごとの動的化トレードオフは、結局あなたのアプリの構成に合わせて詰める必要があります。それでも、白紙から書いて落とし穴を踏むより、検証済みの雛形から始める方が安全で速い。これは Aegis の `scan`（SASTで現状の穴を可視化）と組み合わせると、「今ある穴を見える化 → ランタイム強化を導入」という流れになります。

---

## 8. 最大の注意 — CSPはXSS対策の補完であって、出力設計の代わりではない

ここが、本記事で最も強調したい点です。**CSP を入れても、XSS が「なくなる」わけではありません。**

CSP は多層防御の一層、いわば「XSS が混入してしまった場合に、被害を縮小する最後の砦」です。攻撃者のスクリプトの実行を妨げる確率は上げますが、**そもそも汚染データを安全に出力する責任**は、依然としてアプリのコード側にあります。例えば次のコードは、CSP が厳格でも問題が残ります。

```tsx
// CSP があっても安全設計の代わりにはならない例
// 汚染データをそのまま HTML として注入している
<div dangerouslySetInnerHTML={{ __html: userSuppliedHtml }} />
```

`'unsafe-inline'` を許していない nonce 方式 CSP は、ここに注入された `<script>` の実行を**多くの場合は**妨げます。しかし、CSP のバイパス手法は研究され続けており、`base-uri` の不備、許可した古いライブラリ経由の実行、`'strict-dynamic'` の連鎖の悪用など、設定の穴次第で抜けられます。さらに、スクリプト実行を伴わない攻撃（リンクの差し替え、フォームの乗っ取り、コンテンツ改ざんによるフィッシング）は CSP の `script-src` では止まりません。

**正しい順序はこうです。まず出力を設計で安全にする（汚染データをサニタイズ/エスケープし、危険なシンクに素通ししない）。その上で、CSP を「それでも何か漏れたとき」の保険として重ねる。** 順序を逆にして「CSP があるからサニタイズは適当でいい」と考えるのが、最も危険な誤解です。

XSS そのものの防ぎ方——`dangerouslySetInnerHTML` の安全な扱い、DOM XSS、汚染データのサニタイズ——は[Next.jsのXSS/DOM XSS対策](/blog/nextjs-xss-dom-xss-dangerouslysetinnerhtml-prevention-guide)で別途詳説しています。CSP はその記事の対策を**置き換えるものではなく、補完するもの**です。これは OWASP の[OWASP Top 10](https://owasp.org/www-project-top-ten/)が一貫して示す多層防御の思想そのものです。

---

## 9. ヘッダーと並ぶ、他の水平統制

ヘッダー/CSP は水平統制の一部にすぎません。同じ「設定で一律に固める」発想で、middleware や境界に寄せるべき仲間がいます。本記事の範囲外なので、それぞれ専門記事に委ねます。

- **CSRF / Origin 検証** — Server Actions や `POST` ルートのような状態変更経路は、`SameSite` Cookie に加えて Origin を検証して二段で守ります。詳細は[Next.jsのCSRF/Origin保護](/blog/nextjs-csrf-origin-protection-server-actions-guide)へ。
- **秘密情報の衛生** — `NEXT_PUBLIC_` 接頭辞の誤用で秘密がクライアントバンドルに混入する事故は、ヘッダーでは防げません。env境界の設計は[Next.jsの環境変数・秘密情報の漏洩防止](/blog/nextjs-env-secret-leak-prevention-public-vars-guide)で扱います。
- **レート制限・入力検証** — これらも水平統制で、[アプリケーションセキュリティ全体像](/blog/nextjs-supabase-application-security-guide)に地図としてまとめてあります。

重要なのは、**これらはすべて「自動化できる層」**だということです。設定とライブラリで肩代わりでき、CIに番をさせられます。問題は、その先にある認可やテナント分離といった「設計でしか守れない垂直リスク」で、そこは次節の監査領域に移ります。

---

## 10. 本番前チェックリスト

外注でもAI製でも、本番投入の前にこれだけは確認してください。

- [ ] **CSP に `'unsafe-inline'`（script）を入れていない**。入れるなら nonce/hash を使う
- [ ] **nonce がリクエストごとに変わる**（`curl -I` を2回叩いて値が違うこと）
- [ ] 自前のインラインスクリプト/`<Script>` すべてに **nonce を付与**している
- [ ] **Report-Only で実トラフィックの違反を観測**してから強制へ昇格した
- [ ] `Strict-Transport-Security` を**本番(HTTPS)でのみ**付与している（`preload` の不可逆性も理解の上で）
- [ ] `X-Content-Type-Options: nosniff` / `Referrer-Policy` / `frame-ancestors`（+ `X-Frame-Options`）/ `Permissions-Policy` を付与している
- [ ] `matcher` で静的アセットを除外し、不要なヘッダー付与を避けている
- [ ] **CSP を「サニタイズの代わり」にしていない**。汚染データの出力は設計側で安全にしている
- [ ] middleware が **Edge ランタイム互換**（Node専用APIに依存していない）

発注者の視点で効くのは、**「CSPに `'unsafe-inline'` は入っていますか？」「nonceはリクエストごとに変わりますか？」「Report-Onlyで段階導入しましたか？」**の3問です。良い開発者は即答できます。

---

## 11. どこまで自動化、どこから監査か

最後に正直に線を引きます。

**セキュリティヘッダーと CSP は、水平統制として大半を機械的に固められます。** middleware 1枚に集約し、雛形から始め、Report-Only で段階導入する——この型さえ守れば、人間が毎回ゼロから考える必要はありません。まずは [Aegis](/aegis)（無料OSS、`npx @aegiskit/cli init` でランタイム強化、`scan` で現状把握）で雛形を入れるのが、最もコスパの良い第一歩です。

一方、**CSP は XSS の被害を縮小しても、認可・テナント分離・業務ロジックといった「正規リクエストとして現れる垂直リスク」には一切効きません。** これらはヘッダーでもWAFでも構造的に防げず、あなたのデータモデルを理解した人間にしか設計・検証できません。ここで「ヘッダーを入れたから安全」と考えるのが、最も危険な油断です。ヘッダーは入口の保険であって、認可の正しさを証明するものではありません。

だからこそ線引きが要ります。どこまで自分で固め、どこから専門家のレビューが要るか——その判断や、既存アプリの認可・RLS・出力設計を含むレビューが必要なら、[セキュリティ監査](/aegis/audit)で承ります。私自身、[サーバーレス決済プラットフォームの案件](/case-studies/payment-platform-reliability)で、決済信頼性レイヤーを含むアプリ層の堅牢化を実運用で設計・検証してきました。

---

## よくある質問（FAQ）

**Q. `next.config.js` の `headers()` ではダメですか？**
A. 値が固定のヘッダー（HSTS、nosniff、Referrer-Policy 等）なら `headers()` でも付けられます。ただし **nonce はリクエストごとに変わるため `next.config.js` では表現できません**。CSPを nonce 方式で運用するなら middleware が必須です。固定ヘッダーも含めて middleware に集約した方が、関所が1つになり管理しやすくなります。

**Q. nonce と hash、どちらを使うべきですか？**
A. 自前のインラインスクリプトが動的に変わる、または複数ある場合は **nonce** が扱いやすい。インラインスクリプトが皆無、または固定で、ページを完全静的にキャッシュしたい場合は **hash（`'sha256-…'`）** が向きます。hash なら `headers()` を呼ばずに済み、静的配信を維持できます。アプリの大半は nonce で問題ありませんが、CDNフルキャッシュ最優先のページだけ hash、という使い分けも有効です。

**Q. CSPを入れれば XSS は防げますか？**
A. 防げません。CSP は XSS が混入した場合の**被害を縮小する最後の砦**であって、出力の安全設計（サニタイズ/エスケープ）の代わりにはなりません。CSP のバイパス手法も存在します。まず出力を設計で安全にし、その上で CSP を保険として重ねる——この順序が必須です。詳細は[XSS対策の記事](/blog/nextjs-xss-dom-xss-dangerouslysetinnerhtml-prevention-guide)を参照してください。

**Q. Report-Only のまま運用し続けてもいいですか？**
A. よくありません。Report-Only は**何もブロックしません**。違反を観測する移行期間のためのモードであり、ここで止まると「CSPがあるように見えて、実際には何も守っていない」状態になります。違反が枯れたら必ず `Content-Security-Policy`（強制）へ昇格させてください。

**Q. 個人開発や小規模でもここまでやるべきですか？**
A. ヘッダーは水平統制で、導入コストが極めて低い（middleware 1枚）のに、塞げる攻撃面は広い。費用対効果が最も高い対策の一つです。最低でも nosniff・Referrer-Policy・frame-ancestors・HSTS の4つと、`'unsafe-inline'` を使わない CSP は、規模に関わらず入れる価値があります。

---

## まとめ — 1枚に集約し、保険として重ねる

要点を整理します。

- セキュリティヘッダーは **アプリ横断で一律に効く「水平統制」**。`middleware.ts` 1枚に集約して自動化するのが正解。
- 各ヘッダーは**別々の脅威を1つずつ**塞ぐ（CSP→XSS被害縮小、HSTS→MITM、nosniff→MIMEスニッフィング、frame-ancestors→クリックジャッキング、Referrer-Policy→URL漏洩、Permissions-Policy→機能濫用）。1個で全部は守れない。
- nonce 方式 CSP だけは**リクエストごとの生成が必須**で、静的設定では実現できない。middleware で生成し、ヘッダーとアプリの両方へ受け渡す。
- 本番投入は **Report-Only で観測 → 修正 → 強制へ昇格**。いきなり強制すると壊れる。
- これらは雛形化でき（`npx @aegiskit/cli init`）、自動化できる層。**ただし CSP は XSS の最後の砦であって、出力サニタイズ＝設計の代わりにはならない。**
- ヘッダーで塞げない認可・テナント分離は垂直リスクで、設計と監査の領域。

AIで速く作ること自体は正しい。**速く作ったものを、漏らさず安全に固める**——その水平統制の自動化や、既存アプリのヘッダー/CSP・出力設計のレビューが必要であれば、お気軽にご相談ください。

---

## 参考資料

- [MDN — Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy)
- [MDN — Strict-Transport-Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security)
- [Next.js Docs](https://nextjs.org/docs)
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [OWASP Application Security Verification Standard（ASVS）](https://owasp.org/www-project-application-security-verification-standard/)
