対策・学習20分で読める

PHPの緊急更新で見落としやすい点:WebとコマンドラインのPHPは別、set -e と diff の終了コード、更新後の侵入確認

PHPの緊急更新では、Web用とコマンドライン(CLI)用でPHPの版が違うことがあります。確かめ方、set -e と diff の終了コード、更新後の侵入確認の流れを実例から整理します。

本文を読む
公開日
更新日
最終確認
目次

対象:PHPで動くサイトを運営していて、緊急のPHP更新をするとき、またはしたあとに確認の漏れが心配な人。

2026年10月、PHPの重大な脆弱性2件(CVE-2026-109618・CVE-2026-109619)の修正版が出ました。当サイトの運営者が関わるサイトでも、その夜に更新しました。このページは、そのときに実際にあった「最初の思い込みが外れた2つの点」と、更新後に全サーバーへ同じ項目を当てた侵入確認を、一般の手順として整理したものです。脆弱性の内容と修正版は PHPの重大な脆弱性2件と緊急の修正版(2026年10月) にまとめています。

何があったか(当サイトの運営者が関わるサイトでの出来事)

このサイトは、複数の環境でPHPを使っています。PHPの開発チームが修正版の公開日時を予告していたため、運営者は公開の前にメンテナンス表示へ切り替え、修正版が出るまで公開を止めました。修正版が出たあとに差し替え、メンテナンス表示を解いて戻しました。

  1. 予告を受けて

    サイトをメンテナンス表示にして、修正版を待つ
  2. 修正版の公開後

    修正版に差し替え、メンテナンス表示を解除する
  3. 更新後の確認

    Web側は更新済みだったが、cronが使うコマンドライン用のPHPは古い版を指したままだったので、向き先を直した
  4. 同じ夜

    メンテナンス表示を解く一時スクリプトが、書き込みの前に止まった。原因は set -e と diff の組み合わせ。被害はなく、スクリプトを作り直した
  5. その後

    全サーバーに同じ侵入確認の項目を当て、更新前に届いていた疑わしい1件のアクセスを、ファイルの比較で確かめた

その後、米CISAはこの2件を、実際に悪用された脆弱性の一覧(KEV)に加えました(CISAの発表)。急いで更新した判断は裏づけられましたが、更新は入口を閉じるだけです。すでに入られていないかの確認は、別に必要です。

思い込み1:「WebのPHPを更新したから終わり」

最初の思い込みは、サイトが新しい版で動いていれば、そのサーバーのPHPは全部更新済みだ、というものでした。実際には、cronから呼ばれるコマンドライン用のPHPの向き先が、古い版のままでした。

PHPは、動かし方(SAPI:Server API)によって別のプログラムになることがあります。php.net は、CLIはほかのSAPIと違う点が多いと説明しています(PHP: コマンドラインからの利用)。1台に複数の版を入れていると、Webは新しい版、コマンドラインは古い版、という組み合わせが起こります。

Webのリクエスト → PHP-FPM / Apacheのモジュール

更新後の版で動いている(サイトヘルスなどで確認できる)

cron・シェルのスクリプト → コマンドライン用のPHP

向き先が古い版のまま。Webの確認では見えない

同じサーバーでも、Webが使うPHPとコマンドラインが使うPHPは、別のプログラム・別の版のことがある。更新後はどちらも確かめる。

どのPHPが動いているかを、WebとCLIで別々に確かめる

1

コマンドラインのPHPの版を見る

php -v で、そのシェルから呼ばれるPHPの版が分かります。php.net は、php -v でCGI版かCLI版かも分かると説明しています。which php で、実際に呼ばれる実行ファイルの場所を確かめます。

2

Debian系で版を切り替えているなら、向き先を見る

update-alternatives --display php で、php というコマンドがどの版を指しているかが表示されます(update-alternativesのマニュアル)。切り替えは --config で行いますが、向き先を変えるのはサーバーの設定変更なので、手順と戻し方を確かめてから行ってください。

3

cronのジョブは、PHPをフルパスで書く

cronは、ログインシェルとは別の環境でジョブを動かします(crontab(5))。php とだけ書くと、どのPHPが選ばれるかが環境に左右されます。当サイトの考えでは、更新の対象にした版のPHPをフルパスで書いておくと、更新後の確認が一意になります。

4

Webが使うPHPの版は、Web側から確かめる

WordPressなら管理画面の「サイトヘルス」の情報で確かめられます。コードで確かめるなら、PHPの定数 PHP_VERSION(版)と PHP_SAPI(実行方式。CLIなら cli、PHP-FPMなら fpm-fcgi、Apacheのモジュールなら apache2handler)が使えます(php_sapi_name)。

5

phpinfo() のページは、確認後すぐに削除する

phpinfo() を書いたファイルを公開ディレクトリに置くと版は分かりますが、設定やサーバーの内部情報が全部見えます。確認が終わったら、その場で削除してください。置いたままにしないことが、この方法を使う条件です。

