# CVE-2026-54823 — Variant / Bypass Analysis (Widget Options 4.2.4 fix)

## Summary

**No bypass was found.** After systematically enumerating the materially
distinct trigger paths that could reach the same `widgetopts_safe_eval() ->
eval()` sink as the parent CVE-2026-54823 (Contributor RCE via the
block-renderer REST endpoint), every candidate variant is **blocked on the
fixed 4.2.4 and the current/latest 4.2.5** builds. The fix is a *closed-default
trust gate*: `widgetopts_safe_eval()` now returns `true` (show, no eval) for any
caller that is not an administrator **and** is not executing inside
`widgetopts_safe_eval_trusted()`, whose only call sites consume admin-controlled
/ DB-backed input (classic widgets, Elementor, Beaver, SiteOrigin, and the
admin-only `widgetopts_snippet` CPT). Because a Contributor can never set the
trust flag and can never supply input to a trusted call site, no Contributor
payload of any shape reaches `eval()` on 4.2.4/4.2.5 — this was verified against
the **real extracted plugin functions** with a parse-error probe that
distinguishes "gate blocked (eval never reached)" from "eval reached (Throwable
caught)". On the **vulnerable 4.2.3**, the same probe confirms `eval()` *is*
reached for a contributor, and the alternate payloads (original, stripslashes-
differential, `${'z'}()` brace-call) execute — i.e. they are alternate triggers
of the *same* root cause on the vulnerable version, **not** a bypass of the fix.
The latest release **4.2.5 is byte-identical to 4.2.4 on the entire security
surface** (`includes/extras.php` and `includes/widgets/gutenberg/gutenberg-toolbar.php`
compare equal; 4.2.5 is only a "tested for WordPress 7.0" compat bump per its
changelog), so the 4.2.4 analysis covers the current release.

## Fix Coverage / Assumptions

**Invariant the fix relies on:** the only way a non-administrator can reach
`eval()` is through `widgetopts_safe_eval_trusted()`, and every call site of
that wrapper feeds it input that is provably **admin-controlled / DB-backed**
(not attacker-supplied over the trust boundary). The closed default is:
```php
function widgetopts_safe_eval($expression) {
    if (!current_user_can('manage_options') && !widgetopts_eval_is_trusted()) {
        return true;                       // G1: contributor -> no eval, ever
    }
    if (widgetopts_is_widget_or_post_preview()) {
        if (!current_user_can('manage_options')) { return true; } // G5: preview
    }
    $validation_result = widgetopts_validate_expression($expression); // G2 + blocklist
    ...
    @eval($expression);
}
```

**Code paths explicitly covered:**
- **G1 — closed-default trust gate** (`includes/extras.php::widgetopts_safe_eval`,
  line ~707-711): any non-admin outside a trusted wrapper is short-circuited
  *before* the validator and `eval`. The trust flag (`$GLOBALS['_widgetopts_trust_depth']`)
  is set only by `widgetopts_safe_eval_trusted()`.
- **G2 — token validator hardening** (`widgetopts_validate_code_with_tokens`,
  line ~1077): now blocks `}`, `T_FUNCTION`, `T_FN` (in addition to `]`, `)`)
  immediately before `(`, defeating `${'z'}(...)` / `Foo::{'m'}(...)` /
  closure-IIFE shapes; also blocks `T_EVAL/T_INCLUDE/T_REQUIRE/T_EXIT/T_GOTO`
  constructs and `T_ENCAPSED_AND_WHITESPACE`.
- **G3 — render_block_data sha256 allowlist** (`gutenberg-toolbar.php` line ~429,
  scoped via `rest_pre_dispatch` flag `_widgetopts_in_block_renderer` to the
  `/wp/v2/block-renderer/` route): zeroes any
  `extended_widget_opts[_block].class.logic` whose sha256 is not among the
  values actually stored in the referenced post's `post_content`.
