← ブログに戻る

PHP脆弱性php2shell、更新前の対策7つ

2026 年 10 月 21 日、PHP に任意コード実行の脆弱性が 2 件公表されました。通称 php2shell(CVE-2026-109618 / CVE-2026-109619、CVSS v4.0 9.2)です。

本当の対策は、修正版への更新です。ただ、検証や移行の都合ですぐには上げられない環境もあります。修正版が出ない PHP 7.4 で動いている本番なら、なおさらです。

この記事は、更新するまでの間にやっておける暫定対策を、php.net の告知を一次資料にして 7 つにまとめたものです。それぞれ「何を防ぎ、何を防げないか」も並べます。

仮設のバリケードを積む作業員の奥で、本設の扉が足場付きで組み立て中。暫定対策は本番の修理までのつなぎ

php2shell で何が起きるのか

php.net によると、影響を受けるのは「Web リクエストを処理するすべての PHP」です。PHP-FPM、Apache の mod_php、php-cgi、FrankenPHP、LiteSpeed が挙がっています。最もリスクが高いのは mod_php で、アプリのコードが動く前に、未認証のリクエスト 1 本で攻撃が成立する実証があるとされています。

2 件の中身は次のとおりです。

CVE中身(php.net の説明を要約)
CVE-2026-109618メモリ不足の処理が、リクエストの内部データを初期化しきる前の PHP を中断させる。後片付けがその不正なデータを正しいものとして扱ってしまう
CVE-2026-109619リクエストが持っていた内部状態が、メモリを解放した後も残る。同じワーカーが次のリクエストを処理するとき、初期化より先にその古い状態を使ってしまう

Sansec の分析では、攻撃者が得るものは 2 つです。ログイン不要のコード実行と、Heartbleed のようなメモリの中身の漏えいです。後者では、他の利用者のリクエストに含まれるパスワード・セッショントークン・API キーが漏れうるとされています。Sansec は「既知の発動経路はすべて multipart/form-data のリクエスト」とも書いています。

公式の結論:完全な緩和策はない

先に結論を書いておきます。php.net の告知はこう言い切っています。

There are no complete mitigations, updating is the only safe choice.

以下の 7 つは、更新までの時間を稼ぐためのものです。全部入れても「安全になった」とは言えません。

修正版の状況は次のとおりです。

系統状況
8.2 / 8.3 / 8.4 / 8.5修正版あり(8.2.35 / 8.3.36 / 8.4.27 / 8.5.12)
7.4 / 8.0 / 8.1PHP プロジェクトとしての更新は終了。ソースに当てる最小限のバックポートのみ公開

更新前にやっておける暫定対策 7 つ

1. session.upload_progress.enabled を 0 にする(CVE-2026-109619)

php.net が CVE-2026-109619 の「確認済みのセッション経由の経路」を防ぐ策として挙げているものです。

; /etc/php.d/99-php2shell-mitigation.ini
session.upload_progress.enabled = 0

FPM のプール設定で入れるなら次のように書きます。

; /etc/php-fpm.d/www.conf
php_admin_flag[session.upload_progress.enabled] = off

注意点が 2 つあります。

  • ini_set() や FPM の .user.ini では効きません。 リクエストの解析より前に適用される php.ini・プール設定・サーバー設定で入れます
  • 全ワーカーの再起動が要ります

防げるのは確認済みの経路だけで、php.net も「他の変種は防げない」と書いています。

2. memory_limit を -1 にする(CVE-2026-109618)

CVE-2026-109618 は、PHP のメモリ上限に達したときの処理が入口です。php.net は、上限そのものを外す設定を挙げています。アプリから上書きされない形で入れます。

; FPM プール
php_admin_value[memory_limit] = -1

ほかのプール・ディレクトリ・アプリの設定が、有限の値を入れ直していないかも確かめます。それでも、PHP の外側でのメモリ確保の失敗は脆弱な経路に届きうる、と php.net は注記しています。

上限を外すと、1 本のリクエストがメモリを使い切る危険が出ます。対策 4 とセットで入れてください。

3. post_max_size を下げ、入口でサイズを絞る(CVE-2026-109618)

php.net の推奨値は次のとおりです。

アプリpost_max_size
ファイルアップロードが要らない8M
アップロードが要る32M

upload_max_filesize は post_max_size より小さくします。

