🚫

Next.js で Content-Security-Policy ヘッダをいい感じに設定する

2023/06/03に公開

この記事に記載している内容は以下のブログを参考に2023年6月時点で自ら調査した内容を元に構成しています。
有益な情報を共有していただいたことに感謝します。 🙏

https://kotamat.com/post/nextjs-strict-csp/


CSP を利用することで、ウェブサイトにおけるコードの実行を制限することができ、XSS による攻撃があった場合にその影響範囲を限られた範囲に止めることができます。

本記事では、Next.js での CSP ヘッダの対応方法の一例を紹介します。

tl;dr;

  • ポリシーは Google が提唱する Strict CSP に沿って設定する
  • _document.tsx を利用して head<meta httpEquiv="Content-Security-Policy" content={ ... Policy here ... } /> の形で CSP ヘッダを埋め込む

前提

  • Next.js 12.x
  • レンダリング方式は SSR と CSR を採用している
  • Script コンポーネントで外部の script を読み込んでいる

Strict CSP とは

Google が提唱する Strict CSP は、より高度なセキュリティを実現するための CSP の設定です。

Strict CSP は、アプリケーションのユースケースに依存するため、どのようなユースケースで利用するかを事前に慎重に検討する必要があります。

strict-dynamic ディレクティブとは

Strict CSP では strict-dynamic ディレクティブを利用して script に信頼を与えます。 strict-dynamic はnonce値やscriptから生成されたhash値によって信頼に値するかどうか判断を下します。そして、その信頼されたscriptから動的に読み込まれたscriptにも信頼は自動的に伝搬します。

このように strict-dynamic ディレクティブを利用すると、トップレベルのスクリプトに対してのみ信頼を付与すれば良いため煩雑なホワイトリストの管理は不要になります。

(strict-dynamic によって付与された信頼が伝搬する様子)

Next.js で Strict CSP を実装する

実装方針を以下のとおりとしました。

  • 基本的に Strict CSP に従う
  • nonce でスクリプトを信頼する
    • 複数の外部スクリプトがありそれらのハッシュ値を計算するのが煩雑だったため
  • meta タグの形で CSP ヘッダを設定する
    • next.config.js でもレスポンスヘッダを返却することは可能だが、値を動的に変更することができない
    • 動的に nonce の値を生成するために _document.tsx で meta タグを生成する方式を採用した

実装例

以下に実装例を示します。

import { randomBytes } from 'crypto'

import { Head, Html, Main, NextScript } from 'next/document'

const Document = () => {
  const nonce = randomBytes(128).toString('base64')
  const csp = `object-src 'none'; base-uri 'none'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https: http: 'nonce-${nonce}' 'strict-dynamic'`

  return (
    <Html lang="ja">
      <Head nonce={nonce}>
        <meta httpEquiv="Content-Security-Policy" content={csp} />
      </Head>
      <body>
        <Main />
        <NextScript nonce={nonce} />
      </body>
    </Html>
  )
}

export default Document
  • _document.tsx を利用して、<meta httpEquiv="Content-Security-Policy" content={ ... } /> の形で CSP ヘッダを埋め込むことができます。
    • next.config.js でレスポンスヘッダを設定することも可能ですが、 nonceはページリクエスト毎に毎回生成する必要があるためmetaタグとしてCSPヘッダを返す方式としました。
    • Strict CSP に沿って以下のディレクティブを指定します
      • object-src 'none'
      • base-uri 'none'
      • script-src 'self' 'unsafe-eval' 'unsafe-inline' https: http: 'nonce-${nonce}' 'strict-dynamic'
        • ${nonce} の値は動的に生成されるランダムな値です
  • next/document からimportした Head , NextScript コンポーネントそれぞれの nonce プロパティに生成したnonce値を渡します

以上が、Next.js で CSP ヘッダを対応する方法の概要です。

確認してみる

実行したアプリケーションで生成されたHTMLを参照してみると CSP ヘッダに設定されている nonce 値と同じ値が各 script タグに設定されていることがわかります。

ちなみに、 Head コンポーネントにnonceを設定し忘れると以下のようなエラーが出てscriptの実行が停止することが確認できます。

Refused to load the script '<URL>' because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-eval' 'unsafe-inline' https: http: 'nonce-JyWbm9+... (snip) ...wURE=' 'strict-dynamic'". Note that 'strict-dynamic' is present, so host-based allowlisting is disabled. Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback. 

さいごに

以上が、Next.jsでのCSPヘッダの対応方法についての説明です。

Next.js での CSP ヘッダの扱い方について調べていてまとまった情報が少ない印象だったのでまとめてみました。

この記事が誰かの役に立てれば幸いです。

参考リンク

https://kotamat.com/post/nextjs-strict-csp/
https://csp.withgoogle.com/docs/strict-csp.html
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Security-Policy/Sources
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#strict-dynamic

補足

補足 Scrap を書きました

https://zenn.dev/snaka/scraps/0bf0aba6fb4351

Discussion