2026 年 10 月 21 日 21 時 55 分、高市総理が X にこう投稿しました。
修正パッチを速やかに適用することが困難な場合は、例えばウェブサイト等を一時的に停止させることも、リスクを低減させるための有効策の一つと考えられます。
(高市早苗 on X)
PHP の任意コード実行の脆弱性 2 件、通称 php2shell(CVE-2026-109618 / CVE-2026-109619、CVSS v4.0 9.2)の夜です。
その夜、私が保守を担当しているオンラインサービス事業者 A 社の複数のサイトは、PHP 7.4 で動いていました。公式の修正版が出ない版です。修正パッチを「速やかに適用することが困難」な側でした。
それでもサイトは止めず、WAF と PHP の設定で穴をふさぎました。と書くと格好いいのですが、やったことは「入口で multipart を断る」と「ini に 1 行足す」の 2 つだけです。ふさいだ直後には、実際の攻撃がログに来ています。この記事は、その夜の判断と手順、途中で踏んだ罠を時系列で書いたものです。
この記事を書いたのは、あの夜がちょっと楽しかったからです。不謹慎に聞こえるかもしれませんが、X やチャットには、どこまで分かったか、どこで止めるか、どの暫定策が効くかが次々に流れてきました。それを見ながら手を動かすのは、知らない人たちと同じ夜を一緒に乗り越えている感覚でした。たくさんの情報にもらった分、今度は私たちの夜の出来事をシェアしたいと思います。
守秘義務の範囲で、社名・サービス名・ドメイン・IP アドレス・規模などは伏せています。記事中のドメインは
example.comに置き換えています。構成や設定は当夜の時点のもので、その後の対応状況は書いていません。
まず、放っておくと何が起きるのか
夜の話の前に、この脆弱性が何を許すのかを書いておきます。ここを知らないと、この先の対応が大げさに見えると思います。公開されている分析をもとに整理しました。
攻撃者が直接手に入れるものは 2 つです。
| 得られるもの | 中身 | 出典 |
|---|---|---|
| 任意のコード実行 | ログイン不要で、PHP を動かしているユーザーの権限でコマンドを実行できる | php.net、Sansec |
| メモリの中身 | Heartbleed のように、他の利用者のリクエストに含まれるパスワード・セッショントークン・API キーが漏れうる | Sansec |
2 つ目が地味に怖いところです。サーバーに何も書き込まれず、侵入の痕跡も残らないまま、利用者のログイン情報だけが抜かれる可能性があります。
そこから先は、Web サーバーでコードを実行された場合に一般に起きることです。今回の攻撃で実際に観測された動きではありません。
- データの持ち出し:アプリは DB に接続するために接続情報を持っています。PHP の権限で読めるので、利用者の情報をまとめて持ち出せます
- クラウドへの横展開:EC2 の上なら、インスタンスに付いたロールの認証情報をメタデータから取られえます。そのロールにできることは、攻撃者にもできます
- 居座り:Web シェルや cron を仕込まれると、あとから PHP を更新しても抜けません。脆弱性をふさぐことと、入られた後を片付けることは別の作業です
- 利用者への被害:ページを書き換えられると、被害は自社のサーバーの外、利用者の端末に広がります
1 本の POST で、ここまでの入口が開きます。実際、この夜は私の担当サイトにもその POST が届きました(後半で書きます)。
その夜、止まったサービス
止める判断をした大手サービスも多くありました。報道で確認できた主なものです(時刻は報道時点のもので、その後変わった可能性があります)。
| サービス | 運営 | 停止の時間帯(報道時点) | 出典 |
|---|---|---|---|
| BASE | BASE | 10/21 15:50 〜 10/22 7:00 | GIGAZINE |
| メロンブックス | メロンブックス | 10/21 17:30 〜 10/22 10:00 | 同上 |
| ニコニコ | KADOKAWA | 10/21 19:00 〜 未定 | 同上 |
| バンダイチャンネル | バンダイ | 10/21 20:00 〜 未定 | 同上 |
| pixiv | pixiv | 〜 10/22 早朝 | 同上 |
| BOOK☆WALKER | BOOK☆WALKER | 〜 10/22 早朝 | 同上 |
| ウマ娘 プリティーダービー | Cygames | 10/21 22:00 ごろ 〜(23 時から前倒し) | 同上 |
| グランブルーファンタジー | Cygames | 10/21 22:00 ごろ 〜(23 時から前倒し) | 同上 |
| Shadowverse: Worlds Beyond | Cygames | 10/21 23:00 ごろ 〜 未定 | ファミ通 |
| Pokémon Sleep | ポケモン | 10/21 23:00 〜 未定 | GIGAZINE |
| ヘブンバーンズレッド | WFS | 10/21 22:30 以降 | Game Spark(Yahoo!ニュース) |
| makeshop / カラーミーショップ | 各社 | 10/21 夜 〜 10/22 | 日本ネット経済新聞(Yahoo!ニュース) |
ゲーム、EC、動画、電子書籍と、業種を問わず止まっています。上の入口の広さを考えると、売上を手放してでも止めた各社の判断の重さがよく分かります。BASE のように 23 時の修正版公開を待たず、夕方から止めたところもありました。
止めるのは正しい判断の 1 つです。ただ、止めると売上も利用者の信頼も削られます。A 社のサイトは、止めずに守る道を選びました。
全体の流れ
夜の始まり:守る相手はどんな環境だったか
| 項目 | 内容 |
|---|---|
| サイト | 複数のサービス(同じテンプレートで作られた構成) |
| 構成 | AWS の ALB → EC2(nginx + php-fpm)、サイトごとに複数台を ASG で管理 |
| PHP | 7.4 系。サードパーティのリポジトリから入れたもので、公式サポートは 2022 年に終了済み |
| 管理画面 | 同じサーバーの別ホスト名で動作。nginx で接続元 IP を制限済み |
| WAF | なし |
いちばん厄介なのは PHP 7.4 でした。php.net の告知では、修正版が出るのは 8.2〜8.5 だけです。7.4 / 8.0 / 8.1 には、ソースに当てる最小限のパッチ(GitHub のブランチ)しか出ていません。パッケージで入れた PHP に「更新を入れて終わり」はできない状況でした。
告知を待つ間:バージョンだけで判定しない
最初に、影響の判定軸を 3 つに決めました。
- 影響するバージョンか
- 攻撃が成立する条件(設定・経路)があるか
- インターネットから届くか
調査には AWS Systems Manager(SSM)の Run Command を使い、読み取り専用のコマンドだけを全台に流しました。Web 以外の用途のサーバーにも PHP が入っていることがあるので、サーバー名で対象から外さず全台を見ています。
# 全台に流した確認の一部(読み取りのみ)
php -v | head -1
php -i | grep -E '^session\.upload_progress\.enabled'
ss -ltnp | grep php-fpm # php-fpm がどこで待ち受けているか
結果は次のとおりです。
| 対象 | 判定 | 理由 |
|---|---|---|
| Web サーバーの PHP | 影響あり・高 | 全台 7.4。攻撃に関係するとされた session.upload_progress.enabled が既定値の On |
| 同じ日に報じられた nginx の脆弱性(PoolSlip) | 影響低 | 発動条件になる rewrite の書き方がなかった |
| Web 以外のサーバーの php-fpm | 外から届かない | 127.0.0.1 でしか待ち受けておらず、nginx から渡す経路もない |
WAF を設計する:止める前に、何が壊れるかを調べる
報道によると、攻撃には multipart/form-data(ファイルアップロード形式)のリクエストが使われます。そこで「multipart を入口で止める」WAF を考えました。
ただし大ざっぱに止めると、正規の機能が壊れます。提案の前に、2 つの方向から影響範囲を確かめました。
-
ソースコード:送る側(フロントの
FormData、ネイティブアプリのフォーム送信など)と、受け取る側($_FILESなど)を洗い出す- multipart を使っていたのは、管理画面の画像・CSV 取込(十数系統)だけでした。共通の通信処理が見つかったので、判定はすぐ確定しました
- ネイティブアプリは生のバイト列を、Web のアイコン登録は base64 文字列を通常のフォーム形式で送っていました
- アクセスログ:POST の送り先パスをサブドメインごとに集計し、コードの調査結果と食い違いがないかを照合する
結論は「管理画面以外のホストに来る multipart を Block するなら、利用者への影響はない」でした。
除外条件は、すり抜けられる前提で作る
「Host が管理画面なら除外」という条件は、Host ヘッダーを偽装すればすり抜けられます。そこで、偽装された場合に何が起きるかを 2 段で確かめました。
- 管理画面で PHP に届く API の経路は、nginx の IP 制限(
allow+deny all)の内側にある。入れ子の location まで確認した - 接続元 IP は、ALB が付け足した X-Forwarded-For の値で判定している(クライアントが自分で書いた値は信じない設定)。なので IP の偽装も効かない
もう 1 つ、Web ACL は ALB ごとに分けました。1 つの ACL で全サイトの管理画面を除外すると、別サイトの ALB に「他サイトの管理画面の Host」を付けて送るだけで除外をすり抜けます。すり抜けた先は、その ALB の default server(PHP)です。
ルール 1 の考え方を JSON で書くと、次のようになります(ドメインは仮のもの)。
{
"Name": "block-multipart-except-admin",
"Priority": 0,
"Action": { "Block": {} },
"Statement": {
"AndStatement": {
"Statements": [
{
"ByteMatchStatement": {
"SearchString": "multipart",
"FieldToMatch": { "SingleHeader": { "Name": "content-type" } },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "CONTAINS"
}
},
{
"NotStatement": {
"Statement": {
"ByteMatchStatement": {
"SearchString": "admin.site-a.example.com",
"FieldToMatch": { "SingleHeader": { "Name": "host" } },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "EXACTLY"
}
}
}
}
]
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "block-multipart-except-admin"
}
}
このほかに AWS マネージドルール(CommonRuleSet / KnownBadInputs)も、まずは Count(記録のみ) で入れました。ログは CloudWatch Logs に送り、Count か Block になったものだけを 30 日間残します。
最初は全ルールを Count で入れ、誤検知がないのを見てから Block に切り替える段取りにしました。
23:00 公式告知:7.4 に「完全な暫定策はない」
23 時に PHP の公式告知が出ました。暫定策として確定したのは session.upload_progress.enabled=0 です。ただし告知には「完全な暫定策はなく、更新が唯一の安全な選択」とも書かれていました。修正版の対象外の 7.4 にとっては、暫定策を積み重ねるしかありません。
設定は php.ini 本体を触らず、ファイルを 1 つ足す形にしました。切り戻しは、このファイルを消して reload するだけです。
; /etc/php.d/99-php2shell-mitigation.ini
; CVE-2026-109618 / CVE-2026-109619 の暫定策
session.upload_progress.enabled = 0
reload で 502 を出さない
反映には php-fpm の reload が要ります。ところがこの環境は process_control_timeout が既定値の 0 のままでした。この設定だと reload の瞬間に、処理中のリクエストが 502 になります。
そこで ALB から 1 台ずつ外して反映しました。
1 台分の手順をシェルで書くと、次のようになります。
TG_ARN="arn:aws:elasticloadbalancing:...:targetgroup/site-a/..."
ID="i-0123456789abcdef0"
aws elbv2 deregister-targets --target-group-arn "$TG_ARN" --targets Id="$ID"
aws elbv2 wait target-deregistered --target-group-arn "$TG_ARN" --targets Id="$ID"
# 対象台の上で(SSM Run Command 経由)
# cp 99-php2shell-mitigation.ini /etc/php.d/
# systemctl reload php-fpm
# php-fpm -i | grep session.upload_progress.enabled # => Off
aws elbv2 register-targets --target-group-arn "$TG_ARN" --targets Id="$ID"
aws elbv2 wait target-in-service --target-group-arn "$TG_ARN" --targets Id="$ID"
作業中は Web サイトに 1 秒おきにリクエストを送り続け、すべて 200 のままでした。本番と検証環境の十数台を、約 10 分で終えています。
23:37 最初の攻撃:止めたのは WAF ではなかった
WAF を Count で入れた直後から、ログに次の記録が残り始めました。
| 時刻 | 送信元 | 内容 | 結果 |
|---|---|---|---|
| 23:37 | 英国の VPS |
POST https://example.com/(apex ドメイン)に multipart。サイズは 8,828 B と 25,212 B |
WAF は Count 中。ALB の「apex → www」301 リダイレクトで止まった |
| 23:37 | 同上 | 301 を追って GET https://www.example.com/(本文なし) |
GET なので攻撃データは運ばれない |
最初の攻撃を止めたのは、偶然にも ALB のリダイレクトのルールでした。攻撃ツールは apex ドメインを狙っていて、しかも 301 を追うときに POST を GET に変えていました。そのため攻撃データが PHP まで届いていません。
防御のつもりで入れたルールではなかったはずの 301 が、この夜の最初の防衛線になりました。とはいえ運が良かっただけで、www を直接狙われていたら Count の WAF は素通しでした。急いで Block に切り替えます。
Block に切り替えたら、何も止まらなくなった
Count から Block への切り替えでは、get-web-acl で取り出した Rules の Action だけを書き換え、そのまま update-web-acl に戻しました。エラーは出ません。ところが、動作確認のリクエストが 403 になりませんでした。
動作確認には、Content-Type だけ multipart にした、本文のない GET を静的ファイルに送っています。PHP は GET の本文を multipart として解析しないので、本番に送っても無害です。
curl -s -o /dev/null -w '%{http_code}\n' \
-H 'Content-Type: multipart/form-data; boundary=x' \
https://www.example.com/robots.txt
# 期待値: 403 実際: 200
原因は文字列の扱いでした。
-
get-web-aclが返すSearchStringは base64 になっている - それを
--cli-binary-format raw-in-base64-out付きで戻すと、base64 の文字列そのものが検索文字列として登録される - 結果、
multipartではなくbXVsdGlwYXJ0を探すルールになっていた
# 登録されている検索文字列をデコードして確かめる
aws wafv2 get-web-acl --scope REGIONAL --name site-a-acl --id "$ACL_ID" \
| jq -r '.WebACL.Rules[0].Statement.AndStatement.Statements[0]
.ByteMatchStatement.SearchString' \
| base64 -d
# 正しければ multipart、二重になっていれば bXVsdGlwYXJ0
その間 WAF は、bXVsdGlwYXJ0 という Content-Type が来るのを真面目に待ち続けていました。そんなリクエストは来ません。
手元にあった平文の定義から入れ直し、デコードした値と curl の 403 の両方を確かめました。エラーが出ないまま効いていない、が一番怖いと実感した場面です。
23:51〜 5 つの IP から、同じ攻撃が来た
Block に切り替えたあと、同じ 2 種類のサイズの POST が次々に来て、すべて Block されました。
| 時刻 | 送信元 | 内容 | 結果 |
|---|---|---|---|
| 23:51〜 | フランス・アメリカ・ドイツ・ポーランドの VPS(5 IP) | 8,828 B と 25,212 B の multipart POST | WAF が Block |
- User-Agent はすべて
Python aiohttp。攻撃する側も非同期 I/O で効率化しているようです - 同じツールから同じサイズのデータが、複数の国の IP から順に届いていた。複数の拠点から同時にスキャンしているとみられる
- 送信元の 1 つは、数時間前に
GET /を 1 件だけ送っていた。下見とみられる
WAF のログには送信データの中身が残りません。なのでここでは「今回の脆弱性を狙ったとみられる通信」と書いています。
入られていないかを確かめる
そのうえで、サーバー側に被害がないかを読み取り専用で確かめました。
| 確認項目 | なぜ見るか | 結果 |
|---|---|---|
| php-fpm の worker の異常終了(signal 11 など) | メモリ破壊型の攻撃で出やすい | 10 月は月初の 1 件のみ |
| メモリ上限・実行タイムアウトのエラー | 一方の CVE の発動経路 | 10 月は 0 件 |
| php-fpm 配下の子プロセス | シェルが起動されていないか | なし |
| 直近で変更された PHP ファイル・実行ファイル | Web シェルが置かれていないか | 自分たちのリリース作業の分だけ |
| cron の変更・外向き通信 | 居座り・外部への通信 | なし |
確認した範囲では、侵入や改ざんの痕跡はありませんでした。
冒頭で書いた「1 本の POST で開く入口」の、その 1 本目を WAF で止めていたことになります。WAF がなければ、5 IP の POST はそのまま php-fpm に届いていました。
ここでも 1 つ罠を踏みかけています。php-fpm のログを日別に数えるとき、21-Oct-2026 形式の日付を文字列のまま並べ替えると、月の順になりません。tail で切ると直近の 10 月が落ちます。
# NG: 日付が文字列順に並び、tail で 10 月の行が落ちることがある
grep 'signal 11' /var/log/php-fpm/error.log | awk '{print $1}' | tr -d '[' \
| sort | uniq -c | tail
# OK: 見たい月で絞ってから数える
grep 'signal 11' /var/log/php-fpm/error.log | grep 'Oct-2026' \
| awk '{print $1}' | tr -d '[' | sort | uniq -c
報告を書く前に気付いて、数え直しました。
日付が変わるころ:Remi に 7.4 の更新が出ていた
PHP 7.4 を RPM で入れている環境の多くは、Remi のリポジトリ(rpms.remirepo.net)を使っています。php.net の告知は、このリポジトリについてこう書いていました。
If you rely on the volunteer-maintained
rpms.remirepo.net, we urge you to switch to distribution packages or build from source, so you can immediately apply updates. The repo will not have updates today.
(PHP 2026 News Archive)
「今日は更新が出ない」はずでした。ところが日付が変わるころにリポジトリを直接見ると、7.4.33-31 が 10/21 付けで上がっていました。ボランティアの方の「今日」は、告知よりも少し長かったようです。RPM の変更履歴には、今回の CVE が書かれています。
2026-10-21 Remi Collet <remi@remirepo.net> - 7.4.33-31
- Mitigation fix Avoid state corruption via bailout
CVE-2026-109618, CVE-2026-109619
| 置き場所 | パッケージ |
|---|---|
| EL8 / EL9 / EL10 の php74 モジュール |
php-* 7.4.33-31 |
| EL8 / EL9 の safe(SCL 形式) |
php74-php-* 7.4.33-31 |
この時点では Remi のブログに告知記事はまだありませんでした。告知を待っていると気付けません。Remi を使っているなら、ブログではなくリポジトリと変更履歴を見るほうが早いです。
# 更新(モジュール形式の場合。SCL なら 'php74-php*')
sudo dnf update 'php*'
# 入ったかを変更履歴で確かめる
rpm -q --changelog php-common | head -5
# CVE-2026-109618, CVE-2026-109619 が出れば入っている
注意点が 2 つあります。
- 変更履歴の書き方は「Mitigation fix(緩和策)」です。本家 8.x の修正と同じかどうかは、この記述からは判断できません
- ASG で管理しているなら、手で更新した内容は次に起動する台には反映されません。起動テンプレート(AMI)まで直して完了です。夜に入れた ini も同じです
罠をもう 2 つ
マネージドルールをいきなり Block にすると、アプリが止まる
CommonRuleSet の NoUserAgent_Header が、ネイティブアプリと WebSocket の正常な通信に大量に一致していました。どちらも User-Agent を付けずに通信するためです。SizeRestrictions_Body は管理画面の CSV 取込に一致していました。
「マネージドルールを入れて、すぐ Block」にしていたら、アプリが全部止まっていました。Count で入れてログを見る段取りに救われています。
CloudWatch メトリクスの Rule 次元はルール名ではない
WAF の BlockedRequests を Rule 次元で取るときに指定するのは、ルール名ではなく VisibilityConfig.MetricName です。ルール名を渡すと 0 件が返ります。「Block されていない」と読み違えかけました。
翌日以降:監視を Skill にする
「今夜入れて終わり」にしないために、WAF ログの定期確認を Claude Code の Skill にしました。読み取り専用で、次を確認します。
- 設定が崩れていないか:ルールの動作が意図したままか、検索文字列をデコードすると
multipartか(二重 base64 の再発検知) - ルール 1 の Block 件数
- Block した通信の送信元(IP・国・User-Agent・サイズ)
- マネージドルールの検知(アプリの正常な通信に当たるものは除く)
初回の実行で、上の「5 IP からの Block」が見つかっています。
まとめ:同じ夜を迎えたときのチェックリスト
- 影響はバージョンだけで判定せず、「バージョン」「成立条件」「外から届くか」の 3 軸で見る
- 調査は読み取り専用のコマンドで、サーバー名で外さず全台に流す
- 入口で止める前に、何が壊れるかをコードとアクセスログの両方から調べる
- 除外条件は Host 偽装・ALB をまたいだ偽装を前提に作る(ACL は ALB ごと)
- WAF は Count で入れ、ログを見てから Block に切り替える
- 切り替えたら、curl で実際に 403 になるかと、検索文字列のデコード結果を確かめる
- php-fpm の reload は ALB から 1 台ずつ外して行い、確認リクエストを送り続ける
- 侵害の確認は、異常終了・子プロセス・変更ファイル・cron・外向き通信まで見る
-
Remi を使っているなら、ブログでなくリポジトリと
rpm -q --changelogで更新を確かめる - ASG 環境では AMI / 起動テンプレートへの反映までやって完了にする
すぐにバージョンを上げられない方向けに、php.net 公式の暫定対策 7 つと、それぞれが防げない範囲を kenimoto.dev にまとめました。この記事で使った ini と WAF 以外の策(memory_limit、post_max_size、cgroup の上限など)も入っています。
総理の投稿にあったとおり、サイトを止めるのも有効な策です。ただ、止めなくても守れる手はありました。WAF も ini も緩和策でしかなく、根本の対策は 8.x への移行です(2022 年のサポート終了から、ずっと言われ続けている話ではありますが)。7.4 のまま今回をしのいだ方は、次の夜までにどこまで移行できそうですか。
参考
- PHP 2026 News Archive(php.net)
- PHP脆弱性php2shell、更新前の対策7つ(kenimoto.dev)
- PHP2Shell: Critical PHP RCE(Sansec)
- 高市早苗 on X(2026-10-21 21:55)
- rpms.remirepo.net enterprise/9/php74
- Remi's RPM repository - Blog
- ニコニコやpixivなど各種ウェブサイトおよびアプリがPHPの緊急パッチに対応するべくメンテナンス入り(GIGAZINE)
- 『ウマ娘』『グラブル』『シャドバWB』などが本日(10/21)23時より緊急メンテ(ファミ通)