php.net はさらに、Web サーバーやプロキシの段階で、デコード後のサイズ・入力フィールド数・multipart のパート数・ヘッダー数・入れ子の深さを制限することも勧めています。Content-Length が使えないリクエスト(チャンク転送など)も対象に含めます。nginx なら、少なくともボディ全体の上限は client_max_body_size で絞れます。

4. コンテナや cgroup でメモリの上限をかける

対策 2 で PHP の上限を外した分、外側に「可用性の壁」を置きます。php.net は、問題を起こしたワーカーだけを止めるポリシーを勧めています。

systemd で php-fpm を動かしているなら、たとえば次のようにします。

# systemctl edit php-fpm
[Service]
MemoryMax=4G
OOMPolicy=continue

OOMPolicy を書かないと、ワーカーが 1 つ OOM で止められただけでサービス全体が止まる設定になりえます(systemd の既定値は stop)。

RLIMIT_AS や RLIMIT_DATA で代用してはいけません。 php.net は、これらが脆弱なメモリ確保の失敗そのものを引き起こしうると書いています。

5. 入口で multipart を止める

Sansec によると、既知の発動経路はすべて multipart/form-data です。ファイルアップロードを受けない画面にまで multipart を通す理由はありません。WAF やリバースプロキシで止めると、PHP に届く前に経路を減らせます。

検問所で警備員が段ボールを抱えた配達員を止め、封筒1通だけの配達員は隣のレーンを通っていく

AWS WAF なら、「Content-Type に multipart を含み、かつアップロードが要るホストではない」リクエストを Block します。

{
  "Name": "block-multipart-except-upload-host",
  "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.example.com",
            "FieldToMatch": { "SingleHeader": { "Name": "host" } },
            "TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
            "PositionalConstraint": "EXACTLY" } } } }
      ]
    }
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "block-multipart-except-upload-host"
  }
}

入れる前に確かめることが 3 つあります。

  • 正規に multipart を使う機能の洗い出し。 ソースの FormData / $_FILES と、アクセスログの POST 先を両方見ます
  • 除外条件のすり抜け。 Host ヘッダーは偽装できます。除外したホストの PHP 経路が IP 制限などで守られているかを確かめます。複数サイトで 1 つの Web ACL を共有すると、別サイトの ALB に「除外したホスト名」を付けて送るだけですり抜けます。ACL は ALB ごとに分けます
  • 効いているかの確認。 Block にしたら、本文のない GET で Content-Type だけ multipart にして、静的ファイルに送ります。PHP は GET の本文を解析しないので本番でも無害です
curl -s -o /dev/null -w '%{http_code}\n' \
  -H 'Content-Type: multipart/form-data; boundary=x' \
  https://www.example.com/robots.txt
# 403 になれば効いている

除外したホストの multipart は通るので、そこは対策 1〜4 で守ることになります。

6. PHP 8.5 の追加設定と、診断メッセージの出どころをなくす(CVE-2026-109619)

php.net は CVE-2026-109619 について、ほかにも次を挙げています。

  • PHP 8.5 なら fatal_error_backtraces=0 にして全ワーカーを再起動する。確認済みの「致命的エラーのバックトレース経路」だけを防ぐ
  • リクエストごとに適用される、無効な設定や非推奨の設定をなくす。 初期化より前に診断メッセージを出すものが対象。error_reporting、display_errors、@ で表示を隠しても効かない
  • 攻撃者が起こせる致命的エラーや実行タイムアウトを直す。 コールバック、アップロードやインポートの処理、後片付けでの失敗も含む

7. PHP 7.4 / 8.0 / 8.1 は、バックポートか Remi の更新を入れる

修正版の対象外の系統には、php.net がソースに当てる最小限のバックポートを公開しています(7.4 / 8.0 / 8.1)。自前でビルドしている環境向けです。

RPM の PHP 7.4 なら、Remi のリポジトリ(rpms.remirepo.net)を使っている環境が多いはずです。php.net の告知は「Remi は今日は更新が出ない」と書いていましたが、実際には 7.4.33-31 が 10/21 付けで公開されていました。

# モジュール形式の場合(SCL なら 'php74-php*')
sudo dnf update 'php*'

# 入ったかを変更履歴で確かめる
rpm -q --changelog php-common | head -5

変更履歴に次の行があれば入っています。

- Mitigation fix Avoid state corruption via bailout
  CVE-2026-109618, CVE-2026-109619

書き方は「Mitigation fix(緩和策)」です。8.x の修正版と同じ中身かどうかは、この記述からは判断できません。

反映は 1 台ずつ、無停止で

ini の変更には php-fpm の reload が要ります。process_control_timeout が既定値の 0 のままだと、reload の瞬間に処理中のリクエストが 502 になります。

ロードバランサーの配下なら、1 台ずつ外して反映します。

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"

# 対象台で: 設定を置いて reload、値を確認
#   systemctl reload php-fpm
#   php-fpm -i | grep session.upload_progress.enabled

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"

php-fpm -i で見えるのは php.ini 側の値です。プール設定(php_admin_value)で入れた値は php-fpm -tt の出力で確かめます。

Auto Scaling で台が入れ替わる環境では、手で入れた設定は新しい台に入りません。起動テンプレートや AMI まで直して完了です。

私の本番で確かめたこと

当夜、私が保守している PHP 7.4 の本番では、対策 1(session.upload_progress.enabled=0)と対策 5(multipart の Block)を入れ、1 台ずつ無停止で反映しました。

WAF を入れた直後には、実際に multipart の POST が届いています。5 か国・6 つの IP から、同じ 2 種類のサイズのデータでした。User-Agent はすべて Python の aiohttp です。

届いた攻撃結果
最初の 1 本(apex ドメイン宛て)apex → www の 301 リダイレクトで POST が GET に変わり、データが届かなかった
その後の 5 IPWAF の multipart ルールで Block

赤い小包を抱えたロボットが矢印の標識で回り道をさせられ、小包を落としている。301 で POST が GET に変わり、攻撃データが届かなかった

確認した範囲では、侵入の痕跡はありませんでした。詳しい時系列は Qiita の記録にあります。

資料と実際がズレたところ

ここは手順書には出てこない、踏んでから分かった話です。

ズレ実際
php.net「Remi は今日は更新が出ない」同じ日のうちに 7.4.33-31 が出ていた。Remi のブログに告知が出る前に、リポジトリには上がっていた
WAF を Count から Block に切り替えたら止まるはずget-web-acl の出力をそのまま update-web-acl に戻すと、検索文字列が二重に base64 化され、multipart ではなく bXVsdGlwYXJ0 を探すルールになった。エラーは出ない
マネージドルールも入れれば安心CommonRuleSet の NoUserAgent_Header が、ネイティブアプリや WebSocket の正常な通信に大量に一致した。いきなり Block にするとアプリが止まる

WAF のルールを変えたら、検索文字列をデコードして確かめ、curl で 403 になるかまで見ることを勧めます。

aws wafv2 get-web-acl --scope REGIONAL --name "$ACL_NAME" --id "$ACL_ID" \
  | jq -r '.WebACL.Rules[0].Statement.AndStatement.Statements[0]
           .ByteMatchStatement.SearchString' | base64 -d
# multipart と出れば正しい

防げないものと、入られていないかの確認

7 つを入れても残るものを並べます。

残るもの理由
確認済み以外の変種対策 1・6 は php.net 自身が「確認済みの経路だけ」と書いている
正規に multipart を受ける画面対策 5 で除外したホストには届く
PHP の外側のメモリ確保の失敗対策 2 でも脆弱な経路に届きうる
すでに入られていた場合暫定策は入口をふさぐだけ。仕込まれた Web シェルや cron は残る

最後の行のために、入られていないかを確かめます。

懐中電灯とクリップボードを手に、サーバーラックの裏を一つずつ照らして点検するエンジニア

  • php-fpm のワーカーの異常終了(signal 11 など)が増えていないか
  • メモリ上限・実行タイムアウトのエラーが出ていないか
  • php-fpm の子プロセスにシェルなどが起動していないか
  • 直近で変更された PHP ファイル・実行ファイルが、自分のリリース作業の分だけか
  • cron の変更や、見慣れない外向き通信がないか

ログを日別に数えるときは、21-Oct-2026 形式の日付を文字列のまま並べ替えないでください。月の順にならず、tail で直近の月が落ちます。見たい月で grep してから数えます。

更新できたら、暫定策は外して構いません。外す前に、修正版が入ったことをバージョンか変更履歴で確かめてください。

References