Skip to content

CVE-2026-54823: Verified Repro With Script Download

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 Variant found 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://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
Variants tested

Variant/bypass analysis of the Widget Options 4.2.4 fix for CVE-2026-54823 (Contributor RCE via block-renderer REST -> widgetopts_safe_eval -> eval). Five runtime candidate variants (original payload, stripslashes-differential, ${'z'}() brace-call, array_map concat callable, freeform <!--start_widgetopts--> comment in…

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.

Variant Analysis & Alternative Triggers for CVE-2026-54823

Versions: <= 4.2.3 vulnerable (parent CVE). 4.2.4 fixed. 4.2.5

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:

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).

  • 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.txtNO_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.

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.