# Patch Analysis: WordPress 7.0.2 Coverage for CVE-2026-63030

## Scope and source identities

This analysis compares vulnerable WordPress 7.0.1 commit `18f793b1f16c1b15b0fc37027f4aeaefab0bfe02` with fixed tag/commit `855551c4477bd5a0407221c57dae123c4163b434`. It also checks the exact origin/trunk revision resolved on 2026-07-18, `ace9192af868524bdc49cf4fcb91f4c12c73ee5f`. The complete relevant diff is retained at `bundle/logs/vuln_variant/fix_diff.txt`.

## Target security policy / threat model

The repository's `SECURITY.md` states that WordPress Core is covered and directs potential vulnerabilities to the WordPress HackerOne program. It does not exclude unauthenticated REST requests, privilege escalation, SQL injection, or administrator creation. The parent and candidate paths cross an explicit security boundary: an unauthenticated network client sends data to stock WordPress. This differs from local, user-selected input and is in scope. A copy is retained at `bundle/logs/vuln_variant/security_policy.md`.

## What the fix changes

### 1. Batch request alignment

- **File:** `src/wp-includes/rest-api/class-wp-rest-server.php`
- **Function:** `WP_REST_Server::serve_batch_request_v1()`
- **Commit:** `c8bdf1fa12355f79db94054d307d0e3898b501c9`
- **Change:** When a parsed subrequest is a `WP_Error`, the fixed loop now appends that same object to both `$matches` and `$validation` before continuing. In 7.0.1 it appended only to `$validation`.
- **Effect:** `$requests[$i]`, `$matches[$i]`, and `$validation[$i]` retain a one-to-one relationship. A later valid request can no longer inherit another route's callback or permission callback.

### 2. `author__not_in` integer normalization

- **File:** `src/wp-includes/class-wp-query.php`
- **Function:** `WP_Query::get_posts()`
- **Commit:** `74d37a344cbf28e9187a1a5ca71b33d186bcd333`
- **Change:** Every non-empty `author__not_in` value is passed through `wp_parse_id_list()`. Only a non-empty resulting integer list is sorted and interpolated into `NOT IN (...)`; the normalized list replaces the query variable for stable cache keys.
- **Effect:** The vulnerable scalar string no longer reaches SQL syntax. The normalization is at the sink and applies independently of whether upstream data originated as a scalar, comma-separated string, or array.

### 3. Nested top-level REST dispatch prevention

- **Files:** `src/wp-includes/rest-api.php` and `src/wp-includes/rest-api/class-wp-rest-server.php`
- **Functions:** `rest_api_loaded()` and `WP_REST_Server::serve_request()`
- **Commit:** `85015b84fbc52bf6151a691299896b8971772594`
- **Change:** `rest_api_loaded()` returns before `define()`/`die()` when the global REST server is already dispatching. `serve_request()` independently returns `false` if `is_dispatching()` is true.
- **Effect:** A dynamic post transition or other hook cannot start a second top-level REST lifecycle inside the outer request. Legitimate internal REST subrequests must use `dispatch()`.

## Fix assumptions

The patch relies on these invariants:

1. Every malformed batch URL becomes a `WP_Error` and every such error receives one corresponding match/validation slot.
2. The injectable parent sink is `WP_Query`'s `author__not_in` clause, so sink-level integer parsing covers all input representations and callers.
3. The privilege transition requires top-level `serve_request()` re-entry; ordinary internal `dispatch()` preserves normal permission checks and does not recreate the vulnerable top-level lifecycle.
4. The global REST server accurately tracks active requests through `is_dispatching()`.

Inspection of 7.0.2 and trunk supports these assumptions for this chain. At tested trunk, there is one production `serve_request()` caller, in guarded `rest_api_loaded()`, plus the method-level guard.

## Paths and inputs not changed by the patch

The release intentionally does **not** change:

- oEmbed cache creation and partial post updates;
- `wp_update_post()` cache/object merging;
- hierarchy repair;
- Customizer changeset publication and temporary per-setting user switching;
- dynamic `{$new_status}_{$post_type}` hooks;
- normal `WP_REST_Server::dispatch()` behavior;
- unrelated query classes and unrelated SQL construction;
- the separate `author__in` implementation.

These are not independently demonstrated vulnerabilities at the same unauthenticated boundary. They become relevant to the parent exploit only after attacker-controlled forged rows exist and nested top-level dispatch is possible. Fixing three earlier independent gates prevents that composition without disabling legitimate core behavior.

`author__in` is a nearby asymmetry: it still uses array/scalar conversion plus `absint`, rather than `wp_parse_id_list()`. However, every value is integer-cast before SQL interpolation, so source inspection provides no same-sink SQL-injection bypass. It may merit consistency hardening but is not a confirmed variant.

## Variant/rule-out matrix

| Candidate | Material distinction | 7.0.1 runtime | 7.0.2 runtime | Trunk check | Result |
|---|---|---|---|---|---|
| Scalar `author_exclude` in shifted subrequest JSON body | Alternate data path rather than URL query | No forged marker | No forged marker | Sink normalization retained | Not a trigger; handler does not consume body placement as required query input |
| Scalar `author_exclude` in URL query | Parent primitive/control | Forged full-row marker reflected | Marker absent | Batch and normalization guards retained | Confirms valid test environment; no bypass |
| Malformed batch entry followed by post/user routes | Alternate malformed-entry placement/alignment behavior | Shifted user-controller response observed | Correct posts-controller response observed | One `$matches[]` append per error retained | Alignment fix complete for first/middle/multiple errors; last error cannot shift a later request |
| Alternate `serve_request()` caller | Alternate privileged entry point | Parent uses `rest_api_loaded()` | Both caller and callee guarded | One production caller, guarded | No alternate stock entry point found |

Three meaningful candidates/path classes exhaust the material distinctions identified around the parent chain. Additional APIs that merely call the same already-normalized `WP_Query` sink, or later post-processing methods after a forged cache object, would not be distinct triggers and were not inflated into findings.

## Behavior before and after

### WordPress 7.0.1

- A malformed batch entry omits a `$matches` slot and shifts later handlers.
- The shifted posts handler can consume a scalar URL-query `author_exclude` value.
- `WP_Query` directly interpolates the scalar into `author__not_in` SQL.
- The stage's synthetic full-row marker is reflected through REST, proving this primitive live.
- Nested `serve_request()` re-entry remains possible during the parent chain.

### WordPress 7.0.2

- The malformed entry receives aligned match and validation error slots.
- The same outer/nested request reaches the correctly matched unauthenticated posts route and is denied.
- Even if another caller supplied `author__not_in`, SQL receives integers only.
- Even if forged state somehow reached the dynamic hook, active dispatch blocks a new top-level REST cycle.
- Runtime probes leave one user, four stock posts, and zero oEmbed cache rows.

### Latest trunk at `ace9192...`

- All three guards are present unchanged in their security-relevant logic.
- No additional production `serve_request()` entry point bypasses `rest_api_loaded()`.
- The trunk check is source-backed; exact 7.0.2 remains the live fixed runtime control.

## Completeness assessment

For the disclosed chain and materially distinct paths found in the bounded scan, the fix is complete. It does not merely block the ticket's exact payload: it restores a structural batch invariant, normalizes at the SQL sink regardless of input shape, and blocks nested top-level dispatch at both caller and callee. No fixed-version marker, persistent write, administrator, or code execution was observed.

This is a negative variant verdict, not proof that unrelated future vulnerabilities cannot exist. Recommended defense-in-depth work is to add regression tests for malformed errors in every batch position, normalize `author__in` consistently, assert that internal subrequests always use `dispatch()`, and maintain an end-to-end fixed oracle covering users/posts/oEmbed/plugin state.
