CSPを段階導入する記事を書いた直後、自社サイトで実装中に壁にぶつかりました。AstroのView Transitions (<ClientRouter />) を有効化したまま、'unsafe-inline'もdata:も許可しないstrict CSPにしたい。けれどこの組み合わせは、技術的に詰みます。
Astro公式ドキュメントが明示的に「CSP integrationはClientRouter非対応」と書いていて、回避策は実質3つしかありません。
何が起きるか
Report-Onlyで違反を集めていると、/contact/ページから1件のCSP違反が届きました。
{
"effectiveDirective": "script-src-elem",
"blockedURL": "data",
"sourceFile": "ClientRouter.astro_astro_type_script_index_0_lang.js",
"documentURL": "https://staging.example.com/contact/"
}
blockedURL: "data"はChromeがdata:スキームURIをプライバシー上の理由で省略表記したもの。ClientRouterがランタイムで動的に<script src="data:text/javascript,..."を作成して前ページのinline scriptを再評価しています。
CSPの'unsafe-inline'は<script>foo</script>形式のインラインタグを許可しますが、<script src="data:...">形式のdata URIスクリプトは別カテゴリで未許可です。
試したこと: Astro security.csp + strict-dynamic
Astro 6には安定機能としてsecurity.cspがあります。inline scriptのSHA hashを自動生成して<meta http-equiv="content-security-policy">タグに埋める仕組み。
// astro.config.mjs
export default defineConfig({
security: {
csp: {
algorithm: 'SHA-256',
scriptDirective: {
strictDynamic: true,
},
directives: [
"default-src 'self'",
// ...
],
},
},
});
strict-dynamicを有効にすると、ハッシュ済みスクリプトから動的にロードされたスクリプトも信頼の連鎖に入ります。これでClientRouterのdata: URIスクリプトも救えるはず。
buildしてPlaywrightを流しました。56テスト中6件失敗。エラーは衝撃的でした。
Loading the script 'http://127.0.0.1:4321/_astro/WireframeBackground.js'
violates the following Content Security Policy directive:
"script-src 'self' 'sha256-...' 'strict-dynamic'".
Note that 'strict-dynamic' is present, so host-based allowlisting is disabled.
strict-dynamicを入れた瞬間、'self'が無効化されてAstro自身が生成した外部スクリプト(<script src="/_astro/...">)が全部ブロック。サイトが完全に動かなくなりました。
strict-dynamicの仕様を読み直す
CSP仕様によると、strict-dynamicは:
If
'strict-dynamic'is present, only scripts with valid nonce/hash are allowed, AND scripts dynamically added by such trusted scripts. Allowlist-based sources like'self',https:, and URL allowlists are IGNORED.
つまりstrict-dynamicを入れた瞬間、'self'もURLallowlistも無効化される。信頼の起点はnonceかhashだけ。
Astroが生成する<script type="module" src="/_astro/file.js">はHTMLパーサーが直接ロードするので、信頼の連鎖の対象外。動的にロードされたスクリプトではないので、strict-dynamicの保護下に入りません。
回避するには、外部スクリプトタグ全部にnonceかintegrity(SRI)を付与する必要がありますが、Astro 6のsecurity.cspはそこまで自動化していません。
Astro公式ドキュメントの明記
調査中にAstroのsecurity.cspセクションを精読し直して気付きました。
Astro’s CSP implementation has limitations: it does not support external scripts/styles out of the box (though hashes can be provided), nor Astro’s
<ClientRouter />view transitions.
Astro自身が「ClientRouter非対応」と明記している。設計上の制約として宣言済みです。
つまりAstro標準の機能だけでstrict CSP + View Transitionsを両立させる道は、現時点ではありません。
残された3つの選択肢
| 内容 | View Transitions | CSPの穴 | 工数 | |
|---|---|---|---|---|
| A | script-srcにdata:追加 | 維持 | 既存'unsafe-inline'に加えて1個 | 5分 |
| B | CF Pages Functions middlewareでnonce注入 | 維持 | なし | 2〜3h + 単一障害点 |
| C | <ClientRouter />削除 | 喪失 | なし | 5分 |
A: data:を許可する
script-src 'self' 'unsafe-inline' data:。最小コスト。
リスクは、攻撃者がXSSで<script src="data:text/javascript,任意コード"></script>を仕込めるようになること。けれど既に'unsafe-inline'がある時点で同等の経路は開いているので、incremental riskは小さい。
セキュリティ評価ツール(Mozilla Observatory等)では減点対象。監査時に「なぜdata:が許可されているか」を聞かれます。
B: middleware nonce注入
CF Pages Functionsでfunctions/_middleware.tsを作り、HTMLレスポンスをHTMLRewriterで処理。リクエスト毎にランダムnonceを生成して<script>タグに付与し、CSPヘッダーにも同じnonceを埋める。
export const onRequest = async (context) => {
const response = await context.next();
if (!response.headers.get('content-type')?.startsWith('text/html')) {
return response;
}
const nonce = crypto.randomUUID().replace(/-/g, '');
// HTMLRewriterで<script>にnonce属性を付与
// CSPヘッダーに 'nonce-...' 'strict-dynamic' 付与
// ...
};
これならstrict-dynamicが正しく動きます。ClientRouter(nonced=trusted)が動的に作るdata: URIスクリプトも信頼の連鎖に入る。
デメリットは実装工数とHTMLキャッシュ無効化、middlewareの単一障害点リスク。6 URLのコーポレートサイトでROIが見合いにくい。
C: ClientRouter削除
<ClientRouter />をBaseLayoutから外せば、data: URIスクリプトは生成されなくなり、script-srcにdata:を入れる必要がなくなります。
ページ遷移時のフェード演出は失います。実測では200〜400msの差。コーポレートサイトでこの差が事業に響くケースは稀。
このサイトで選んだ判断
自社サイトではAを選びました。理由は:
- 6 URLの静的サイトでBの2〜3hはROIが見合わない
- 既に
'unsafe-inline'があるので、data:追加のincremental riskは小さい - 「妥協した経緯を記事化する」方が、サニタイズされたbest practice記事よりクライアントに刺さる
_headersファイルに判断理由をコメントで残しました。将来読み返した時に「なぜここに穴がある」を即理解できる状態。
# CSPにdata:をscript-srcへ含める判断について:
# Astro View Transitions (ClientRouter)はランタイムでdata: URI scriptを
# 生成する設計。Astro security.csp integrationはClientRouter非対応と明記。
# middlewareでnonce注入する選択肢があるが、現時点では実装コストROIが
# 見合わないため、interimとしてdata:を許可する。
いつBに進むか
クライアント案件で同等の構成 (View Transitions必須 + 完全strict CSP) を要求された時、または自社サイトのリニューアル時にBへ移行する想定。その時にこの記事を書いた経験と、自社サイトで触ったWorker構築の知見が、提案の解像度を上げます。
View Transitionsとstrict CSPは、Astro 6時点では両立できません。妥協なしで進めるならmiddlewareでnonce注入、コスト優先ならdata:を許可してその判断を文書化する。どちらを選ぶかはROIと監査要件次第です。自社サイトで触ってみるとAstroの限界点が体感でき、客への提案も「動くと思います」ではなく「やったことあります」で語れます。