144 lines
5.6 KiB
Bash
144 lines
5.6 KiB
Bash
#!/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/<user>/... .
|
||
|
|
## 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'
|
||
|
|
<?php
|
||
|
|
echo "DOCUMENT_ROOT=" . $_SERVER['DOCUMENT_ROOT'] . "\n";
|
||
|
|
echo "SCRIPT_FILENAME=" . $_SERVER['SCRIPT_FILENAME'] . "\n";
|
||
|
|
echo "PREPEND_RAN=" . (defined('CUSTOMER_PREPEND_RAN') ? 'yes' : 'no') . "\n";
|
||
|
|
PHP
|
||
|
|
|
||
|
|
## Stand-in for the customer's wordfence-waf.php.
|
||
|
|
cat > "$SITE/customer-waf.php" <<'PHP'
|
||
|
|
<?php
|
||
|
|
define('CUSTOMER_PREPEND_RAN', 1);
|
||
|
|
PHP
|
||
|
|
|
||
|
|
## Stand-in for the OLD mechanism (scripts/cac-lsphp-normalize.php).
|
||
|
|
cat > "$TMP/old-normalize.php" <<'PHP'
|
||
|
|
<?php
|
||
|
|
foreach (array('DOCUMENT_ROOT', 'SCRIPT_FILENAME') as $k) {
|
||
|
|
if (!empty($_SERVER[$k]) && strncmp($_SERVER[$k], '/mnt/users/', 11) === 0) {
|
||
|
|
$r = realpath($_SERVER[$k]);
|
||
|
|
if ($r !== false) { $_SERVER[$k] = $r; }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
PHP
|
||
|
|
|
||
|
|
cat > "$TMP/fpm.conf" <<EOF
|
||
|
|
[global]
|
||
|
|
error_log = $TMP/fpm-error.log
|
||
|
|
daemonize = no
|
||
|
|
[www]
|
||
|
|
listen = 127.0.0.1:$PORT
|
||
|
|
pm = static
|
||
|
|
pm.max_children = 2
|
||
|
|
EOF
|
||
|
|
|
||
|
|
run_case() {
|
||
|
|
"$FPM_BIN" -n -y "$TMP/fpm.conf" -F -d user_ini.cache_ttl=0 "$@" \
|
||
|
|
>"$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"
|