- **G4 — save/REST stripper** (`wp_insert_post_data` filter line ~377 +
  `rest_request_before_callbacks` scrub `extras.php` line ~679 +
  `widgetopts_strip_logic_from_blocks` line ~306): strips legacy
  `extended_widget_opts[_block].class.logic` **and** freeform
  `<!--start_widgetopts ... end_widgetopts-->` comment logic for non-admins on
  every save (classic editor, block editor, `wp_insert_post`) and every
  non-admin REST write (GET/HEAD/OPTIONS untouched, but those are covered by G3
  on the block-renderer route).
- **G5 — preview guard**: non-admin preview / customizer / `?preview=true` ->
  no eval even when trusted (prevents a contributor from previewing a draft to
  trigger their own logic).

**What the fix does NOT cover (examined, found non-exploitable):**
- The freeform `<!--start_widgetopts-->` render extractor
  (`blockopts_filter_before_display`) reads `innerContent[0]` and calls
  `widgetopts_safe_eval_trusted()` — which **sets the trust flag**. This is the
  most dangerous-looking path because G3 (the allowlist) only inspects parsed
  `attrs`, not `innerContent`. However it is neutralised by **G4** (the save
  stripper explicitly matches and rewrites freeform comments for non-admins) and
  by reachability: the block-renderer REST endpoint builds blocks from request
  `attributes` only and does **not** populate `innerContent`, so the freeform
  extractor cannot fire through that endpoint; and a contributor cannot publish,
  so a saved freeform comment only renders on preview (G5) or after an admin
  publishes it (admin action, not a contributor trigger).
- `post_id` manipulation against G3: a contributor controls `$_REQUEST['post_id']`,
  but the allowlist is the set of sha256 hashes of logic *stored in that post*.
  Passing requires a sha256 preimage of an admin's stored logic (infeasible) or
  supplying the admin's exact logic string (which is admin-controlled anyway).
  An empty/invalid `post_id` yields an empty allowlist -> every logic is zeroed.

## Variant / Alternate Trigger

Five candidate variants were tested (entry point + data path each):

| # | Variant | Entry point / data path | Result on 4.2.3 (vuln) | Result on 4.2.4 / 4.2.5 (fixed) |
|---|---------|-------------------------|------------------------|---------------------------------|
| A | Original CVE payload `$z='s'..'m'; ${'z'}('CMD'); return true;` | block-renderer REST, `attrs.extended_widget_opts_block.class.logic` | eval reached (RCE) | **Blocked at G1** (trust gate) |
| B | stripslashes-differential payload (validator `stripslashes()` vs raw `eval`) | same endpoint | eval reached | **Blocked at G1** |
| D | `${'z'}(...)` brace variable-call (the 4.2.3 token bypass) | same endpoint | eval reached (token `}` not blocked) | **Blocked at G1**; also **G2** now blocks `}` before `(` |
| E | `array_map($concat,['x'])` alternate dynamic-call shape | same endpoint | rejected by validator (`]` before `(`) | **Blocked at G1** |
| F | freeform `<!--start_widgetopts {"class":{"logic":"..."}} end_widgetopts-->` comment injection | saved post `innerContent` (alternate data path) | stripped at save (G4) | stripped at save (G4); not reachable via block-renderer (no innerContent); preview blocked (G5) |

Additional paths inspected and ruled out by trust-boundary / capability
analysis (not runtime-tested because they are admin-gated by design):
- **Snippets API** (`WidgetOpts_Snippets_API::execute_snippet` ->
  `widgetopts_safe_eval_trusted`): the `widgetopts_snippet` CPT is registered
  with `capability_type=>'post'` overridden by `capabilities` requiring
  `manage_options` for `edit_post`/`edit_posts`/`create_posts`/`delete_post`
  (`class-snippets-cpt.php` lines 76-86). A Contributor cannot create or edit
  snippets, so cannot control the trust-flagged code. Admin AJAX endpoints
  (`class-snippets-admin.php`) all check `manage_options`.
