#!/usr/bin/env bash # 检查 PHP 是否受 CVE-2026-109618 / CVE-2026-109619 影响。 # 只读操作:不会升级、重启或修改服务器配置。 # # 用法: # bash php-cve-check.sh # bash php-cve-check.sh --no-containers # bash php-cve-check.sh --json # # 判断依据: # PHP 8.5 >= 8.5.12 : 已修复 # PHP 8.4 >= 8.4.27 : 已修复 # PHP 8.3 >= 8.3.36 : 已修复 # PHP 8.2 >= 8.2.35 : 已修复 # PHP 8.1 及更早版本 : 官方分支已停止支持,按高风险处理 # # 注意:Linux 发行版可能将安全补丁回移到较低版本号。脚本无法仅凭 PHP_VERSION # 识别这类回移补丁,因此对版本号低于阈值的结果采取保守判断,请再核对发行版安全公告/软件包 changelog。 set -u CVE1="CVE-2026-109618" CVE2="CVE-2026-109619" JSON=0 SCAN_CONTAINERS=1 FOUND_VULNERABLE=0 FOUND_UNKNOWN=0 usage() { sed -n '1,35p' "$0" } while [[ $# -gt 0 ]]; do case "$1" in --json) JSON=1 ;; --no-containers) SCAN_CONTAINERS=0 ;; -h|--help) usage; exit 0 ;; *) echo "未知参数:$1" >&2; usage >&2; exit 2 ;; esac shift done # 输出一条结果。JSON 模式下只输出简单、可被 jq 读取的 JSON 对象。 report() { local source="$1" path="$2" version="$3" status="$4" detail="$5" if [[ "$JSON" -eq 1 ]]; then python3 - "$source" "$path" "$version" "$status" "$detail" <<'PY' import json, sys keys = ["source", "path", "version", "status", "detail"] print(json.dumps(dict(zip(keys, sys.argv[1:])), ensure_ascii=False)) PY else printf '%-18s %-42s %-12s %-28s %s\n' "$source" "$path" "$version" "$status" "$detail" fi } # 返回:0=安全,1=易受影响/不受支持,2=未知。 classify() { local version="$1" major minor patch if [[ ! "$version" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then echo "UNKNOWN|无法解析版本号" return 2 fi major="${BASH_REMATCH[1]}"; minor="${BASH_REMATCH[2]}"; patch="${BASH_REMATCH[3]}" if [[ "$major" -ne 8 ]]; then if (( major < 8 )); then echo "VULNERABLE_OR_UNSUPPORTED|PHP 8.1 及更早分支,官方不再提供常规修复" return 1 fi echo "UNKNOWN|超出本次公告覆盖的已知版本范围,请核对该分支公告" return 2 fi case "$minor" in 2) if (( patch >= 35 )); then echo "FIXED|PHP 8.2.35 或更高"; return 0; fi echo "VULNERABLE|低于 PHP 8.2.35"; return 1 ;; 3) if (( patch >= 36 )); then echo "FIXED|PHP 8.3.36 或更高"; return 0; fi echo "VULNERABLE|低于 PHP 8.3.36"; return 1 ;; 4) if (( patch >= 27 )); then echo "FIXED|PHP 8.4.27 或更高"; return 0; fi echo "VULNERABLE|低于 PHP 8.4.27"; return 1 ;; 5) if (( patch >= 12 )); then echo "FIXED|PHP 8.5.12 或更高"; return 0; fi echo "VULNERABLE|低于 PHP 8.5.12"; return 1 ;; 0|1) echo "VULNERABLE_OR_UNSUPPORTED|PHP 8.1 及更早分支,官方不再提供常规修复"; return 1 ;; *) echo "UNKNOWN|PHP 8.${minor} 不在官方公告列出的分支中,请人工核对"; return 2 ;; esac } if [[ "$JSON" -eq 0 ]]; then echo "PHP 漏洞检查:$CVE1 / $CVE2" echo "检查时间:$(date -Is)" printf '%-18s %-42s %-12s %-28s %s\n' "来源" "路径/容器" "版本" "状态" "说明" printf '%-18s %-42s %-12s %-28s %s\n' "------" "------------------------------------------" "------------" "----------------------------" "--------------------" fi # 记录已经检查过的宿主机可执行文件,避免 PATH 和目录扫描重复输出。 declare -A SEEN check_binary() { local source="$1" bin="$2" version result status detail rc [[ -x "$bin" ]] || return 0 bin="$(readlink -f "$bin" 2>/dev/null || printf '%s' "$bin")" [[ -n "${SEEN[$bin]+x}" ]] && return 0 SEEN["$bin"]=1 version="$("$bin" -r 'echo PHP_VERSION;' 2>/dev/null || true)" if [[ -z "$version" ]]; then version="$("$bin" -v 2>/dev/null | sed -n 's/^PHP \([0-9][0-9.]*\).*/\1/p' | head -n1)" fi if [[ -z "$version" ]]; then report "$source" "$bin" "-" "UNKNOWN" "无法执行或读取 PHP 版本" FOUND_UNKNOWN=1 return 0 fi result="$(classify "$version")"; rc=$? status="${result%%|*}"; detail="${result#*|}" report "$source" "$bin" "$version" "$status" "$detail" (( rc == 1 )) && FOUND_VULNERABLE=1 (( rc == 2 )) && FOUND_UNKNOWN=1 } # 1. PATH 中的 php,以及常见目录中的 php/php-cgi/php-fpm。 while IFS= read -r bin; do check_binary "host-binary" "$bin"; done < <( { command -v php 2>/dev/null || true command -v php-cgi 2>/dev/null || true command -v php-fpm 2>/dev/null || true command -v php-fpm8.1 2>/dev/null || true command -v php-fpm8.2 2>/dev/null || true command -v php-fpm8.3 2>/dev/null || true command -v php-fpm8.4 2>/dev/null || true command -v php-fpm8.5 2>/dev/null || true find /usr/bin /usr/local/bin /opt -maxdepth 3 -type f \( -name 'php' -o -name 'php[0-9]*' -o -name 'php-cgi' -o -name 'php-fpm*' \) -perm -111 2>/dev/null || true } | sort -u ) # 2. 从正在运行的 PHP 进程反查实际执行文件,避免 PATH 版本与服务版本不同。 if [[ -r /proc/1/cmdline ]]; then for proc in /proc/[0-9]*; do [[ -r "$proc/exe" ]] || continue cmd="$(tr '\0' ' ' < "$proc/cmdline" 2>/dev/null || true)" [[ "$cmd" =~ php|php-fpm|php-cgi ]] || continue exe="$(readlink -f "$proc/exe" 2>/dev/null || true)" [[ -n "$exe" ]] && check_binary "running-process" "$exe" done fi # 3. 可选扫描正在运行的 Docker 容器。只执行 php -r,不修改容器。 if [[ "$SCAN_CONTAINERS" -eq 1 ]] && command -v docker >/dev/null 2>&1; then while IFS= read -r cid; do [[ -z "$cid" ]] && continue name="$(docker inspect --format '{{.Name}}' "$cid" 2>/dev/null | sed 's#^/##')" version="$(docker exec "$cid" php -r 'echo PHP_VERSION;' 2>/dev/null || true)" if [[ -z "$version" ]]; then report "docker-container" "$name ($cid)" "-" "NOT_FOUND" "容器内未找到可执行的 php CLI,可能仍运行 PHP-FPM/CGI" continue fi result="$(classify "$version")"; rc=$? status="${result%%|*}"; detail="${result#*|}" report "docker-container" "$name ($cid)" "$version" "$status" "$detail" (( rc == 1 )) && FOUND_VULNERABLE=1 (( rc == 2 )) && FOUND_UNKNOWN=1 done < <(docker ps -q 2>/dev/null) elif [[ "$JSON" -eq 0 ]]; then echo "提示:未扫描 Docker 容器(docker 不可用或使用了 --no-containers)。" fi if [[ "$JSON" -eq 0 ]]; then echo if (( FOUND_VULNERABLE )); then echo "结论:发现易受影响或官方不再支持的 PHP 环境。请立即升级并重启对应 PHP-FPM/Web 服务。" echo "注意:低版本发行版软件包可能已回移补丁,请结合发行版安全公告复核。" exit 1 elif (( FOUND_UNKNOWN )); then echo "结论:未发现明确的易受影响版本,但存在无法自动判定的环境,请人工核对。" exit 2 else echo "结论:扫描到的 PHP 环境均达到本次公告列出的修复版本。" exit 0 fi else # JSON 模式仍用退出码表示总体结果:0 安全,1 有风险,2 有未知项。 if (( FOUND_VULNERABLE )); then exit 1; elif (( FOUND_UNKNOWN )); then exit 2; else exit 0; fi fi