'8.5.12', '8.4' => '8.4.27', '8.3' => '8.3.36', '8.2' => '8.2.35', ); } function phpcheck_cve_ids() { return array('CVE-2026-109618', 'CVE-2026-109619'); } /* 版数だけから言えることを 3 段で返す。 state: fixed_or_newer / needs_changelog / no_fix / unknown */ function phpcheck_version_verdict($version) { if (!preg_match('/^(\d+)\.(\d+)\.(\d+)/', (string)$version, $m)) { return array('state' => 'unknown', 'branch' => null, 'fixed' => null, 'text' => '版数を読めませんでした。'); } $branch = $m[1] . '.' . $m[2]; $fixed = phpcheck_fixed_versions(); if (isset($fixed[$branch])) { if (version_compare($version, $fixed[$branch], '>=')) { return array('state' => 'fixed_or_newer', 'branch' => $branch, 'fixed' => $fixed[$branch], 'text' => '版数は修正版 ' . $fixed[$branch] . ' 以上です。'); } return array('state' => 'needs_changelog', 'branch' => $branch, 'fixed' => $fixed[$branch], 'text' => '版数は修正版 ' . $fixed[$branch] . ' より前です。ディストリのパッケージは修正を当てても版数を変えない(例: 8.2.33-1+deb12u3 のようにパッケージ番号だけ上がる)ので、下の changelog の確認で判断してください。'); } if (version_compare($version, '8.2.0', '>=')) { return array('state' => 'unknown', 'branch' => $branch, 'fixed' => null, 'text' => 'この系列の修正版をこのファイルは知りません(php.net の告知で確認してください)。'); } if (version_compare($version, '7.4.0', '>=')) { return array('state' => 'needs_changelog', 'branch' => $branch, 'fixed' => null, 'text' => $branch . ' 系は PHP 本体のサポートが終わっており公式の修正リリースはありません。ディストリ/LTS ベンダのバックポート(ソースパッチ)が当たっているかを changelog で確認してください。当たらないなら 8.2 以降への移行が必要です。'); } return array('state' => 'no_fix', 'branch' => $branch, 'fixed' => null, 'text' => $branch . ' 系には修正がありません(7.3 以前・PHP 5 も影響を受けます)。サポート中の PHP への移行、それまでは外部からの到達を止める判断が必要です。'); } /* ---------------------------------------------------------------- 観測 */ function phpcheck_sapi_info() { $sapi = php_sapi_name(); $server = isset($_SERVER['SERVER_SOFTWARE']) ? (string)$_SERVER['SERVER_SOFTWARE'] : ''; $map = array( 'apache2handler' => array('Apache mod_php', 'mod_php', '最も危険とされる実行形態です(認証なし・アプリ実行前に発火が実証)。修正を当てたら Apache の再起動が必要です。'), 'fpm-fcgi' => array('PHP-FPM', 'fpm', '修正を当てたら php-fpm の再起動(reload)が必要です。プールごとに別の PHP が動いている場合は、プールごとにこのファイルを置いて確認してください。'), 'cgi-fcgi' => array('CGI / FastCGI', 'cgi', 'レンタルサーバや LiteSpeed などで使われる形態です。提供元の告知で適用状況も確認してください。'), 'litespeed' => array('LiteSpeed (lsapi)', 'litespeed', '提供元の告知で適用状況も確認してください。'), 'cli-server' => array('PHP 組込サーバ (php -S)', 'cli-server', '開発用です。本番の実行形態ではありません。'), 'cli' => array('コマンドライン (CLI)', 'cli', 'CLI の PHP と Web の PHP は別のことがあります。本番の判定はブラウザから開いてください。'), 'frankenphp' => array('FrankenPHP', 'frankenphp', '修正を当てたらプロセスの再起動が必要です。'), ); $label = isset($map[$sapi]) ? $map[$sapi][0] : $sapi; $kind = isset($map[$sapi]) ? $map[$sapi][1] : 'other'; $note = isset($map[$sapi]) ? $map[$sapi][2] : ''; $is_web = !in_array($sapi, array('cli', 'phpdbg', 'embed'), true); return array('sapi' => $sapi, 'label' => $label, 'kind' => $kind, 'note' => $note, 'is_web' => $is_web, 'server_software' => $server); } function phpcheck_ini_info() { $keys = array( 'session.upload_progress.enabled' => '緩和策で無効化が案内された機能。無効でも「それだけでは不十分」と PHP 開発者が明言。', 'memory_limit' => '緩和策で -1 が案内されたが、更新後は元に戻す(1 リクエストで全メモリを使えてしまう)。', 'expose_php' => 'On だと応答ヘッダに PHP の版数が出ます。Off を推奨。', 'opcache.enable' => 'CVE-2026-109619 は OPcache の型混同を経由します(値は参考)。', 'display_errors' => '本番では Off が通例。', ); $out = array(); foreach ($keys as $k => $why) { $v = ini_get($k); $out[] = array('key' => $k, 'value' => ($v === false ? '(未定義)' : ($v === '' ? '(空)' : (string)$v)), 'why' => $why); } return $out; } /* Debian/Ubuntu 系: changelog に CVE が載っているか(読める場合のみ)。 戻り値: array(state => found/not_found/unreadable, file => path|null, lines => [...]) */ function phpcheck_changelog_info($version) { $branch = preg_match('/^(\d+\.\d+)/', (string)$version, $m) ? $m[1] : null; $candidates = array(); if ($branch !== null) { $candidates[] = '/usr/share/doc/php' . $branch . '-common/changelog.Debian.gz'; $candidates[] = '/usr/share/doc/php' . $branch . '-cli/changelog.Debian.gz'; } $candidates[] = '/usr/share/doc/php-common/changelog.Debian.gz'; $file = null; foreach ($candidates as $c) { if (@is_readable($c)) { $file = $c; break; } } if ($file === null) { return array('state' => 'unreadable', 'file' => null, 'lines' => array(), 'text' => 'このサーバからは Debian/Ubuntu のパッケージ changelog を読めませんでした(別の OS か、読み取り権限/open_basedir の制限)。下のコマンドを管理者権限で実行して確認してください。'); } if (!function_exists('gzopen')) { return array('state' => 'unreadable', 'file' => $file, 'lines' => array(), 'text' => 'changelog はありますが zlib が無く開けません。下のコマンドで確認してください。'); } $hits = array(); $fh = @gzopen($file, 'rb'); if (!$fh) { return array('state' => 'unreadable', 'file' => $file, 'lines' => array(), 'text' => 'changelog を開けませんでした。下のコマンドで確認してください。'); } $n = 0; while (!gzeof($fh) && $n < 20000) { $line = gzgets($fh, 4096); if ($line === false) break; $n++; foreach (phpcheck_cve_ids() as $cve) { if (strpos($line, $cve) !== false) { $hits[] = trim($line); break; } } if (count($hits) >= 5) break; } gzclose($fh); if ($hits) { return array('state' => 'found', 'file' => $file, 'lines' => $hits, 'text' => 'パッケージの changelog に CVE の記載があります。このパッケージには修正が含まれています(実行中のプロセスが更新後に再起動されていることも確認してください)。'); } return array('state' => 'not_found', 'file' => $file, 'lines' => array(), 'text' => '読んだ範囲の changelog(先頭 ' . $n . ' 行)に CVE の記載がありません。パッケージがまだ更新されていないか、別名で記載されています。apt の更新状況を確認してください。'); } /* Linux: このプロセスの起動時刻と PHP 本体(またはモジュール)の更新時刻を比べる。 戻り値: array(state => restarted/stale/unknown, proc_start => ts|null, binary => path|null, binary_mtime => ts|null) */ function phpcheck_restart_info($sapi) { $out = array('state' => 'unknown', 'proc_start' => null, 'binary' => null, 'binary_mtime' => null, 'text' => ''); $binary = null; if (defined('PHP_BINARY') && PHP_BINARY !== '' && @is_file(PHP_BINARY)) { $binary = PHP_BINARY; } elseif ($sapi === 'apache2handler') { foreach (array('/usr/lib/apache2/modules', '/etc/httpd/modules', '/usr/lib64/httpd/modules', '/usr/local/apache2/modules') as $dir) { $found = @glob($dir . '/libphp*.so'); if ($found) { $binary = $found[0]; break; } } } if ($binary !== null) { $out['binary'] = $binary; $mt = @filemtime($binary); if ($mt !== false) $out['binary_mtime'] = $mt; } $stat = @file_get_contents('/proc/self/stat'); $boot = @file_get_contents('/proc/stat'); if ($stat !== false && $boot !== false && preg_match('/btime (\d+)/', $boot, $bm)) { $rp = strrpos($stat, ')'); $fields = preg_split('/\s+/', trim(substr($stat, $rp + 2))); // after the comm field: field 22 (1-based) is starttime in clock ticks; here index 19 if (isset($fields[19]) && ctype_digit($fields[19])) { $hz = 100; if (function_exists('posix_times')) { /* clock ticks per second is 100 on Linux x86/arm in practice */ } $out['proc_start'] = (int)$bm[1] + (int)floor((int)$fields[19] / $hz); } } if ($out['proc_start'] !== null && $out['binary_mtime'] !== null) { if ($out['proc_start'] < $out['binary_mtime'] - 60) { $out['state'] = 'stale'; $out['text'] = 'このプロセスは PHP 本体が更新される前から動いています。更新後の再起動がまだです(Apache/php-fpm を再起動してください)。'; } else { $out['state'] = 'restarted'; $out['text'] = 'このプロセスは PHP 本体の更新より後に起動しています。'; } } elseif ($out['binary_mtime'] !== null) { $out['text'] = 'PHP 本体の更新時刻は読めましたが、プロセスの起動時刻は読めませんでした(/proc が無い OS か、制限)。再起動済みかは手で確認してください。'; } else { $out['text'] = 'PHP 本体の場所が分からず、再起動済みかは判定できません。手で確認してください。'; } return $out; } /* ---------------------------------------------------------------- 総合 */ function phpcheck_overall($verdict, $changelog, $restart) { // 3 段: ok(修正版以上 or changelog に CVE)/ check(確認事項あり)/ ng(修正なし or 未適用の根拠) $reasons = array(); if ($verdict['state'] === 'no_fix') { return array('level' => 'ng', 'title' => '修正がありません', 'reasons' => array($verdict['text'])); } $fixed = ($verdict['state'] === 'fixed_or_newer') || ($changelog['state'] === 'found'); if ($verdict['state'] === 'fixed_or_newer') $reasons[] = $verdict['text']; if ($changelog['state'] === 'found') $reasons[] = 'changelog に CVE の記載があります。'; if ($restart['state'] === 'stale') { $reasons[] = $restart['text']; return array('level' => 'ng', 'title' => '更新後の再起動がまだです', 'reasons' => $reasons); } if ($fixed) { if ($restart['state'] === 'unknown') $reasons[] = '再起動済みかはこのサーバからは判定できませんでした(手で確認)。'; return array('level' => 'ok', 'title' => '修正が当たっている根拠があります', 'reasons' => $reasons); } if ($verdict['state'] === 'needs_changelog' && $changelog['state'] === 'not_found') { return array('level' => 'ng', 'title' => '修正が当たっている根拠がありません', 'reasons' => array($verdict['text'], $changelog['text'])); } return array('level' => 'check', 'title' => '確認が必要です', 'reasons' => array($verdict['text'], $changelog['text'])); } function phpcheck_commands($version) { $branch = preg_match('/^(\d+\.\d+)/', (string)$version, $m) ? $m[1] : '8.x'; return array( 'Debian / Ubuntu(パッケージに修正が入ったか)' => 'zgrep CVE-2026-109618 /usr/share/doc/php' . $branch . '-common/changelog.Debian.gz', 'RHEL / AlmaLinux / Rocky(同上。Remi は php' . str_replace('.', '', $branch) . '-php-common)' => 'rpm -q --changelog php-common | grep CVE-2026-109618', 'このホストにある PHP を全部出す' => 'ls -1 /usr/bin/php* /usr/sbin/php-fpm* /etc/php 2>/dev/null; apachectl -M 2>/dev/null | grep -i php', '更新後の再起動(どれか該当するもの)' => 'sudo systemctl restart php' . $branch . '-fpm # または apache2 / httpd / php-fpm', ); } /* ---------------------------------------------------------------- 出力 */ function phpcheck_collect() { $sapi = phpcheck_sapi_info(); $version = PHP_VERSION; $verdict = phpcheck_version_verdict($version); $changelog = phpcheck_changelog_info($version); $restart = phpcheck_restart_info($sapi['sapi']); $overall = phpcheck_overall($verdict, $changelog, $restart); return array( 'generated_at' => gmdate('Y-m-d H:i:s') . ' UTC', 'version' => $version, 'sapi' => $sapi, 'verdict' => $verdict, 'changelog' => $changelog, 'restart' => $restart, 'ini' => phpcheck_ini_info(), 'overall' => $overall, 'commands' => phpcheck_commands($version), 'os' => php_uname('s') . ' ' . php_uname('r'), 'ini_file' => (string)php_ini_loaded_file(), ); } function phpcheck_render_text($r) { $lines = array(); $lines[] = '=== ShipCheck PHP 修正確認レシート(' . $r['generated_at'] . ') ==='; $lines[] = '総合: [' . strtoupper($r['overall']['level']) . '] ' . $r['overall']['title']; foreach ($r['overall']['reasons'] as $x) $lines[] = ' - ' . $x; $lines[] = ''; $lines[] = 'PHP ' . $r['version'] . ' / ' . $r['sapi']['label'] . ' (' . $r['sapi']['sapi'] . ') / ' . $r['os']; if ($r['sapi']['server_software'] !== '') $lines[] = 'サーバ: ' . $r['sapi']['server_software']; if ($r['sapi']['note'] !== '') $lines[] = '実行形態の注意: ' . $r['sapi']['note']; $lines[] = '版数の判定: ' . $r['verdict']['text']; $lines[] = 'changelog: ' . $r['changelog']['text'] . ($r['changelog']['file'] ? ' [' . $r['changelog']['file'] . ']' : ''); foreach ($r['changelog']['lines'] as $x) $lines[] = ' ' . $x; $lines[] = '再起動: ' . $r['restart']['text']; if ($r['restart']['binary']) $lines[] = ' 本体: ' . $r['restart']['binary'] . ($r['restart']['binary_mtime'] ? ' (更新 ' . gmdate('Y-m-d H:i:s', $r['restart']['binary_mtime']) . ' UTC)' : ''); if ($r['restart']['proc_start']) $lines[] = ' プロセス起動: ' . gmdate('Y-m-d H:i:s', $r['restart']['proc_start']) . ' UTC'; $lines[] = ''; $lines[] = '設定値:'; foreach ($r['ini'] as $i) $lines[] = ' ' . $i['key'] . ' = ' . $i['value'] . ' ; ' . $i['why']; $lines[] = 'php.ini: ' . ($r['ini_file'] !== '' ? $r['ini_file'] : '(なし)'); $lines[] = ''; $lines[] = '管理者権限で確かめるコマンド:'; foreach ($r['commands'] as $k => $v) { $lines[] = ' # ' . $k; $lines[] = ' ' . $v; } $lines[] = ''; $lines[] = '版数と設定の確認です。侵害の有無や安全を判定するものではありません。このファイルは通信も書き込みもしません。'; return implode("\n", $lines) . "\n"; } function phpcheck_h($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } function phpcheck_render_html($r, $deleted) { $lv = $r['overall']['level']; $color = $lv === 'ok' ? '#176b4d' : ($lv === 'ng' ? '#b3261e' : '#8a5a00'); $o = array(); $o[] = '
'; $o[] = 'ShipCheck
' . phpcheck_h($r['generated_at']) . ' ・ php2shell(CVE-2026-109618 / CVE-2026-109619)
'; $o[] = '版数と設定の確認です。侵害の有無や安全を判定するものではありません。 このファイルは通信も書き込みもしません' . ($deleted ? '(表示と同時にこのファイル自身を削除しました)' : '(このファイルを削除できませんでした。手で削除してください)') . '。
'; $o[] = '| 版数 | PHP ' . phpcheck_h($r['version']) . ' |
|---|---|
| 実行形態 | ' . phpcheck_h($r['sapi']['label']) . ' ' . phpcheck_h($r['sapi']['sapi']) . '' . ($r['sapi']['note'] !== '' ? '' . phpcheck_h($r['sapi']['note']) . '' : '') . ' |
| サーバ | ' . phpcheck_h($r['sapi']['server_software']) . ' |
| OS | ' . phpcheck_h($r['os']) . ' |
| php.ini | ' . phpcheck_h($r['ini_file'] !== '' ? $r['ini_file'] : '(なし)') . ' |
| 版数 | ' . phpcheck_h($r['verdict']['text']) . ' |
|---|---|
| changelog | ' . phpcheck_h($r['changelog']['text']) . ($r['changelog']['file'] ? '' . phpcheck_h($r['changelog']['file']) . '' : '');
foreach ($r['changelog']['lines'] as $x) $o[] = '' . phpcheck_h($x) . '';
$o[] = ' |
| 再起動 | ' . phpcheck_h($r['restart']['text']);
if ($r['restart']['binary']) $o[] = '' . phpcheck_h($r['restart']['binary']) . '' . ($r['restart']['binary_mtime'] ? ' 更新 ' . gmdate('Y-m-d H:i:s', $r['restart']['binary_mtime']) . ' UTC' : '');
if ($r['restart']['proc_start']) $o[] = 'プロセス起動 ' . gmdate('Y-m-d H:i:s', $r['restart']['proc_start']) . ' UTC'; $o[] = ' |
| 項目 | 値 | なぜ見るか |
|---|---|---|
' . phpcheck_h($i['key']) . ' | ' . phpcheck_h($i['value']) . ' | ' . phpcheck_h($i['why']) . ' |
';
foreach ($r['commands'] as $k => $v) $o[] = '# ' . phpcheck_h($k) . "\n" . phpcheck_h($v) . "\n";
$o[] = '';
$o[] = 'レンタルサーバやマネージド環境では、このレシートの版数は提供元が配っている PHP のものです。適用状況は提供元の告知で確認してください。別のプールや別サイトの PHP は、そちらにこのファイルを置いて見てください。
'; $o[] = '出典: php.net Security advisory(2026-10-21)・IPA 注意喚起(2026-10-22)。配布元 shipcheck.runsol.jp/php-check
'; $o[] = ''; return implode("\n", $o) . "\n"; } function phpcheck_main() { $r = phpcheck_collect(); if ($r['sapi']['is_web']) { // 1 回表示したら自分を消す(置き忘れて版数と設定を公開し続けないため)。 $deleted = @unlink(__FILE__); if (!headers_sent()) { header('Content-Type: text/html; charset=utf-8'); header('Cache-Control: no-store'); header('X-Robots-Tag: noindex'); } echo phpcheck_render_html($r, $deleted); } else { echo phpcheck_render_text($r); } }