思い込み2:「diff の結果は、成功か失敗かのどちらか」

メンテナンス表示を解除する一時スクリプトは、書き込みの前に、いまの内容と新しい内容の差を diff で見る作りでした。このスクリプトは set -e を付けていました。

diff は、差が見つかると終了コード1を返します。GNUのマニュアルでは、0は差なし、1は差あり、2は異常(trouble)です(GNU diffutils: diff)。差があるのは、そのスクリプトの想定どおりでした。しかし set -e(errexit)は、条件文の中などの例外を除き、0以外の終了コードでシェルを止めます。差があるだけでスクリプトが止まり、書き込みには進みませんでした。

結果として、メンテナンス表示を解く処理は何も書かないまま止まり、被害はありませんでした。止まる側に倒れたので安全でしたが、逆の組み合わせ(止まるべきところで止まらない)だと危険です。スクリプトは作り直しました。

終了コード1が「異常」ではないコマンド

コマンド終了コード1の意味2の意味出典
diff差がある異常GNU diffutils: diff
cmp差がある異常GNU diffutils: cmp
grep選ばれた行がない(一致なし)エラーGNU grep

安全な書き方

GNU Bashのマニュアルは、set -e でも、if の条件の一部、&& や || の列の最後以外のコマンド、! で反転したコマンドなどが0以外を返してもシェルは終了しない、と説明しています(GNU Bash: The Set Builtin)。これを使います。

止まる、または異常を隠す書き方

  • set -e のもとで diff a b だけを書く(差があるだけで止まる)
  • diff a b || true(差ありも異常(2)も区別せず、握りつぶす)
  • grep を || true で受けて、一致なしと読み取りエラーを同じ扱いにする

終了コードを区別して受ける

  • 終了コードを変数に受けて、0・1・2を分けて分岐する(こちらを勧める)
  • 差の有無だけでよいなら if diff -q a b; then … でもよいが、2(異常)も「差あり」と同じ側に入る点に注意する
  • || true を使うなら、2(異常)を見逃してよいかを考えたうえで使う

0・1・2を分ける例です。|| の左側の失敗は errexit の対象外なので、スクリプトは止まらず、rc に終了コードが入ります。

#!/usr/bin/env bash
set -euo pipefail
 
rc=0
diff -rq deployed/ live/ || rc=$?
 
case "$rc" in
  0) echo "差なし" ;;
  1) echo "差あり: 内容を確認してから進める" ;;
  *) echo "diff が異常終了" >&2; exit 2 ;;
esac

set -e は、完全な安全装置ではありません。例外が多く、使い方によっては見落としが起こるため、重要な分岐は、終了コードを自分で見て判断するほうが読み間違いません(当サイトの考えです)。

更新後の侵入確認:全サーバーに同じ項目を当てる

更新は入口を閉じる作業で、更新前に入られていないかを調べる作業ではありません。KEVの対応では、米CISAが更新の前の侵入の確認を求めています(詳しくは PHPの重大な脆弱性2件と緊急の修正版 の「悪用の確認と米CISAのKEVへの追加」)。当サイトの運営者は、サーバーごとに調べる項目がばらつかないよう、全サーバーに同じチェックリストを当てました。

項目見るものコマンドの例
ログイン用の鍵登録した覚えのない公開鍵がないかauthorized_keys を開く
ユーザー知らないユーザー、管理者権限のグループの増減getent passwd、getent group sudo
パッケージ管理下のファイルの改変パッケージが入れたファイルが、配布時から変わっていないかdebsums -s(Debian系)、rpm -Va(RHEL系)
パッケージに属さない実行ファイル/usr/local、/opt、PATH上に、どのパッケージにも属さない実行ファイルが増えていないかdpkg -S <ファイル>(Debian系)、rpm -qf <ファイル>(RHEL系)で所有パッケージが出ないものを調べる
SUID実行した人でなく所有者の権限で動くファイルが増えていないかfind / -xdev -perm -4000 -type f
cronとsystemd知らない定期実行、知らないタイマーcrontab -l、/etc/crontab、/etc/cron.d、systemctl list-timers
/tmp実行できるファイルが置かれていないかfind /tmp /var/tmp -type f -perm /111
外向きの通信見覚えのない接続先への通信ss -tunp
コンテナの中動いているプロセス、起動後に書き込まれたファイルdocker top、docker diff

crontab -l が表示するのは、実行したユーザー自身のジョブだけです。/etc/crontab、/etc/cron.d、ユーザーごとのcronの保管場所は別に確かめます。ss -tunp は、すべてのプロセス名を見るには管理者権限(root)が必要です。dpkg -S と rpm -qf は、ファイルがどのパッケージのものかを調べる機能で、「所有パッケージなし」と出ても、自分で置いたファイルである可能性があるため、置いた覚えがあるかを照らし合わせます。

