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