CSPを設定する
Content-Security-Policyを設定すると、コンテンツに紛れ込んだスクリプトを動かさずに済みます。このモードでは、フレームワークが応答ごとに新しいnonceを作るので、それを名指すポリシーをguard.tsでヘッダーに書きます。
このページの内容
フレームワークが署名するもの
フレームワークはポリシーを決めません。自分がHTMLに書くインラインスクリプトに署名するだけで、それを許すポリシーはアプリが書きます。
フレームワークが書くインラインスクリプトは、hydrationのためにHTMLへ埋め込むペイロードと、Reactのスクリプトです。
アプリが自分で書くインラインスクリプトも、同じようにポリシーで許します。@k8ordo/color-schemeが描画の前に色を決めるスクリプトも、その1つです。
guard.tsでポリシーを書く
フレームワークは応答ごとにnonceを作り、自分のインラインスクリプトとモジュールのスクリプトに付けます。@k8ordo/server/runtimeのnonce()がその値を返すので、ルートのguard.tsでポリシーに書き込みます。
src/routes/guard.tsimport { nonce, responseHeaders } from '@k8ordo/server/runtime';
export default function guard() {
const policy = [
`script-src 'nonce-${nonce()}' 'strict-dynamic'`,
"object-src 'none'",
"base-uri 'none'",
].join('; ');
responseHeaders().set('content-security-policy', policy);
}ルートのguard.tsは、redirect.tsを除くすべての答えの前に走ります。そのため、ページにもペイロードにも、どこにも当たらなかったURLへの404にも、同じポリシーが付きます。
モジュールのスクリプトにもnonceが付くので、'strict-dynamic'の下では、そこから残りのクライアントのコードが読み込まれます。
アプリのインラインスクリプトに署名する
アプリ自身のインラインスクリプトには、同じnonceを付けます。@k8ordo/color-schemeなら、ルートレイアウトでColorSchemeProviderにnonce={nonce()}を渡します。
src/routes/layout.tsximport { ColorSchemeProvider } from '@k8ordo/color-scheme';
import { nonce } from '@k8ordo/server/runtime';
import type { ReactNode } from 'react';
export default function RootLayout({
children,
}: {
children: ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ColorSchemeProvider nonce={nonce()}>
{children}
</ColorSchemeProvider>
</body>
</html>
);
}nonce()は、リクエストに答えている間ならどこで呼んでも同じ値を返します。描画の中も同じです。スクリプトに署名することは、応答を書くことではないからです。
リクエストの外、たとえばモジュールのトップレベルで呼ぶと、例外を投げます。
nonceの付いた答えをキャッシュしない
nonceは、答えのたびに新しいことで意味を持ちます。CDNなどの共有キャッシュに置くと同じnonceが何度も配られ、攻撃者が前もって知ったnonceを付けて、スクリプトを差し込めてしまいます。nonceを含む答えは、共有キャッシュに置かないでください。