- **Classic widgets** (`includes/widgets/display.php:586`): widget instances
  live in `widget_<id>` options edited by users with `edit_theme_options`
  (administrators). Contributor cannot control.
- **Elementor / Beaver / SiteOrigin** render paths: page-builder content is
  edited by users with builder edit access (administrators/editors); the legacy
  `widgetopts_logic`/`widgetopts_settings_logic` keys are also scrubbed by G4
  on REST writes.

**Exact entry points:** `POST/GET /wp-json/wp/v2/block-renderer/<block>`
(A-E, same as parent), and saved post `post_content` freeform comments (F).

**Code paths involved:** `includes/extras.php` (`widgetopts_safe_eval`,
`widgetopts_safe_eval_trusted`, `widgetopts_eval_is_trusted`,
`widgetopts_validate_expression`, `widgetopts_validate_code_with_tokens`,
`widgetopts_rest_scrub_legacy_logic`); `includes/widgets/gutenberg/gutenberg-toolbar.php`
(`blockopts_filter_before_display` freeform extractor + legacy logic path,
`widgetopts_strip_logic_from_blocks`, `render_block_data` allowlist,
`rest_pre_dispatch`/`rest_post_dispatch` scope flag,
`wp_insert_post_data` stripper); `includes/snippets/class-snippets-cpt.php`
(CPT capabilities).

## Impact

- **Package/component affected:** `widget-options` plugin, `includes/extras.php`
  eval surface and `includes/widgets/gutenberg/gutenberg-toolbar.php` render
  path — **as tested on 4.2.3 (vulnerable), 4.2.4 (fixed), 4.2.5 (latest)**.
- **Affected versions:** <= 4.2.3 vulnerable (parent CVE). 4.2.4 fixed. 4.2.5
  (current) identical fix.
- **Risk level (this variant analysis):** No new exploitable path on fixed/latest.
  The alternate triggers (A/B/D) on 4.2.3 are the same critical Contributor RCE
  class as the parent (CVSS-class critical), already mitigated by upgrading to
  >= 4.2.4.

## Impact Parity

- **Disclosed/claimed maximum impact (parent):** Authenticated (Contributor+)
  Remote Code Execution — arbitrary server command execution + webroot PHP drop.
- **Reproduced impact from this variant run:** No impact on 4.2.4/4.2.5 —
  `eval()` is never reached for a contributor (G1 gate). On 4.2.3, variants
  A/B/D reach `eval()` (alternate triggers of the same RCE), confirming the
  candidates are real triggers of the underlying bug, just not a bypass.
- **Parity:** `none` (no bypass of the fix; the fix fully closes the
  contributor->eval trust boundary).
- **Not demonstrated:** Any Contributor code execution on 4.2.4 or 4.2.5.

## Root Cause

