#!/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