PHP version it was alerted for}. Swept by uninstall's csdt_ prefix. */ const OPTION_SENT = 'csdt_php_critical_sent'; /** * The flaws. 'fixed' maps each branch php.net patched to its first fixed release. A branch * older than every listed one got no fix and is reported as exposed; a newer one shipped * after the fix and is not. * * An optional 'requires' narrows a flaw to the environments it can be exploited in: 'os' is * PHP_OS_FAMILY and 'sapi' is a list of web SAPIs. A flaw whose environment this site is not in * is never reported, so a flaw that only bites PHP-CGI on Windows does not alarm the Linux and * PHP-FPM sites that are the norm. 'note' is an extra sentence appended for that flaw. */ const FLAWS = array( array( 'id' => 'php2shell-2026-10', 'name' => 'php2shell', 'cves' => array( 'CVE-2026-109618', 'CVE-2026-109619' ), 'cvss' => '9.2', 'disclosed' => '2026-10-21', 'fixed' => array( '8.2' => '8.2.35', '8.3' => '8.3.36', '8.4' => '8.4.27', '8.5' => '8.5.12', ), ), array( 'id' => 'php-cgi-argument-injection-2024-06', 'name' => 'PHP-CGI argument injection', 'cves' => array( 'CVE-2024-4577' ), 'cvss' => '9.8', 'disclosed' => '2024-06-06', 'fixed' => array( '8.1' => '8.1.29', '8.2' => '8.2.20', '8.3' => '8.3.8', ), 'requires' => array( 'os' => 'Windows', 'sapi' => array( 'cgi-fcgi', 'cgi' ), ), 'note' => 'This one is exploitable only where PHP runs in CGI mode (php-cgi) on Windows, which is this server. Serving PHP through PHP-FPM instead, or putting the CloudScale Edge Gateway in front of the site, also closes it.', ), ); /** Test-only PHP version, SAPI and OS. Null in production. */ public static $version_override = null; public static $sapi_override = null; public static $os_override = null; /** Hooks. Called from CSDT_DevTools::init(). */ public static function register(): void { add_action( 'admin_notices', array( __CLASS__, 'render_notice' ), 5, 0 ); add_action( 'network_admin_notices', array( __CLASS__, 'render_notice' ), 5, 0 ); // After CSDT_Php_Advisory::observe_runtime() (20), so a just-upgraded PHP is recorded first. add_action( 'admin_init', array( __CLASS__, 'maybe_alert' ), 30, 0 ); // Core's own twice-daily cron, present on every install: sites nobody logs in to still email. add_action( 'wp_version_check', array( __CLASS__, 'maybe_alert' ), 30, 0 ); } // ── Assessment ───────────────────────────────────────────────────────── /** * The web server's PHP version, or '' when it cannot be known from this process. * * @return string */ public static function version(): string { $sapi = null !== self::$sapi_override ? self::$sapi_override : PHP_SAPI; if ( '' !== $sapi && ! in_array( $sapi, array( 'cli', 'phpdbg', 'embed', 'cli-server' ), true ) ) { return CSDT_Php_Advisory::clean_version( null !== self::$version_override ? self::$version_override : PHP_VERSION ); } $rt = CSDT_Php_Advisory::runtime(); return null === $rt ? '' : $rt['version']; } /** * The web server's SAPI: this process's when it is a web one, else the recorded runtime SAPI, * so the cron path knows it too. '' when it cannot be known. * * @return string */ public static function sapi(): string { $sapi = null !== self::$sapi_override ? self::$sapi_override : PHP_SAPI; if ( '' !== $sapi && ! in_array( $sapi, array( 'cli', 'phpdbg', 'embed', 'cli-server' ), true ) ) { return $sapi; } $rt = CSDT_Php_Advisory::runtime(); return null === $rt ? '' : (string) $rt['sapi']; } /** The server's OS family (PHP_OS_FAMILY); the one machine serves both web requests and cron. */ public static function os(): string { return null !== self::$os_override ? self::$os_override : PHP_OS_FAMILY; } /** * Whether this site is in an environment a flaw's 'requires' narrows it to. A missing key is * not a constraint; an unknown SAPI ('') matches nothing, so a flaw is never reported on a guess. * * @param array $requires {os?:string, sapi?:string[]} * @return bool */ private static function environment_has( array $requires ): bool { if ( isset( $requires['os'] ) && self::os() !== $requires['os'] ) { return false; } if ( isset( $requires['sapi'] ) && ! in_array( self::sapi(), (array) $requires['sapi'], true ) ) { return false; } return true; } /** * The listed flaws a PHP version has. * * @param string $version A clean PHP version, "8.3.35". * @return array * fixed is '' when php.net shipped no fix for the branch; upgrade is what to ask for; * note is an extra sentence for the flaw, or ''. */ public static function assess( string $version ): array { if ( ! preg_match( '/^(\d+)\.(\d+)\.\d+$/', $version, $m ) ) { return array(); } $branch = $m[1] . '.' . $m[2]; $out = array(); foreach ( self::FLAWS as $f ) { if ( isset( $f['requires'] ) && ! self::environment_has( $f['requires'] ) ) { continue; } $branches = array_keys( $f['fixed'] ); usort( $branches, 'version_compare' ); $fixed = ''; if ( isset( $f['fixed'][ $branch ] ) ) { if ( version_compare( $version, $f['fixed'][ $branch ], '>=' ) ) { continue; } $fixed = $f['fixed'][ $branch ]; $upgrade = 'PHP ' . $fixed . ' or later'; } elseif ( version_compare( $branch, $branches[0], '<' ) ) { $upgrade = 'a supported PHP branch: ' . implode( ', ', array_map( array( __CLASS__, 'or_later' ), array_values( $f['fixed'] ) ) ); } else { continue; } $out[] = array( 'id' => $f['id'], 'name' => $f['name'], 'cves' => $f['cves'], 'cvss' => $f['cvss'], 'disclosed' => $f['disclosed'], 'branch' => $branch, 'fixed' => $fixed, 'upgrade' => $upgrade, 'note' => $f['note'] ?? '', ); } return $out; } /** "8.3.36" reads "8.3.36+". */ private static function or_later( string $v ): string { return $v . '+'; } /** * The listed flaws this site's PHP has. Empty when none, or the version is unknown. * * @return array */ public static function current(): array { $v = self::version(); return '' === $v ? array() : self::assess( $v ); } /** * One plain sentence per flaw: what it is and what to ask the host for. * * @param array $hit One assess() row. * @param string $version The PHP version. * @return string */ public static function sentence( array $hit, string $version ): string { $fix = '' === $hit['fixed'] ? 'php.net shipped no fix for PHP ' . $hit['branch'] . '. ' : 'Fixed in PHP ' . $hit['fixed'] . '. '; $note = '' !== (string) ( $hit['note'] ?? '' ) ? ' ' . $hit['note'] : ''; return 'PHP ' . $version . ' has ' . $hit['name'] . ' (' . implode( ', ', $hit['cves'] ) . ', CVSS ' . $hit['cvss'] . ', disclosed ' . $hit['disclosed'] . '): anyone on the internet can run code on this server without logging in. ' . $fix . 'The flaw is in PHP itself, so no WordPress plugin or setting can block it. Ask your host to upgrade to ' . $hit['upgrade'] . ' today.' . $note; } // ── Site Audit ───────────────────────────────────────────────────────── /** * The critical finding, or null when this site's PHP has no listed flaw. Never a request. * * @return array|null */ public static function finding(): ?array { $v = self::version(); $hits = '' === $v ? array() : self::assess( $v ); if ( array() === $hits ) { return null; } $names = array(); $details = array(); $asks = array(); foreach ( $hits as $h ) { $names[] = $h['name']; $details[] = self::sentence( $h, $v ); $asks[] = $h['upgrade']; } return array( 'key' => 'php_critical_unpatched', 'category' => 'Security', 'severity' => 'critical', 'title' => 'PHP ' . $v . ' has ' . implode( ' and ', $names ) . ': anyone can run code on this server', 'detail' => implode( ' ', $details ), 'fix' => 'Ask your host to upgrade to ' . implode( '; ', array_unique( $asks ) ) . '. On your own server, install the PHP update; on Docker, rebuild the image. Then reload any admin page: this warning clears by itself. Status: AI Security Scan → PHP Advisory Alerts.', 'affected' => 'PHP ' . $v . ' (web server)', ); } // ── The notice ───────────────────────────────────────────────────────── /** admin_notices: red, every admin screen, manage_options only, not dismissible while true. */ public static function render_notice(): void { try { if ( ! current_user_can( 'manage_options' ) ) { return; } $v = self::version(); $hits = '' === $v ? array() : self::assess( $v ); if ( array() === $hits ) { return; } } catch ( \Throwable $e ) { if ( class_exists( 'CSDT_Monitor' ) ) { CSDT_Monitor::log_exception( $e, __METHOD__ ); } return; } $href = admin_url( 'admin.php?page=' . CSDT_DevTools::TOOLS_SLUG . '&tab=security' ) . '#cs-panel-php-advisory'; ?>