0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cloudflareの_headersが「1行2000文字」上限で落ちた(CSPハッシュ列挙の原因と対処)

0
Posted at

自社サイトで wrangler deploy を実行したところ、Cloudflare の _headers にある「1行2000文字」の上限に引っかかってデプロイが止まりました。
原因は CSP の script-src に並べていたインラインscriptの sha256 ハッシュです。ページを足すたびにハッシュが増え、1行が少しずつ伸びていました。
対処として、ハッシュを「共通行」と「ページ別ルール」の2層に分けました。最長行は 2077 文字から 1374 文字に下がり、ページを追加しても共通行は伸びなくなりました。

前提

  • 静的サイトを wrangler deploy で Cloudflare に載せている
  • レスポンスヘッダは _headers ファイルで付けている
  • _headers はビルド時に自動生成する
    • 生成元は src/lib/csp.mjs
    • 生成された _headers の先頭に「直接編集しても次のビルドで消える」と書いてある
  • CSP の script-src には 'unsafe-inline' を使わず、インラインscriptの sha256 ハッシュを並べている(Observatory 対策)
  • 障害の時点で、サイト全体のユニークなインラインscriptは18個。ハッシュの部分だけで954文字あった

起きたこと

CSP に埋め込み用のホストを2つ足して wrangler deploy を実行すると、次のエラーで失敗しました。

Invalid _headers configuration: Line 6: Line exceeds limit of 2000 characters [code: 100324]

ホストを足す直前までデプロイは通っていました。
ただし、上限2000文字に対する余裕は8文字しか残っていませんでした。
ホスト2つ分の文字数で、6行目の CSP が上限を超えた形です。

原因:ハッシュ列挙は「ページを足すほど伸びる」構造

'unsafe-inline' を使わずにハッシュを並べる運用には、次の性質があります。

  • インラインscriptの中身が1文字でも違えば、別のハッシュが必要になる
  • ツールページや LP を1本足すたびに、そのページだけのscriptのハッシュが共通の CSP 行に加わる
  • 全ページ共通の /* ルールにまとめて載せると、1行が単調に伸び続ける

今回落ちた直接のきっかけはホストの追加です。ただ、ハッシュが増え続ける構造そのものが、いずれ上限に当たる時限爆弾でした。

対処の方針:ハッシュを2層に分ける

インラインscriptを「何ページに出るか」で2種類に分けました。

層 載せるハッシュ 本数・文字数 置き場所
共通行 半数以上のページに出るscript(テンプレート由来) 4本・212文字 全ページ共通のルール
ページ別 特定のページにしか出ないscript(ワンオフ) ページごと パス別ルールで CSP を上書き

ワンオフのscriptを持つページは24ページありました。
この24ページには、パス別のルールで「共通のハッシュ+そのページだけのハッシュ」を並べた CSP を発行し直します。

手順

1. 生成物ではなく生成元を直す

最初は生成された _headers を直接編集してしまい、次のビルドで書き戻されて1回無駄にしました。
_headers の先頭に書いてあるとおり、修正先は生成元の src/lib/csp.mjs です。

2. インラインscriptをページ数で振り分ける

全ページのインラインscriptのハッシュを集め、ハッシュごとに出現ページ数を数えます。

  • 半数以上のページに出る → 共通行へ(今回は4本)
  • それ以外 → そのページのパス別ルールへ

3. _headers を2層で出力する

出力の形は次のとおりです(ハッシュ値と他のディレクティブは伏せています)。

/*
  Content-Security-Policy: <共通のディレクティブ>; script-src <許可ホスト> 'sha256-<共通1>' 'sha256-<共通2>' 'sha256-<共通3>' 'sha256-<共通4>'

/<ワンオフscriptを持つページのパス>
  ! Content-Security-Policy
  Content-Security-Policy: <共通のディレクティブ>; script-src <許可ホスト> 'sha256-<共通1>' 'sha256-<共通2>' 'sha256-<共通3>' 'sha256-<共通4>' 'sha256-<このページ固有>'

ポイントは ! Content-Security-Policy の行です。
/* から継承した CSP をパス別ルールでいったん外し、共通ハッシュとページ固有ハッシュをまとめた CSP を1本だけ付け直します。

4. postbuild に「行長の検査」を入れる

ビルド後に _headers を読み、2000文字を超える行があればビルドを止める検査を足しました。
考え方だけを最小の形にした例を以下に示します(実際のスクリプトそのものではありません)。

// postbuild の検査の考え方(最小例)
import { readFileSync } from "node:fs";

const LIMIT = 2000;
const lines = readFileSync("<出力先>/_headers", "utf8").split("\n");

lines.forEach((line, i) => {
  if (line.length > LIMIT) {
    console.error(`_headers ${i + 1}行目が ${line.length} 文字(上限 ${LIMIT})`);
    process.exit(1);
  }
});

上限を超えた時点ではなく、上限に近づいた時点で気づけるようにするのが、次の落とし穴の話です。

落とし穴

同じ CSP ヘッダーを複数行で足しても union にはならない

「ページ固有のハッシュだけを、2本目の Content-Security-Policy として追加すればいい」と考えたくなりますが、この方法は使えません。
CSP が複数ある場合は和集合(union)ではなく、すべてを満たす必要がある AND で評価されます。

  • 1本目(共通)にないページ固有のscriptは、1本目に止められる
  • 2本目(固有)にない共通のscriptは、2本目に止められる

このため、パスごとに ! で外してから1本にまとめ直す形が正解です。

検査が「緑」でも余裕がほとんど無い

障害の直前、行は上限の内側に収まっていたので、どの検査も緑でした。
実際の余裕は8文字です。上限つきの設定ファイルでは、今の長さではなく「上限までの余裕」を検査の対象にする必要があります。余裕が2桁の文字数しか残っていない状態を緑にしてはいけません。

生成物を直接直してしまう

手順1で書いたとおりです。自動生成ファイルを触る前に、ファイルの先頭を読みます。

結果

  • 共通行は 1,320 文字になった
  • _headers の最長行は 2077 文字 → 1374 文字
  • ページを足しても、増えるのはそのページのパス別ルールだけで、共通行は伸びない
  • 上限を超える行が出たらビルドの段階で止まるようになった

まとめ

  • Cloudflare の _headers には1行2000文字の上限がある(code: 100324)
  • CSP のハッシュ列挙を全ページ共通の1行にまとめると、ページを足すたびに上限へ近づく
  • 「半数以上のページに出るscript」だけを共通行に置き、ワンオフは ! Content-Security-Policy を使ったパス別ルールで上書きする
  • 複数の CSP は AND で評価されるため、追加分を2本目のヘッダーとして足す方法は使えない
  • 上限つきの設定ファイルは、長さではなく「上限までの余裕」を検査する
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?