Skip to content

CVE-2026-54823: Verified Reproduction

CVE-2026-54823: Widget Options WordPress plugin contributor remote code execution

CVE-2026-54823 is verified against the affected target. Vulnerability class: RCE. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00230.

REPRO-2026-00230 RCE Jul 6, 2026 CVE entry .txt
Severity
CRITICAL
CVSS
9.9
Confidence
HIGH
Reproduced in
28m 45s
Tool calls
304
Spend
$6.36
01 · Overview

What Is CVE-2026-54823?

CVE-2026-54823 is a critical code-injection vulnerability (CWE-94) in the Widget Options WordPress plugin that lets a low-privileged Contributor account execute arbitrary code on the server. Pruva reproduced it (reproduction REPRO-2026-00230).

02 · Severity & CVSS

CVE-2026-54823 Severity & CVSS Score

CVE-2026-54823 is rated critical severity, with a CVSS base score of 9.9 out of 10.

CRITICAL threat level
9.9 / 10 CVSS base
Weakness CWE-94 (Improper Control of Generation of Code / Code Injection) — Improper Control of Generation of Code ('Code Injection')

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

How to Reproduce CVE-2026-54823

$ pruva-verify REPRO-2026-00230
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00230/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-54823

Remote code execution — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

Display Logic PHP expression supplied in extended_widget_opts_block.class.logic via the block-renderer REST endpoint attributes (no save step)

Attack chain
  1. POST /wp-json/wp/v2/block-renderer/core/archives with JSON body {attributes:{extended_widget_opts_block:{class:{logic:PAYLOAD}}}, context:'edit'} (context=edit in BODY only, NOT in query string) as an authenticated Contributor via WordPress Application Password
How the agent worked 701 events · 304 tool calls · 29 min
29 minDuration
304Tool calls
175Reasoning steps
701Events
Agent activity over 29 min
Support
28
Hypothesis
2
Repro
286
Judge
21
Variant
360
0:0028:45

Root Cause and Exploit Chain for CVE-2026-54823

Versions: Widget Options <= 4.2.3 (vulnerable). Fixed in

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/<block> 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.

  • 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('<cmd>') 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 (<?php echo 7*6;) into the webroot, which was then fetched over HTTP and returned the computed value 42 — proving the contributor both executed arbitrary commands and planted executable PHP in the webroot. The identical request against the fixed 4.2.4 build produced no proof artifacts (blocked).
  • Parity: full.
  • Not demonstrated: N/A — code execution parity is achieved (not merely a crash).

Root Cause

  1. Insufficient eval guard (preview-only). widgetopts_safe_eval() in 4.2.3 only short-circuits to "show without eval" for non-admins inside a preview context:

    if ( widgetopts_is_widget_or_post_preview() ) {
        if ( ! current_user_can('manage_options') ) { return true; }
    }
    

    widgetopts_is_widget_or_post_preview() returns true only for REST_REQUEST && $_GET['context'] === 'edit', is_preview(), ?preview=true, the Customizer, or Beaver/Elementor edit mode. On any non-preview render path the eval() runs for every user, including contributors and logged-out visitors.

  2. Block-renderer REST endpoint bypasses the save-time stripper. The /wp-json/wp/v2/block-renderer/<block> 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=<bundle> 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":"<PAYLOAD>"}}},"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 (<?php echo 7*6;).
  • bundle/repro/wo_rce_proof.txtPRUVA_RCE + id output produced by system().
  • bundle/logs/exploit_vuln.log — vulnerable-run exploit output (HTTP 200, PHP_PROOF_EXISTS True, TMP_PROOF_EXISTS True, id output).
  • bundle/logs/exploit_fixed.log — fixed-run exploit output (HTTP 200, PHP_PROOF_EXISTS False, TMP_PROOF_EXISTS False).
  • bundle/logs/reproduction_steps.log — full phase log.
  • bundle/repro/exploit_request.py — the exploit client (auth + payload + verification).
  • bundle/repro/validator_harness.php — local proof that the payload passes the real 4.2.3 widgetopts_validate_expression().
  • bundle/artifacts/wo-versions/ext-4.2.3 and ext-4.2.4 — extracted plugin sources used for the vulnerable/fixed comparison.

Key excerpts (vulnerable run, logs/exploit_vuln.log):

POST http://127.0.0.1:<port>/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)
<<TMP_PROOF_CONTENT

Fixed control (logs/exploit_fixed.log):

HTTP 200
PHP_PROOF_EXISTS False
TMP_PROOF_EXISTS False

Environment: WordPress 7.0, PHP 8.5, MariaDB 11.8, Widget Options 4.2.3 (vulnerable) / 4.2.4 (fixed), PHP built-in server + custom router, contributor authenticated via WordPress Application Password.

Recommendations / Next Steps

  • Upgrade immediately to Widget Options >= 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.

CVE-2026-54823 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:002:07
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-54823 · REPRO-20
0:02
0:03
web search
0:05
web search
0:07
0:08
0:10
web search
0:13
1:18
1:19
web search
1:21
1:29
1:32
1:34
web search
1:35
web search
1:39
1:41
1:53
1:53
extract_facts
no facts extracted
1:54
1:54
1:54
supportrepro
2:04
2:06
2:06
2:06
2:07
2:07
2:07
08 · How to Fix

How to Fix CVE-2026-54823

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-54823 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-54823

How does the CVE-2026-54823 contributor-to-RCE exploit work?

A Contributor (or any role with edit_posts) sends a block-renderer request whose Display Logic expression uses a variable-variable call built from string concatenation, e.g. $z='s'.'y'.'s'.'t'.'e'.'m'; ${'z'}('CMD'); return true;, which hides the word 'system' from the plugin's regex blocklist, and places a '}' before '(' — a token sequence the 4.2.3 validator did not block. Because the request carries context=edit in the body rather than the query string, the preview-context guard is bypassed and the expression reaches eval() as PHP with the web server's privileges, allowing an executable PHP file to be dropped into the webroot.

Which Widget Options versions are affected by CVE-2026-54823?

Widget Options - Advanced Conditional Visibility versions up to and including 4.2.3 are affected.

How severe is CVE-2026-54823?

It is rated critical severity: an authenticated Contributor-level account (or any role with edit_posts) can achieve arbitrary command execution as the web server user.

How can I reproduce CVE-2026-54823?

Download the verified script from this page and run it in an isolated environment against WordPress with Widget Options <= 4.2.3 installed. Using a Contributor account, it sends a crafted block-renderer request with a Display Logic payload disguised via variable-variable syntax and context=edit in the request body, and confirms server-side code execution on the vulnerable build.
11 · References

References for CVE-2026-54823

Authoritative sources for CVE-2026-54823 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.