#!/usr/bin/env bash ## fpm-parity-check.sh — end-to-end proof under a REAL web SAPI. ## ## WHY NOT .phpt: the CLI SAPI overwrites DOCUMENT_ROOT / SCRIPT_FILENAME / ## PATH_TRANSLATED after importing the environment, and the cli-server SAPI does ## not process .user.ini at all — so neither can exercise the two things that ## actually matter here. ## ## WHY PHP-FPM: php-fpm takes DOCUMENT_ROOT and SCRIPT_FILENAME as caller- ## supplied FastCGI params and honours .user.ini — structurally the same shape as ## OpenLiteSpeed handing a detached lsphp its LSAPI params. It is the closest ## analogue available without an OLS runtime. ## ## Asserts: ## 1. CONTROL — no mapping => PHP reports the raw /mnt/users paths, i.e. the ## test reproduces the bug before claiming to fix it. ## 2. FIX — mapping => both keys read /home//... . ## 3. WORDFENCE — mapping AND a customer .user.ini auto_prepend_file (the state ## 7 live shared_ols sites are in): paths are STILL corrected ## AND the customer's prepend STILL runs. This is the case the ## old auto_prepend_file normaliser silently lost. ## 4. OLD — for the record: the previous auto_prepend mechanism, with the ## same customer .user.ini, does NOT run. This is the evidence ## that hardening the prepend hook could not have worked. ## ## Usage: ./fpm-parity-check.sh [ROOT] [PHP_FPM_BIN] [EXT_SO] ## ROOT defaults to /mnt/users (falls back to a temp dir if not creatable). set -uo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="${1:-/mnt/users}" FPM_BIN="${2:-$(command -v php-fpm8.3 || echo /usr/sbin/php-fpm8.3)}" EXT_SO="${3:-$HERE/../modules/cac_path_parity.so}" PORT="${PORT:-9001}" command -v cgi-fcgi >/dev/null || { echo "SKIP: cgi-fcgi not installed (apt install libfcgi-bin)"; exit 0; } [ -x "$FPM_BIN" ] || { echo "SKIP: php-fpm not found"; exit 0; } [ -f "$EXT_SO" ] || { echo "SKIP: $EXT_SO not built (run phpize && ./configure && make)"; exit 0; } mkdir -p "$ROOT" 2>/dev/null || ROOT="$(mktemp -d)/mnt/users" USER_NAME=bob SITE="$ROOT/$USER_NAME/site.com" DOCROOT="$SITE/public_html" HOME_PATH="/home/$USER_NAME" TMP="$(mktemp -d)" fail=0 mkdir -p "$DOCROOT" || { echo "cannot create $DOCROOT"; exit 1; } trap 'rm -rf "$TMP"; rm -f "$DOCROOT/.user.ini"' EXIT cat > "$DOCROOT/probe.php" <<'PHP' "$SITE/customer-waf.php" <<'PHP' "$TMP/old-normalize.php" <<'PHP' "$TMP/fpm.conf" <"$TMP/fpm.out" 2>&1 & local pid=$! out="" for _ in $(seq 1 40); do sleep 0.15 out=$(SCRIPT_FILENAME="$DOCROOT/probe.php" DOCUMENT_ROOT="$DOCROOT" \ SCRIPT_NAME=/probe.php REQUEST_METHOD=GET QUERY_STRING= \ cgi-fcgi -bind -connect "127.0.0.1:$PORT" 2>/dev/null) [ -n "$out" ] && break done kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null printf '%s' "$out" } expect() { local label="$1" got="$2" want="$3" if [ "$got" = "$want" ]; then echo " PASS $label" else echo " FAIL $label" echo " want: $want" echo " got: $got" fail=1 fi } field() { printf '%s' "$1" | sed -n "s/^$2=//p"; } EXT=( -d "extension=$EXT_SO" ) MAP=( -d "cac_path_parity.from=$SITE" -d "cac_path_parity.to=$HOME_PATH" ) USERINI_LINE="auto_prepend_file = $SITE/customer-waf.php" echo "== 1. CONTROL: extension loaded, no mapping (reproduces the bug) ==" rm -f "$DOCROOT/.user.ini" out=$(run_case "${EXT[@]}") expect "DOCUMENT_ROOT is the raw OLS path" "$(field "$out" DOCUMENT_ROOT)" "$DOCROOT" expect "SCRIPT_FILENAME is the raw OLS path" "$(field "$out" SCRIPT_FILENAME)" "$DOCROOT/probe.php" echo "== 2. FIX: mapping configured ==" out=$(run_case "${EXT[@]}" "${MAP[@]}") expect "DOCUMENT_ROOT == cac-fpm value" "$(field "$out" DOCUMENT_ROOT)" "$HOME_PATH/public_html" expect "SCRIPT_FILENAME == cac-fpm value" "$(field "$out" SCRIPT_FILENAME)" "$HOME_PATH/public_html/probe.php" echo "== 3. WORDFENCE: customer .user.ini auto_prepend_file present ==" printf '%s\n' "$USERINI_LINE" > "$DOCROOT/.user.ini" out=$(run_case "${EXT[@]}" "${MAP[@]}") expect "DOCUMENT_ROOT still corrected" "$(field "$out" DOCUMENT_ROOT)" "$HOME_PATH/public_html" expect "SCRIPT_FILENAME still corrected" "$(field "$out" SCRIPT_FILENAME)" "$HOME_PATH/public_html/probe.php" expect "customer auto_prepend_file still ran" "$(field "$out" PREPEND_RAN)" "yes" echo "== 4. OLD MECHANISM (why the prepend hook could not be hardened) ==" out=$(run_case -d "auto_prepend_file=$TMP/old-normalize.php") expect "auto_prepend normaliser is displaced by the customer's .user.ini" \ "$(field "$out" DOCUMENT_ROOT)" "$DOCROOT" expect "customer's prepend is the one that ran" "$(field "$out" PREPEND_RAN)" "yes" rm -f "$DOCROOT/.user.ini" if [ "$fail" -eq 0 ]; then echo "ALL PASS"; else echo "FAILURES"; fi exit "$fail"