The parent root cause is that `widgetopts_safe_eval()` exposed PHP `eval()` of
user-supplied "Display Logic" to low-privilege users, with only a *preview-
context* guard and an incomplete blocklist/token-validator. The 4.2.4 fix
changes the **default** from "eval unless preview" to "**never eval for non-
admins unless a trusted wrapper vouches that the input is admin-controlled**".
That single closed-default inversion (G1) is what removes the trust-boundary
crossing: a Contributor's input can no longer reach `eval()` through any path,
because every path that *would* set the trust flag consumes input the
Contributor cannot author (DB-backed widget/snippet/builder settings requiring
`manage_options`/`edit_theme_options`). The validator hardening (G2), the
block-renderer allowlist (G3), the save/REST stripper (G4), and the preview
guard (G5) are defense-in-depth on top of G1; even if any one were bypassed in
isolation, G1 still blocks a contributor. No single upstream commit hash was
published with the advisory; the fix ships in plugin version 4.2.4
(`readme.txt`: "Strengthened security for Authenticated (Contributor+) Remote
Code Execution vulnerability") and is unchanged in 4.2.5.

## Reproduction Steps

1. **Script:** `bundle/vuln_variant/reproduction_steps.sh` (self-contained;
   run with `PRUVA_ROOT=<bundle> bash bundle/vuln_variant/reproduction_steps.sh`).
2. **What it does:**
   - Ensures the real plugin sources for 4.2.3, 4.2.4, 4.2.5 are extracted
     (from `bundle/artifacts/wo-versions/*.zip` if the extracted trees are
     missing).
   - Runs `bundle/vuln_variant/variant_harness.php` for each version. The
     harness **token-extracts the real** `widgetopts_safe_eval`,
     `widgetopts_validate_expression`, `widgetopts_validate_code_with_tokens`,
     `widgetopts_strip_logic_from_blocks` etc. from each version's actual
     `extras.php` / `gutenberg-toolbar.php`, stubs the minimal WordPress
     functions, simulates a **Contributor** (`current_user_can('manage_options')=false`,
     non-preview, no trust flag), and runs:
       * a **parse-error probe** (`@@@ $$$ not php (((;`) — returns `true` on
         4.2.4+ (G1 short-circuited before `eval`) vs `false` on 4.2.3
         (`eval()` ran and raised/caught a ParseError), proving whether `eval`
         is reached;
       * the candidate variant payloads A/B/D/E through `widgetopts_safe_eval`;
       * the G2 token-validator `}`-before-`(` check in isolation;
       * the G4 freeform-comment stripper on a
         `<!--start_widgetopts {"class":{"logic":"system('id');..."}} end_widgetopts-->`
         input.
   - Classifies each version as `eval_reached_for_contributor = True/False`,
     writes `bundle/logs/vuln_variant/variant_summary.log` and
     `bypass_flag.txt`, and exits **0 only if a variant reaches `eval` on the
     fixed/latest version** (a true bypass); otherwise exits **1** (negative
     result, script still ran fully).
3. **Expected evidence:** `eval_reached_for_contributor = True` on 4.2.3 and
   `= False` on 4.2.4 and 4.2.5; `bypass_flag.txt = NO_BYPASS`; all variant
   verdicts `BLOCKED_at_trust_gate_G1` on 4.2.4/4.2.5.

## Evidence

- `bundle/logs/vuln_variant/variant_summary.log` — full classification
  (vulnerable=True, fixed=False, latest=False; per-variant verdicts; G1/G2/G4
  gate states).
- `bundle/logs/vuln_variant/harness_4.2.3.log`,
  `bundle/logs/vuln_variant/harness_4.2.4.log`,
  `bundle/logs/vuln_variant/harness_4.2.5.log` — raw JSON harness output per
  version.
- `bundle/logs/vuln_variant/bypass_flag.txt` — `NO_BYPASS`.
- `bundle/vuln_variant/variant_harness.php` — the token-extraction harness
  (loads real plugin functions; not a mock).
- `bundle/vuln_variant/patch_analysis.md` — detailed fix/assumption analysis.
- `bundle/artifacts/wo-versions/ext-4.2.{3,4,5}/widget-options/` — extracted
  plugin sources tested.

Key excerpts (`variant_summary.log`):
```
vulnerable 4.2.3: eval_reached_for_contributor = True
fixed      4.2.4: eval_reached_for_contributor = False
latest     4.2.5: eval_reached_for_contributor = False
  4.2.3: G1=False G2_brace_blocked=False ... probe=false (eval_reached_caught_throwable (no G1 on vuln))
    variant A_original_cve: reached_eval_on_vuln (no G1)
    variant B_stripslashes_diff: reached_eval_on_vuln (no G1)
    variant D_brace_var_call: reached_eval_on_vuln (no G1)
  4.2.4: G1=True G2_brace_blocked=True ... probe=true (gate_blocked_G1 (eval never reached for contributor))
    variant A_original_cve: BLOCKED_at_trust_gate_G1
    variant B_stripslashes_diff: BLOCKED_at_trust_gate_G1
    variant D_brace_var_call: BLOCKED_at_trust_gate_G1
    variant E_concat_callable: BLOCKED_at_trust_gate_G1
  4.2.5: (identical to 4.2.4)
```
Environment: PHP 8.x CLI, Widget Options 4.2.3 / 4.2.4 / 4.2.5 extracted from
wordpress.org distribution zips; harness uses PHP `token_get_all` to extract the
real functions (no mocked logic).

## Recommendations / Next Steps

- **No fix gap identified.** The closed-default trust gate (G1) is the correct
  root-cause fix and fully closes the Contributor -> `eval()` trust boundary.
  The Coding agent should preserve G1 as the authoritative gate and treat
  G2-G5 as defense-in-depth, not as the primary control.
- **Defense in depth to retain/extend:**
  - Keep the `widgetopts_safe_eval_trusted()` allowlist of call sites tight;
    any *new* call site must prove its expression is admin-controlled. Audit
    that no future call site passes request-derived data into the trusted
    wrapper.
  - Consider making the freeform `<!--start_widgetopts-->` extractor
    `render_block_data`-aware (treat `innerContent` comments under the same
    sha256 allowlist as parsed `attrs`) so G3 also covers the freeform data
    path, removing reliance on G4 + reachability for that shape.
  - Prefer replacing `eval()` entirely with an allowlisted-operator expression
    engine (no `system`/`exec`/variable-variable calls possible by
    construction) rather than a blocklist + token validator.
  - Add regression tests: (1) a contributor cannot trigger
    `widgetopts_safe_eval` via block-renderer for any block; (2) the token
    validator rejects `${...}()`, `Foo::{'...'}`, closures, concatenation-built
    callables, and `T_FUNCTION`/`T_FN` IIFEs; (3) freeform comments carrying
    `class.logic` are stripped on non-admin save.
- **Hardening:** `disable_functions=system,exec,passthru,shell_exec,proc_open,popen`
  on the PHP SAPI; least-privilege web-server user; WAF rule flagging
  `extended_widget_opts_block` attributes and `start_widgetopts` comments
  carrying PHP-like logic in REST payloads.

## Additional Notes

- **Idempotency:** `reproduction_steps.sh` was run **twice consecutively**;
  both runs completed without crashing and exited 1 (NO_BYPASS) with identical
  classification. It only reads/extracts plugin zips and runs a read-only PHP
  harness; it does not mutate the repro clone or start services.
- **Bounded search justification:** Fewer than 3 *bypass* candidates exist
  because the fix's closed-default trust gate (G1) is **input-agnostic** — it
  blocks *every* payload shape for a contributor regardless of validator
  behaviour, so validator-bypass techniques (stripslashes differential,
  alternate dynamic-call shapes, alternate block names) are all moot on the
  fixed version by construction. The only class of candidate that could
  theoretically bypass G1 is one that sets the trust flag with contributor-
  controlled input; all six `widgetopts_safe_eval_trusted()` call sites were
  audited and each consumes admin-controlled/DB-backed input (capabilities
  `manage_options`/`edit_theme_options`), so no such candidate exists. The
  freeform-comment path is the one shape that sets the trust flag at render
  time, and it is closed by G4 (save stripper) + endpoint reachability + G5.
- **Version identity:** 4.2.5 (current/latest) is byte-identical to 4.2.4 on
  `includes/extras.php` and `includes/widgets/gutenberg/gutenberg-toolbar.php`
  (`cmp` equal); 4.2.5 changelog is solely a WordPress 7.0 compat bump. The
  4.2.4 analysis therefore covers the latest release.
- **Threat-model scope:** This is an authenticated Contributor crossing a
  server-side trust boundary (REST API -> server `eval()`), consistent with the
  parent CVE's scope and WordPress's security model (low-privilege authenticated
  RCE is a vulnerability). No out-of-scope "user attacks self with local input"
  framing is used.