find の -perm -4000 は、指定したビットがすべて立っているファイルを探します(find(1))。docker diff は、コンテナを作ってから変わったファイル(A:追加、C:変更、D:削除)を表示し、docker top は、動いているプロセスを表示します(docker container diff、docker container top)。コンテナは普通、作り直して使うものなので、docker diff に出る実行ファイルは、想定外のものとして調べます(当サイトの考えです)。

各項目の見方と、レンタルサーバー・WordPress・VPSごとの手順は サイト・サーバーが不正アクセスされていないか確かめる方法 で解説しています。

疑わしいログは、ファイルの比較で決着させる

この確認の中で、更新の前に、問い合わせフォームへ、Tor(通信元を隠す仕組み)の出口ノードからのPOSTが届いていたことが見つかりました。ログに残っているのは、リクエストが届いた事実までで、その結果、何かが書き込まれたかどうかは分かりません。

そこで、公開中のファイルと、デプロイ時に手元に残したコピーを比べました。結果は完全に一致し、増えたPHPファイルもありませんでした。この確認では、ファイルが書き換えられた形跡は見つかりませんでした。

1

配布時のコピーを、サーバーの外に残しておく

比べる相手がサーバーの中にしかないと、同時に書き換えられても気づけません。デプロイのたびに、公開した状態をサーバーの外に残しておきます。

2

公開中のファイルと比べる

diff -rq で差のあるファイルを探すか、ファイルごとのハッシュ(sha256sum)を一覧にして比べます。差があれば、それが想定した変更かを確かめます。

3

増えたファイルも確認する

内容が一致していても、新しく置かれたPHPファイルは、元のコピーに無いので別に探す必要があります。diff -rq は、片側にしかないファイルも表示します。

4

一致しても、安全の証明にはならない

この方法で分かるのは、比べた範囲のファイルが変わっていないことだけです。データベース、アップロード用のフォルダ、サーバーの外側の変更は見えません。ほかの項目の確認と合わせて判断します(当サイトの考えです)。

当サイトの視点:緊急対応の夜こそ、確認の項目を固定しておく

急いでいる夜は、どのサーバーで何を見たかが、ばらつきやすくなります。項目の一覧を先に決めておくと、全部に同じものを当てられ、見ていないものが分かります。今回、古い版のコマンドラインのPHPを見つけられたのも、更新の確認をWebだけで終わらせなかったからです。

今日やること

  1. サーバーにログインできるなら、php -v と which php を実行し、cronのジョブにPHPのフルパスを書く
  2. Webが使うPHPの版は、サイトヘルスなど、Web側から確かめる(phpinfo() を使ったら、確認後すぐに削除する)
  3. set -e を使うスクリプトで diff・grep・cmp を呼んでいる箇所を探し、終了コード1の扱いを決める
  4. 公開したファイルのコピーをサーバーの外に残し、更新後に比べる
  5. 侵入確認の項目を全サーバーで同じにして、1つずつ当てる

出典

次に読む

よくある質問

QPHPを更新したのに、古い版のままのことがありますか?
A

あります。PHPは、Webのリクエストを処理するもの(PHP-FPMやApacheのモジュール)と、シェルやcronから使うコマンドライン用(CLI)が別のプログラムで、別の版が入っていることがあります。php.netは、実行方式をSAPIと呼び、CLIはほかのSAPIと違う点が多いと説明しています。Webの更新が済んでも、cronなどが使うPHPは古いままという状態が起こります。

Q実際に動いているPHPのバージョンは、どう確かめますか?
A

コマンドラインは php -v で確かめます。Webは、WordPressなら管理画面のサイトヘルスなど、Web側から確かめるのが安全です。phpinfo() のページを使うなら、確認後すぐに削除してください。どの実行ファイルが呼ばれるかは which php で分かり、Debian系で複数の版を切り替えているなら update-alternatives --display php で現在の向き先を確かめられます。cronのジョブは、PHPをフルパスで書くと、どのPHPが動くかが一意に決まります。

Qset -e を付けたシェルスクリプトで、diff を使うと止まるのはなぜですか?
A

diff は、差が見つかると終了コード1を返します。GNUのマニュアルでは、0は差なし、1は差あり、2は異常です。set -e(errexit)は0以外の終了コードでスクリプトを止めるため、差があるだけで止まります。if の条件に置くか、終了コードを変数に受けて0・1・2を分けて扱います。grep(1は一致なし)と cmp(1は差あり)も同じです。

Q疑わしいログがあったとき、侵入されたかどうかをどう判断しますか?
A

ログだけでは決められません。ログは届いた試みの記録で、成功の記録とは限りません。公開しているファイルと、配布時に手元に残したコピーを比べ、一致するか、PHPファイルが増えていないかを確かめます。一致すれば『ファイルの改ざんは見つからなかった』と言えますが、安全の証明ではないので、ほかの項目の確認も合わせて行います。