Next.jsでCSP対応をしました
CSPとは何か
Content Security Policy の略です。
適切なCSPディレクティブを定義することによって、読み込むコンテンツを制限できることで予期しない悪質なコンテンツを読み込まされることを防ぎます。
CSPを有効にするには2つの方法があります。
-
Content-Security-Policy
HTTPヘッダーを返す -
<meta>
タグに埋め込む(弊社はこちらで有効にしています)
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; img-src https://*; child-src 'none';" />
ポリシーの構文は
Content-Security-Policy: <policy-directive>; <policy-directive>
<policy-directive>
は <directive> <value> <value> ...
の形で指定することができます。
適用例はMDNの例が分かりやすいので参考にしてみてください。
JavaScriptの実行を制御するためには以下のような方法があります。
nonce
nonce
(number used once) 値をリクエストごとに生成して指定します。
Content-Security-Policy: script-src 'nonce-2726c7f26c'
スクリプトは次のように制御されます。
// nonce が一致するため実行されます。
<script src="/js/hoge.js" nonce="2726c7f26c"></script>
// nonce が一致しないため実行されません。
<script src="/js/hoge.js" nonce="12345"></script>
// nonce が一致しているので実行されます。
<script nonce="2726c7f26c">
var inline = 1;
</script>
// nonce が一致しないため実行されません。
<script nonce="12345">
var inline = 1;
</script>
hash
筆者の所感ではサブリソース完全性と似ているなと思いました。
このようなスクリプトを実行したい時
<script>hoge();</script>
スクリプトのハッシュ値をbase64でエンコードします。
sha256, sha384, sha512に対応しています。
echo -n 'hoge();' | openssl sha256 -binary | openssl base64 // 5KUX4DuAakiviv/EBxmQrE/U2E7Uvb+4BTX2tQ24DgU=
ハッシュアルゴリズム-<ハッシュ値>
を指定することでブラウザ側で値の検証をし、実行を制御します。
Content-Security-Policy: script-src 'sha256-5KUX4DuAakiviv/EBxmQrE/U2E7Uvb+4BTX2tQ24DgU='
ドメインのホワイトリスト
以下のように設定します。
Content-Security-Policy: script-src 'self' example.com
スクリプトは次のように制御されます。
// example.com から読み込むので実行されます。
<script src="https://example.com/hoge.js"></script>
// 自分自身のドメインから読み込むので実行されます。
<script src="/js/hoge.js"></script>
// インラインスクリプトは実行されません。
<script>alert("hoge")</script>
pixivさんの記事がとても分かりやすいです。
やったこと
Strict CSP
への対応
Vercelのプレビュー環境以外では Strict CSP
という方法を適用しました。
ページのリクエストごとに新しい nonce
を生成して、すべての <script>
タグに nonce
属性を付与します。JavaScriptの実行は nonce
によって制御されます。
strict-dynamic
で nonce
によるルートスクリプトの実行許可を伝播させていきます。これにより、ルートスクリプトのセキュリティをキープしながら、フレームワークやライブラリによって動的に挿入されたスクリプトをCSPによってブロックしないようにすることができます。
Vercel
のプレビュー環境への対応
プレビュー環境はコメントを残せる機能があります。(すごく便利!)
そのため、いくつかスクリプトが埋め込まれますが、ポリシーに違反するスクリプトは実行できないため、1つずつエラーを見ていってドメインをホワイトリストに設定し許容するようにしました。
ちなみに、Vercelの公式ドキュメントに記載のある値も追加しています。
実装
上記を踏まえ、以下のような実装になりました。
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
} from 'next/document';
import crypto from 'crypto';
import { v4 } from 'uuid';
type DocumentProps = {
csp: string;
nonce: string;
};
const isVercelPreview = process.env.VERCEL_ENV === 'preview';
const generateCsp = () => {
const hash = crypto.createHash('sha256');
hash.update(v4());
const nonce = hash.digest('base64');
/**
* Strict SCPを適用します
* @see https://csp.withgoogle.com/docs/strict-csp.html
*/
const csp = `
script-src 'unsafe-eval' 'unsafe-inline' https: http: ${
isVercelPreview
? `https://vercel.live/ https://vercel.com`
: `'nonce-${nonce}' 'strict-dynamic'`
};
object-src 'none';
base-uri 'none';
${
isVercelPreview
? `connect-src 'self' https://vercel.live/ https://vercel.com https://*.pusher.com/ wss://*.pusher.com/;
img-src 'self' https://vercel.live/ https://*.vercel.com https://vercel.com https://*.pusher.com/ data: blob:;
font-src 'self' https://*.vercel.com https://*.gstatic.com;
frame-src 'self' https://vercel.live/ https://vercel.com;
`
: ''
}
`
.replace(/\s{2,}/g, ' ')
.trim();
return {
csp,
nonce,
};
};
export default class MyDocument extends Document<DocumentProps> {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
const { csp, nonce } = generateCsp();
return {
...initialProps,
csp,
nonce,
};
}
render() {
const { csp, nonce } = this.props;
return (
<Html lang="ja">
<Head nonce={nonce}>
<meta httpEquiv="Content-Security-Policy" content={csp}></meta>
</Head>
<body>
<Main />
<NextScript nonce={nonce} />
</body>
</Html>
);
}
}
サブドメインのワイルドカードはメインのドメインをカバーしないため、 https://*.vercel.com
https://vercel.com
のようにそれぞれ指定しているところもあります。
懸念事項
SSR前提で設計する場合、リクエストの度に nonce
を生成するという条件をクリアできるのですが、SSGではこれをクリアできません。
筆者は利用したことがないのですが、このようなライブラリもあるみたいです。
このライブラリは
-
getStaticProps
->hash方式
-
getServerSideProps
->nonce方式
-
getStaticProps + Revalidate
->hash方式
というようにレスポンスのHTTPヘッダーにCSPを設定してくれるようです。
参考
Next.jsでの実装に際して、以下の記事を大変参考にさせていただきました。
ありがとうございました!
おわりに
XSSなどの攻撃に対して予期しないスクリプトをブロックできるCSPは強力な対策になります。
また、スクリプトの改竄でユーザーの情報が漏洩するケースが多く見られるため、SRIの対応をしたり、以下のような製品と組み合わせてさらにセキュリティを向上させていくのが良いかと思いました。
Discussion