# Patch Analysis for CVE-2026-8441 Variant Stage

## Fix Summary

The reproduction-stage fixed control modifies WP Review Slider Pro's review-query helper in:

- `public/partials/getreviews_class.php`
- Function: `GetReviews_Functions::wppro_queryreviews()`
- Parent sink: `$notinsearchstring = " AND id NOT IN (".$notinstring.") "`

The vulnerable 12.6.7 code treats `notinstring` as a comma-separated list of already-displayed review IDs, but only splits and rejoins it:

```php
$tempnotinarray = explode(",",$notinstring);
if(is_array($tempnotinarray)){
    $notinstring = implode(",",$tempnotinarray);
    $notinsearchstring = " AND id NOT IN (".$notinstring.") ";
}
```

The fixed control changes that line to integer-cast every CSV element before rebuilding the numeric `NOT IN` clause:

```php
$tempnotinarray = array_filter(array_map('absint', explode(",", $notinstring)));
```

This converts payloads such as `1) OR (SELECT SLEEP(3))#` into numeric IDs only, preventing SQL syntax from escaping the `NOT IN (...)` expression.

## Patch Assumptions

The fix assumes:

1. `notinstring` is semantically a list of review IDs and should never contain SQL syntax or non-numeric identifiers.
2. The exploited parent primitive is specifically the numeric `id NOT IN (...)` fragment.
3. Casting the list elements with `absint()` preserves legitimate load-more behavior while removing attacker-controlled SQL operators.
4. Other public filter fields appended to the same query are either separately safe, escaped by WordPress/`wpdb`, or outside the parent advisory's immediate patch scope.

## Covered Code Paths

The patch covers the disclosed unauthenticated parent path:

1. Public page rendering `[wprevpro_usetemplate]` exposes `wpfb_nonce` via `wp_localize_script()`.
2. Unauthenticated visitor sends `POST /wp-admin/admin-ajax.php` with `action=wprp_load_more_revs`.
3. `includes/class-wp-review-slider-pro.php` registers both `wp_ajax_wprp_load_more_revs` and `wp_ajax_nopriv_wprp_load_more_revs`.
4. `WP_Review_Pro_Public::wppro_loadmore_revs_ajax()` reads `$_POST['notinstring']`.
5. `GetReviews_Functions::wppro_queryreviews()` builds the `id NOT IN (...)` filter.
6. `$wpdb->get_results()` executes the constructed review query.

The variant script confirms the covered behavior at runtime:

- Vulnerable original `notinstring` `SLEEP(3)`: ~3.10s.
- Fixed-control original `notinstring` `SLEEP(3)`: ~0.045s.

## Code Paths / Inputs Reviewed but Not Confirmed as Bypasses

`wppro_queryreviews()` contains additional dynamic filters appended to the same `SELECT` query. The variant stage reviewed and tested public fields accepted by `wppro_loadmore_revs_ajax()`:

- `textsource` -> `$publicsourcefilter = " AND pageid = '".sanitize_text_field($textsource)."'"`
- `textsearch` -> `$textsearchquery` with `LIKE` predicates over review text/name/title/type/tags
- `textlang` -> `$publiclangfilter = " AND language_code = '".sanitize_text_field($textlang)."'"`
- `textrtype` -> `$publicrtypefilter = "AND type = '".sanitize_text_field($textrtype)."'"`
- `shortcodepageid` -> `$rpagefilter` through the shortcode page ID argument

These paths were materially distinct candidate data paths because they are attacker-controlled HTTP parameters and reach the same review-query construction. However, no tested candidate produced a delay on the fixed control. The response `dbcall` fields showed quote characters escaped in string-context filters, and all candidate timings were fast (~0.014-0.017s).

## What the Fix Does Not Broadly Change

The notinstring fix does not refactor the full query builder. The function still builds many SQL fragments by string concatenation and relies on a mixture of `intval()`, `sanitize_text_field()`, `$wpdb->prepare()`, and runtime escaping. Although no bypass was confirmed in this run, this style remains fragile because future changes could accidentally move untrusted values into unquoted or unescaped SQL contexts.

The patch also does not change:

- public exposure of the AJAX nonce on frontend pages using the plugin shortcode;
- unauthenticated reachability of `wprp_load_more_revs`;
- query fragments generated from administrator-controlled template settings;
- the use of a single large dynamically assembled SQL statement.

## Behavior Before and After the Fix

### Before fix

- `notinstring=1) OR (SELECT SLEEP(3))#` enters the unquoted numeric `NOT IN` expression.
- MariaDB evaluates the injected `SLEEP(3)`.
- The HTTP request takes approximately three seconds.

### After notinstring fix

- The same `notinstring` payload is parsed as a CSV list and cast through `absint()`.
- SQL metacharacters are removed from the numeric ID list.
- The same request is fast.

### Alternate public-field candidates after fix

- Candidate quote-break payloads in `textsource`, `textsearch`, `textlang`, `textrtype`, and `shortcodepageid` did not execute timing primitives.
- The tested fixed-control behavior was fast for all candidates.
- No distinct bypass was confirmed.

## Threat Model / Security Policy Review

The bundled commercial plugin source did not contain a `SECURITY.md`, `security.txt`, `README.md`, or explicit vulnerability/threat-model document. The relevant trust boundary for this analysis is therefore the standard WordPress public HTTP boundary: unauthenticated visitors can load a page containing `[wprevpro_usetemplate]`, receive a frontend nonce, and call the `wp_ajax_nopriv_wprp_load_more_revs` AJAX endpoint.

Within that boundary, the parent bug is security-relevant because attacker-controlled HTTP input reaches database query construction. The variant candidates were only considered valid if they used the same public HTTP boundary and the same review-query sink.

## Completeness Assessment

For the parent CVE path, the notinstring integer-casting patch is effective in the tested fixed control. The bounded variant search did not find a bypass through other public filter fields. However, a more complete long-term fix would replace broad SQL string concatenation in `wppro_queryreviews()` with structured query construction and `$wpdb->prepare()` placeholders for every dynamic value, because the current function remains difficult to audit and has multiple string-built predicates.