# REPRO-2026-00230: Widget Options WordPress plugin contributor remote code execution ## Summary Status: published Severity: critical Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00230 CVE: CVE-2026-54823 ## Package Name: Unknown Ecosystem: Unknown Affected: Unknown Fixed: Unknown ## Root Cause # CVE-2026-54823 — Widget Options <= 4.2.3 — Contributor Remote Code Execution ## Summary The **Widget Options – Advanced Conditional Visibility** WordPress plugin (MarketingFire) versions up to and including **4.2.3** allow an authenticated **Contributor** (and any role with `edit_posts`) to execute arbitrary PHP/server code remotely. The plugin's *Display Logic* feature evaluates user-supplied PHP expressions through `widgetopts_safe_eval()` (which calls `eval()`). In 4.2.3 the only guard for non-administrators is a *preview-context* short-circuit (`widgetopts_is_widget_or_post_preview()`), which inspects `$_GET['context']`. The server-side block-rendering REST endpoint `/wp-json/wp/v2/block-renderer/` renders attacker-supplied block attributes with **no save step**, so the save-time logic stripper never runs; and because the endpoint accepts `context=edit` in the JSON **body** (not the query string), the plugin's `$_GET['context']` guard is bypassed and the contributor's Display Logic expression reaches `eval()`. The accompanying blocklist (regex) and token allowlist are defeated with a variable-variable call built from string concatenation: `$z='s'.'y'.'s'.'t'.'e'.'m'; ${'z'}('CMD'); return true;`, which hides the word `system` from the regex and places a `}` before `(` (a token the 4.2.3 validator did not block). This yields arbitrary command execution as the web server user, including dropping an executable PHP file into the webroot. ## Impact - **Package/component affected:** `widget-options` plugin, file `includes/extras.php` (`widgetopts_safe_eval()` / `widgetopts_validate_expression()` / `widgetopts_validate_code_with_tokens()`), reached via `includes/widgets/gutenberg/gutenberg-toolbar.php` (`blockopts_filter_before_display()` on the `render_block` filter) when the *Display Logic* module is active (`widgetopts_tabmodule-logic` / `widgetopts_settings['logic'] == 'activate'`). - **Affected versions:** Widget Options **<= 4.2.3** (vulnerable). Fixed in **4.2.4**. (Earlier Contributor+ RCE variants were CVE-2025-22630, fixed in 4.1.1, and CVE-2026-2052, partially patched in 4.2.0; CVE-2026-54823 is the block-renderer/`}`-token bypass that remained through 4.2.3.) - **Risk level:** Critical. A low-privileged authenticated user (Contributor, who cannot publish posts or manage options) obtains remote code execution as the WordPress/web-server user, leading to full site compromise, data exfiltration, and pivoting to the host. ## Impact Parity - **Disclosed/claimed maximum impact:** Authenticated (Contributor+) Remote Code Execution — "send a crafted request that executes arbitrary code on the server … writes or executes a PHP file." - **Reproduced impact from this run:** Full RCE demonstrated end-to-end through the real WordPress REST API surface. The contributor's single HTTP request executed `system('')` on the server, writing a marker file (`/tmp/wo_rce_proof.txt` containing `PRUVA_RCE` + `id` output, `uid=1000`) **and** dropping an executable PHP file `wp-content/wo_rce_proof.php` (`` route renders request-supplied block attributes server-side without persisting the post, so the `wp_insert_post_data` / `rest_pre_insert` logic strippers (which clear `extended_widget_opts[_block].class.logic` for non-admins on save) never execute. The contributor's malicious `class.logic` therefore reaches the `render_block` filter → `blockopts_filter_before_display()` → `widgetopts_safe_eval()`. 3. **`$_GET['context']` vs REST body context mismatch.** The block-renderer endpoint *requires* `context=edit`. Sending `context=edit` in the JSON **body** satisfies the REST controller (so it returns 200 and renders) while leaving `$_GET['context']` unset, so the plugin's preview guard in step 1 evaluates to false and eval proceeds for the contributor. 4. **Blocklist + token-allowlist bypass.** `widgetopts_validate_expression()` applies a regex blocklist (e.g. `/\b(eval|system|exec|...)\b/i`) and `widgetopts_validate_code_with_tokens()` enforces a function allowlist, blocking `T_STRING`/`T_VARIABLE`/`T_CONSTANT_ENCAPSED_STRING` and `]` or `)` immediately before `(`. It does **not** block `}` before `(`. The payload `$z = 's'.'y'.'s'.'t'.'e'.'m'; ${'z'}('CMD'); return true;`: - builds the function name `system` via concatenation so the literal word never appears (regex `\bsystem\b` does not match); - calls it through `${'z'}(...)`, i.e. a variable-variable call where the token before `(` is `}` — not blocked in 4.2.3; - contains the substring `return`, so `widgetopts_safe_eval()` skips its `return( ... );` wrapper and evaluates the raw multi-statement code. Net effect: `system('CMD')` executes as the contributor. This was verified against the **real** plugin validator extracted from 4.2.3 (`bundle/repro/validator_harness.php`): the payload returns `['valid' => true, 'message' => 'Expression is safe.']` while direct `system('id')` and `array_map('s'.'y'..., ...)` are correctly rejected. **Fix (4.2.4):** `includes/extras.php` introduces a closed-default trust flag — `if (!current_user_can('manage_options') && !widgetopts_eval_is_trusted()) return true;` at the top of `widgetopts_safe_eval()` — so non-admins get no eval unless a callsite wrapped in `widgetopts_safe_eval_trusted()` flagging DB-backed, admin-controlled logic. A `render_block_data` sha-256 allowlist (`widgetopts_get_post_logic_allowlist()`), scoped to the block-renderer route via `rest_pre_dispatch` (`_widgetopts_in_block_renderer`), zeroes any `class.logic` not actually stored in the post. A `rest_request_before_callbacks` scrub net clears legacy logic shapes on all non-admin REST writes. Finally, the token validator now also blocks `}` (catching `${$fn}()` / `Foo::{'m'}()`), `T_FUNCTION`, and `T_FN` before `(`. (See `bundle/artifacts/wo-versions/ext-4.2.4` vs `ext-4.2.3` diffs.) No single upstream commit hash was published with the advisory; the fix ships in plugin version 4.2.4. ## Reproduction Steps 1. **Script:** `bundle/repro/reproduction_steps.sh` (self-contained; run with `PRUVA_ROOT= bash bundle/repro/reproduction_steps.sh`). 2. **What it does:** - Installs PHP + MariaDB deps via apt, starts a dedicated MariaDB instance on a private socket. - Downloads WordPress core (7.0), wp-cli, and the plugin zips for **4.2.3** (vulnerable) and **4.2.4** (fixed) from wordpress.org. - Installs WordPress with `WP_ENVIRONMENT_TYPE=local` (enables Application Passwords over HTTP), enables pretty permalinks, and serves it via the PHP built-in server with a router so `/wp-json/*` routes to WordPress. - Installs the vulnerable plugin 4.2.3, activates the Display Logic module (`widgetopts_settings['logic'] = 'activate'`), creates a **Contributor** user and an Application Password for it. - As the contributor, sends the exploit: `POST /wp-json/wp/v2/block-renderer/core/archives` with JSON body `{"attributes":{"extended_widget_opts_block":{"class":{"logic":""}}},"context":"edit"}` (no `?context=edit` query param). The payload writes a marker file and a webroot PHP file via `system()`. - Confirms RCE: fetches the dropped PHP file over HTTP (returns `42`) and reads the marker file (contains `id` output). - Repeats the identical request against the **fixed** 4.2.4 build on a fresh server (to avoid PHP `require_once` staleness) and confirms **no** proof artifacts are produced. - Writes `bundle/repro/runtime_manifest.json` and exits 0 only if the vulnerable build is exploited AND the fixed build blocks it. 3. **Expected evidence of reproduction:** HTTP 200 on the exploit request; a dropped `wp-content/wo_rce_proof.php` that returns `42` over HTTP; a `/tmp/wo_rce_proof.txt` containing `PRUVA_RCE` + `id` output; and absence of both on the fixed 4.2.4 control. ## Evidence - `bundle/repro/runtime_manifest.json` — runtime manifest (api_remote, service started, healthcheck passed, target path reached, proof artifacts listed). - `bundle/repro/validation_verdict.json` — structured verdict (`confirmed`). - `bundle/repro/wo_rce_proof.php` — the PHP file the contributor dropped into the webroot (`/wp-json/wp/v2/block-renderer/core/archives (context=edit in BODY, none in query string) HTTP 200 PHP_PROOF_EXISTS True TMP_PROOF_EXISTS True TMP_PROOF_CONTENT>> PRUVA_RCE uid=1000(vscode) gid=1000(vscode) groups=1000(vscode),969(969) <= 4.2.4** (4.2.5 is current). - **Defense in depth:** restrict the block-renderer REST endpoint to roles that actually need server-side block preview; treat any Display Logic/eval feature as admin-only and never evaluate attacker-supplied expressions on non-admin render paths. Prefer a non-`eval` expression engine (allowlisted operators only) over a blocklist. - **Hardening:** disable `system`/`exec`/`passthru`/`shell_exec`/`proc_open` via `disable_functions` where possible; enforce least privilege for the web server user; use WAF rules to flag `extended_widget_opts_block` attributes carrying PHP-like logic in REST payloads. - **Testing:** add regression tests that (a) a contributor cannot trigger `widgetopts_safe_eval` via the block-renderer endpoint, and (b) the token validator rejects `${...}()`, `Foo::{'...'}`, closures, and concatenation-built callables. ## Additional Notes - **Idempotency:** The script was run **twice consecutively**, both runs exited 0 with identical results (4.2.3 exploited, 4.2.4 blocked). It cleans up its own MariaDB and PHP-server processes (SIGTERM then SIGKILL) and kills any stale mariadbd bound to its datadir before starting. - **require_once staleness note:** The vulnerable and fixed runs are served by **separate fresh PHP built-in server processes**; a single long-lived process would keep 4.2.3's `extras.php` loaded via `require_once` and mask the fixed build's behavior. opcache is off for the CLI SAPI and is not a factor. - **Surface match:** The claim's `claimed_surface` is `api_remote` and the reproduction proves the real WordPress REST API path end-to-end (no library harness), so `validated_surface = api_remote` and `claim_outcome = confirmed`. - **Auth:** Application Passwords are used because regular WordPress passwords are not accepted over HTTP Basic Auth; `WP_ENVIRONMENT_TYPE=local` permits app passwords over plain HTTP in this isolated reproduction environment. - **Bypass minimality:** The exploit uses only the contributor's own credentials and a single REST request — no admin interaction, no post publication, no shortcode/block insertion step required by the victim. ## Reproduction Details Reproduced: 2026-07-06T08:15:39.660Z Duration: 1725 seconds Tool calls: 304 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00230 pruva-verify CVE-2026-54823 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00230&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00230/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-54823 - Source: wordpress.org/plugin/widget-options (MarketingFire/Widget Options) ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 15682 bytes) - bundle/repro/rca_report.md (analysis, 13281 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 8010 bytes) - bundle/vuln_variant/rca_report.md (analysis, 18495 bytes) - bundle/repro/exploit_request.py (script, 1654 bytes) - bundle/artifact_promotion_manifest.json (other, 10582 bytes) - bundle/vuln_variant/variant_harness.php (other, 8837 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 2464 bytes) - bundle/vuln_variant/source_identity.json (other, 2076 bytes) - bundle/repro/wo_rce_proof.php (other, 16 bytes) - bundle/repro/wo_rce_proof.txt (other, 73 bytes) - bundle/repro/runtime_manifest.json (other, 684 bytes) - bundle/repro/validation_verdict.json (other, 982 bytes) - bundle/logs/exploit_vuln.log (log, 434 bytes) - bundle/logs/exploit_fixed.log (log, 323 bytes) - bundle/logs/vuln_variant/variant_summary.log (log, 2667 bytes) - bundle/logs/vuln_variant/bypass_flag.txt (other, 10 bytes) - bundle/vuln_variant/variant_manifest.json (other, 5014 bytes) - bundle/vuln_variant/validation_verdict.json (other, 3542 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 10631 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1927 bytes) - bundle/logs/vuln_variant/harness_4.2.4.log (log, 1211 bytes) - bundle/logs/vuln_variant/harness_4.2.3.log (log, 1089 bytes) - bundle/logs/vuln_variant/harness_4.2.5.log (log, 1211 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00230 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00230/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00230 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev