Next.jsでStrict CSPを実現する

この記事は個人ブログと同じ内容です。

kotamat.com

業務では Nuxt.js を使用しているのですが、CSP の nonce の対応ができておらず、Next.js だとどうなんだろうと触ってみたらメチャクチャ簡単に設定できる事がわかったので、その方法について紹介します。

やりたいこと

CSP の対応のなかで Google が提唱しているやり方としてStrict CSPというのがあります。 これは簡単に言うと下記のように CSP の設定を行えば モダンなブラウザにおいて プロダクションレベルのセキュリティーが担保できるよというものです。

Content-Security-Policy:
  object-src 'none';
  script-src 'nonce-{random}' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
  base-uri 'none';

このなかで大事なものとしては strict-dynamic が挙げられます。これは、CSP Level3 から導入されたディレクティブで、ほかのドメインホワイトリストは無視した上で、'nonce-{random}'で指定された <script> タグのスクリプトを許可し、かつその先で生成された別のスクリプトに関しても同様に許可するというものです。

この挙動により、開発者はトップレベルのスクリプトに関して nonce をつければ良くなり、攻撃者はその nonce を奪えない限り XSS ができなくなるため、ホワイトリスト形式での設定より安全になるということで、非常に便利に対策ができるようになります。

注意点としては、モダンなブラウザでなければ strict-dynamic は解釈してくれないので、そういったブラウザでは脆弱性は防ぎきれないというところになります。 また Safari はまだ対応していません。(モダンなブラウザではないということですかね…?)

https://caniuse.com/?search=strict-dynamic

対応方法

/pages/_document.tsx にて、nonce の生成と、Head,NextScript への反映を行います。

import { randomBytes } from "crypto";
import Document, { Html, Head, Main, NextScript } from "next/document";

type WithNonceProp = {
  nonce: string;
};
export default class MyDocument extends Document<WithNonceProp> {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    const nonce = randomBytes(128).toString("base64");
    return {
      ...initialProps,
      nonce,
    };
  }
  render() {
    const nonce = this.props.nonce;
    const csp = `object-src 'none'; base-uri 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http: 'nonce-${nonce}' 'strict-dynamic'`;

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

これだけです。

Next.js では、SSR では当然のこと、SSG する際にもこの処理を適応してくれるため、例えば下記のようなページコンポネントがあったとしても

import { GetStaticProps } from "next";
import { FC } from "react";

type Props = {
  name: string;
};
const SSG: FC<Props> = ({ name }) => {
  return <div>{name}</div>;
};

export default SSG;

export const getStaticProps: GetStaticProps<Props> = async (ctx) => {
  return { props: { name: "hoge" } };
};

next export 後のファイルに nonce がちゃんと反映されています。 ただ、当然ですが、SSG だと nonce の本来の持つべき役割である 「リクエストごとに生成し直す」ことができないため、SSG を前提としたプロダクトでは攻撃者の攻撃にさらされる危険性があります。

まとめ

Next.js にて nonce を用いた StrictCSP を実装する方法を紹介させていただきました。 モダンではないブラウザが対応してくれれば、相当対策をしやすくなるので、ブラウザの対応に期待したいですね。

余談

Nuxt.js では、この辺で HTML タグが直書きされていることによって nonce を入れる余地がないため、今回のやりたいことはできない感じになっています。 その代わり inlineScript の hash 値を CSP に埋め込む形で実装されているため、アプリケーションのソースコードに閉じる限りはそれなりに硬い対策になるようになっているようです。