KAKESU~/@dotbind-kondo/wordpress-static-fallback-on-cloudflare-workers

PHP の緊急脆弱性で Zenlogic が止まる前に、WordPress を静的化して Cloudflare Workers で配信した

trail

前提Simply Static で静的化して Cloudflare に退避する案から始まった
却下Simply Static — 脆弱な PHP 上で新しいプラグインを動かすことになる。外から wget で取れば足りる
観察最初のミラーのカバー率は、アクセス上位のリクエスト数で見て 63%。/old/ などの WP 外の静的ディレクトリが抜けていた
決定GraphQL の上位 5,000 パスを基準に、欠けたものを追加取得して約 86% にした
却下25MiB 超を R2 で配信する案 — アクセス上位優先なら後回しでよく、リソースも増える
観察Redirection の正規表現は先頭一致でなく、本番でも /old/old/... と転送が続いていた
決定転送ルールは _redirects ではなく Worker で再現した(正規表現が要るため)。^ を付けてループを防いだ
観察www.example/* が *.example/.../event* に勝ち、特設ページがお知らせページになった
決定www を明示した特設ページのルートを追加して解消した(ゾーンのルート変更はユーザーが手で実施)

背景

2026-10-21、PHP 本体にログイン不要の RCE が 2 件見つかった(CVE-2026-109618 / 109619、CVSS 9.2、通称 php2shell)。mod_php・PHP-FPM のどちらでも、WordPress を含む構成で再現するとされ、Session Upload Progress(ext/session)が関係するとみられている。session.upload_progress.enabled=0 では防げないと追記されていた。修正版は日本時間 23:00 に公開予定だった。

これを受けて、Zenlogic ホスティングが緊急メンテナンスを告知した。

  • 10-21 22:00〜10-22 18:00(予定)に Apache を停止する(Web にアクセスできない)。
  • PHP 8.1 は、パッチの状況によっては最新版に上げる可能性がある。

学会サイト(クライアントA)の旧サイトは、この Zenlogic 上の WordPress(6.2 系・PHP 8.1)で、前段に Cloudflare がある。ゾーン(ドメイン)のプランは Free、Workers は Paid プラン。何もしなければ、約 20 時間サイト全体がエラー画面になる。そこで、止まる前に静的コピーを取り、Cloudflare から配信することにした。

当初の相談は「Simply Static で静的化して Cloudflare に退避するのはどうか」だった。

調査

  • 規模:サイトマップは 214 個のサブサイトマップに分かれていて、合計 1,266 URL(うちお知らせが 1,068)。静的化できる量だった。
  • 振り分け:既に一部のパス(特設ページ・ニュースメールのバックナンバー)は、新サイトの Worker にルートで振り分けられていた。それ以外は全部オリジンに届いていた。
  • 動的機能:
  • サイト内検索は Google カスタム検索で、静的化しても動く。
  • 決済フォームは外部サービスへの POST なので、これも動く。
  • 止まるのは、施設名称検索(WP の ?s=)・用語集の検索(POST)・英語版の問い合わせフォーム。
  • アクセス上位の名簿 2 ページは、全件(約 1.8 万行と 9 千行)がページ内の表になっていて、絞り込みは JS だった。静的化しても使える。
  • リダイレクト:WP の Redirection プラグインに 155 件のルールがあった(REST で取得)。
  • アクセスが多いのは、/guideline/pdf/*.pdf などを、昔の静的サイトのディレクトリ /old/ へ転送する正規表現ルール。
  • これらの正規表現は先頭一致になっていない。そのため存在しない PDF は、本番でも /old/old/old/… と転送が続き、元から開けなくなっていた。
  • アクセス上位:Cloudflare の GraphQL(httpRequestsAdaptiveGroups)で直近 23 時間の上位 5,000 パス(約 46.5 万リクエスト)を取り、写しのカバー率をリクエスト数で重み付けして測った。
  • 最初のミラーだけでは 63% だった。WP の外にある静的ディレクトリ(/old/・/share/・/sikkanpg/ など)が抜けていたためで、追加取得して 71% になった。
  • これに既存ルートの範囲 15% を足すと約 86%。残りは、本番でも 404 になるものや wp-admin・wp-cron など、静的版では要らないものだった。
  • wget の保存名:
  • クエリ付きの CSS・JS は ? が @ に置き換えられて保存される(style.css@ver=6.2)。そのため、ファイル名を戻す処理が要る。
  • 本当に @ を含むファイル名(img@sm.png のような画像。@は実際は半角)を取り違えたので、「@ のあとに = があるときだけクエリとみなす」に直した。
  • 日本語の URL は UTF-8 のファイル名で保存され、Workers の静的アセットでそのまま配信できた。
  • ファイルサイズ:Workers の静的アセットは 1 ファイル 25MiB まで。超えたのは 9 ファイル(動画 4 本で計約 5GB、PDF 数個)。ファイル数の上限はプランによって違うが、Workers が Paid プランなので、約 6,300 ファイル・2.5GB でも問題にならなかった。

判断

  • Simply Static は使わず、外から wget でミラーした。 脆弱な PHP 上で新しいプラグインを動かさずに済み、サーバーに手を入れずに速く取れる。
  • 配信は Worker 1 本にした(静的アセット+未一致時の処理)。
  • html_handling: auto-trailing-slash。
  • アセットに無い URL では、Redirection の有効ルールを Worker で再現する。固定 URL は大文字小文字と末尾スラッシュを区別しない。正規表現はループしないよう ^ を付け、行き先が壊れていたものは $1 に直した。
  • それでも無ければ、メンテナンスのお知らせを 404 で返す。GET・HEAD 以外は 503 にして、PHP に届かないようにした。
  • R2 は使わなかった。 25MiB を超えるファイルは、アクセス上位を優先する方針で今回は配信しない。
  • ルートの切り替え:www/ と apex の / をこの Worker に付けた。ゾーンのルート操作だけで、数秒で切り替えられ、戻すのも 2 行を削除するだけ。
  • 落とし穴(Workers ルートの優先順位):Workers ルートはパスの長さより先に、ホスト名の具体さで優先される。www.example/ が、既存の .example/…/event* に勝ってしまい、特設ページがお知らせページになった。www を明示したルートを新サイトの Worker 側に追加して直した。バックナンバーは www 版のルートもあったので無事だった。
  • プレビュー:workers.dev の URL はデプロイ時は無効にしていた。確認のために一時的に有効にし、切り替え後に workers_dev: false と preview_urls: false で閉じた。手で付けたゾーンのルートは、wrangler deploy(設定にルートが無い状態)では消えなかった。
  • お知らせページ:クライアント側が用意した HTML を元に、「実施予定日時」の見出し、終了時刻が前後しうる旨の注記、トップページへのボタン、noindex を足した。
  • 今後の扱い:停止中は PHP が動かないので、静的化は表示を続けるための対策。セキュリティ上の効果は、停止前の約 1 時間、外から PHP を隠せたことぐらい。

Next Action

  • メンテ明け(10-22 18:00 以降)の作業:
  1. Zenlogic のコントロールパネル(WEB → PHP 設定)で PHP のバージョンを確認する。8.1 はサポートが終わっているので、上がっている可能性が高い。
  2. 静的版のまま、オリジンに直接つないで WP 6.2 と古いプラグインが動くかを確認する。
  3. 問題がなければ、ルート 2 本を削除して WP に戻す。www を明示した特設ページのルートは残してよい。
  • PHP の更新で WP が壊れていたら、静的版のまま修正する(別見積もり)。
  • apex は静的アセットが先に応答するため、www にリダイレクトされない(未対応)。必要なら Worker 側で直す。
  • Worker と前処理スクリプトは残しておく。次にオリジンが止まるときも、ルートを付けるだけで再利用できる。写しは取り直しが必要。

※ クライアント名・ドメイン・Worker 名・特設ページ名・Cloudflare のアカウント ID・workers.dev のサブドメイン・費用の見積もりは省略した。パスは相対パスにした。