# CVE-2026-57624 — Variant Root Cause Analysis

## Summary

A distinct **alternate trigger** for the CVE-2026-57624 `eval()` sink was
found and empirically confirmed on the **patched (fixed) build**: an
unauthenticated `GET` to a published page that embeds a Blocksy content block
(via the `[blocksy-content-block id=..]` shortcode) — or any frontend render
of a content block configured as a **hook** or **inline popup** — causes the
`blocksy-companion-pro/code-editor` block's `render_callback` to execute
`eval()` on the block content via `get_eval_content()`, with command execution
as `www-data`. This path survives the 2.1.47+/2.1.48 fix because the fix's
`ct_allow_code_editor` guard deliberately **permits** execution inside the
legitimate `ContentBlocksRenderer` (the "explicit renderer context"), and
`is_admin()` is `false` on a real frontend page load. **This is intended
post-fix behaviour per the vendor changelog ("restrict execution to explicit
renderer context"), so it is NOT a standalone security bypass of
CVE-2026-57624.** It is, however, a residual unauthenticated eval-triggering
surface that the fix does **not** cover; combined with the separate
unauthenticated-injection CVE-2026-57630 (companion IDOR), unauthenticated RCE
persists on patched builds. A second candidate — the unauthenticated, nonce-less
AJAX endpoints `blc_retrieve_popup_content` / `blc_retrieve_mega_menu_content`
— reaches the same sink via `ContentBlocksRenderer` but is blocked by the
fix's `is_admin()` guard (admin-ajax ⇒ `is_admin()` true), so it is **not** a
bypass. The original `do_blocks` single-view surface is correctly blocked by
the fix (negative control re-confirmed).

## Fix Coverage / Assumptions

- **Invariant the fix relies on:** the `ct_allow_code_editor` flag is set
  *only* inside `CustomPostTypeRenderer::mark_code_editor_blocks()` (via the
  self-removing `blocksy:block-parser:result` filter), which is reachable
  *only* through `ContentBlocksRenderer` (content-block rendering). Standard
  WP `do_blocks()` (e.g. a `ct_content_block` single-view) never sets the
  flag. The fix additionally assumes AJAX contexts are non-trusted and gates
  them with `is_admin()`.
- **Code paths the fix explicitly covers:**
  1. `do_blocks()` single-view of a `ct_content_block` (the repro surface) →
     flag absent → guard B returns `''`. **Confirmed blocked (TEST2).**
  2. AJAX-context rendering (`admin-ajax.php` nopriv popup/mega-menu
     endpoints) → `is_admin()` true → guard A returns `''`. **Confirmed
     blocked (TEST3).**
  3. REST block renderer → already `edit_posts`-gated (not a vector).
- **What the fix does NOT cover:** frontend `ContentBlocksRenderer` rendering
  of content blocks (shortcode, hook, inline popup, inline mega-menu walker,
  single/archive templates). There `is_admin()` is false and the flag is set,
  so `eval()` runs. This is **intended** (the renderer is the explicit context
  the fix carves out as allowed) but it is an unauthenticated request path
  that reaches the sink.

## Variant / Alternate Trigger

**Primary alternate trigger (confirmed on FIXED):** *ContentBlocksRenderer
frontend rendering of a content block containing a code-editor block.*

- **Entry point (unauthenticated):** `GET /<page-slug>/` where the page
  `post_content` contains `[blocksy-content-block id=<CB>]`, or any frontend
  request that causes `ContentBlocks::display_hooks()` / `render_popup()` /
  the mega-menu walker to render a content block. No cookies/credentials.
- **Code path:**
  `ContentBlocks` shortcode (`content-blocks.php`) →
  `is_hook_eligible_for_display($id, ['match_conditions'=>false])` →
  `output_hook($id)` → `new ContentBlocksRenderer($id)` →
  `get_content()` → `CustomPostTypeRenderer::get_content()` →
  `parse_blocks_with_code_editor_mark()` (sets `ct_allow_code_editor=true` on
  every `blocksy-companion-pro/code-editor` parsed block) →
  `render_block($block)` → `CodeEditor` render_callback → guard A
  (`is_admin()` false ⇒ pass) → guard B (flag set ⇒ pass) →
  `get_eval_content($inline_code)` → `eval('?>'.$inline_code.$ending)`.
- **Same sink as the parent CVE:** `Blocksy\CodeEditor::get_eval_content()`
  in `framework/premium/features/code-editor.php` (line ~104/119).
- **Equivalent frontend paths (same mechanism):** inline popup
  (`popups.php::render_popup` → `output_hook`, `load_content_with_ajax=no`);
  hook content blocks (`content-blocks.php::display_hooks` →
  `output_hook`); inline mega-menu walker; single/archive content-block
  templates (`templates.php` line 169 `new ContentBlocksRenderer`).

**Secondary candidate (blocked on FIXED — not a bypass):** the unauthenticated
nonce-less AJAX endpoints `wp_ajax_nopriv_blc_retrieve_popup_content`
(`popups.php:44`) and `wp_ajax_nopriv_blc_retrieve_mega_menu_content`
(`mega-menu/includes/api.php:17`). Both call `ContentBlocksRenderer` (flag
set) but run under `admin-ajax.php` ⇒ `is_admin()` true ⇒ guard A blocks.
Confirmed blocked on both builds (TEST3). On a hypothetical fully-vulnerable
`<=2.1.46` with **both** guards absent these would be additional
unauthenticated alternate triggers; that older commercial build is not
downloadable, so it could not be tested directly.

**Negative control (re-confirmed):** `do_blocks` single-view
(`GET /ct_content_block/<slug>/`) — blocked on FIXED (guard B), executes on
vuln (TEST2).

## Impact

- **Package/component affected:** `blocksy-companion-pro` plugin,
  `Blocksy\CodeEditor` (`framework/premium/features/code-editor.php`),
  reachable via `Blocksy\ContentBlocksRenderer`
  (`framework/premium/features/content-blocks/renderer.php` +
  `content-blocks.php` + `content-blocks/popups.php`).
- **Affected versions (as tested):** Blocksy Companion Pro **2.1.48**
  (fixed build = original; vulnerable build = 2.1.48 with the
  `ct_allow_code_editor` guard reverted). Blocksy theme 2.1.48. WordPress 7.0.
  The alternate trigger reproduces on the fixed 2.1.48 build.
- **Risk level:** Critical when combined with CVE-2026-57630. In isolation the
  alternate trigger is intended behaviour (admin-authored code executed for
  visitors). The exploitable impact (unauthenticated attacker-controlled
  code) requires the separate IDOR for content injection.
- **Consequences:** unauthenticated remote PHP/command execution as `www-data`
  (demonstrated: `uid=33(www-data)`), when an attacker can place a code-editor
  block in a content block (via 57630) rendered as a popup/hook/shortcode.

## Impact Parity

- **Disclosed/claimed maximum impact (parent CVE):** Unauthenticated Remote
  Code Execution (Patchstack: "Required privilege: Unauthenticated",
  CVSS 10.0, CWE-94).
- **Reproduced impact from this variant run:** Unauthenticated command
  execution (`shell_exec("id; whoami; hostname")` → `uid=33(www-data)`,
  `www-data`, container hostname) on the **fixed/patched** build via an
  unauthenticated `GET` to a page embedding a content block (TEST1). The
  command output appears in the HTTP body (`BLOCKSY_VARIANT_RCE::uid=33(...)`)
  and is persisted to `/tmp/rce_marker_variant.txt` by `www-data`.
- **Parity:** `partial`. The unauthenticated eval-execution primitive is
  reproduced on the patched build via a different entry point, achieving the
  same code-execution impact. The attacker **content control** (placing the
  code-editor block) is not reproduced here in isolation — it depends on the
  separate CVE-2026-57630 IDOR, which is out of scope for this variant stage.
  Therefore parity is `partial` (execution primitive: full; end-to-end
  unauthenticated injection: not demonstrated, depends on 57630).
- **Not demonstrated:** unauthenticated *injection* of the code-editor block
  content (that is CVE-2026-57630). The variant stage demonstrates the
  *execution* surface that survives the 57624 fix.

## Root Cause

The same underlying bug — `get_eval_content()` running `eval()` on the
code-editor block's innerHTML/code attribute — is still reachable because the
fix chose to **allow** execution inside `ContentBlocksRenderer` (the explicit
renderer context). The fix's guard B
(`empty($block->parsed_block['ct_allow_code_editor'])`) is *satisfied* on the
frontend `ContentBlocksRenderer` path (the flag is set by
`mark_code_editor_blocks()`), and guard A (`is_admin()`) is *not* satisfied
(`is_admin()` is false for a real frontend page load). Consequently every
frontend content-block render that flows through `ContentBlocksRenderer`
executes any code-editor block it contains — including renders triggered by
unauthenticated visitors. The fix closes the `do_blocks` single-view path
(flag never set there) and the AJAX path (`is_admin()` true), but not the
frontend `ContentBlocksRenderer` path, because closing it would disable the
code-editor block's documented feature.

No fix commit SHA is available (the plugin is distributed as a commercial
freemius/GPL-mirror zip, not a public VCS). The fixed reference is Blocksy
Companion Pro 2.1.48 (`blocksy-companion-pro-v2.1.48.zip`,
sha256 `df44244019dcc7f0fd93ac039dacfc1b8caf821b8f8839b8efef94e47d34062a`);
in-container fixed `code-editor.php` sha256
`7e5ccb97ab2559f8583cd97be5df1e87a64493f87a6a7e8822e54c9b3e0a8c2b`.

## Reproduction Steps

1. **Script:** `bundle/vuln_variant/reproduction_steps.sh` (self-contained,
   idempotent; reuses the running repro Docker stack if present, otherwise
   deploys its own `blocksycvevar` stack with vulnerable + fixed builds).
2. **What it does:**
   1. Ensures a vulnerable build (2.1.48 with guard B reverted) and a fixed
      build (original 2.1.48) of Blocksy Companion Pro are running on
      WordPress 7.0 + Blocksy theme 2.1.48, each in its own container.
   2. Runs `bundle/vuln_variant/setup_variant.php` (idempotent) in each
      container to create: a `ct_content_block` containing a code-editor block
      whose payload runs `shell_exec("id; whoami; hostname")` and writes
      `/tmp/rce_marker_variant.txt`; a published page embedding it via
      `[blocksy-content-block id=..]`; a popup content block
      (`template_type=popup`, `load_content_with_ajax=yes`); and a
      `ct_content_block` for the single-view test.
   3. Sends **unauthenticated** requests for three paths on **both** builds:
      - **TEST1** `GET /<shortcode-page>/` (ContentBlocksRenderer frontend)
      - **TEST2** `GET /ct_content_block/<slug>/` (do_blocks single-view)
      - **TEST3** `POST /wp-admin/admin-ajax.php action=blc_retrieve_popup_content`
        (nopriv AJAX)
   4. Checks the marker file + `BLOCKSY_VARIANT_RCE` in each HTTP response and
      prints the differential matrix. Writes
      `bundle/vuln_variant/runtime_manifest.json`.
3. **Expected evidence:**
   - TEST1: marker written + `BLOCKSY_VARIANT_RCE::uid=33(www-data)...` in the
     response on **both** builds (alternate trigger survives the fix).
   - TEST2: marker on vuln only; **no** marker on fixed (fix blocks
     single-view).
   - TEST3: **no** marker on either build (`is_admin()` guard blocks AJAX).
   - Exit 0 when `TEST1_fixed=executed ∧ TEST2_fixed=blocked ∧ TEST3_fixed=blocked`
     (the fix boundary is characterised).

## Evidence

- `bundle/logs/vuln_variant/reproduction_steps.log` — full transcript (both
  verification runs).
- `bundle/logs/vuln_variant/t1_fixed_marker.txt` — FIXED TEST1 marker:
  ```
  VARIANT_RCE_CONFIRMED
  CMD_OUTPUT:
  uid=33(www-data) gid=33(www-data) groups=33(www-data)
  www-data
  7737f7e10e48
  ```
  (proof: `eval()` executed on the patched build via an unauthenticated page
  load through `ContentBlocksRenderer`).
- `bundle/logs/vuln_variant/t1_vuln_marker.txt` — vuln TEST1 marker (same).
- `bundle/logs/vuln_variant/t2_fixed_marker.txt` — `NO_MARKER` (fix blocks
  single-view on FIXED).
- `bundle/logs/vuln_variant/t2_vuln_marker.txt` — marker present (vuln
  single-view executes).
- `bundle/logs/vuln_variant/t3_fixed_marker.txt` / `t3_vuln_marker.txt` —
  `NO_MARKER` on both (`is_admin()` guard blocks AJAX popup rendering).
- `bundle/vuln_variant/http/fixed_shortcode_page.html` — contains
  `BLOCKSY_VARIANT_RCE::uid=33(www-data) gid=33(www-data) groups=33(www-data)`.
- `bundle/vuln_variant/http/fixed_singleview.html` — zero
  `BLOCKSY_VARIANT_RCE` occurrences.
- `bundle/vuln_variant/http/fixed_popup_ajax.json` / `vuln_popup_ajax.json` —
  zero `BLOCKSY_VARIANT_RCE` occurrences.
- `bundle/vuln_variant/http/vuln_singleview.html` / `vuln_shortcode_page.html`
  — contain `BLOCKSY_VARIANT_RCE::...`.
- `bundle/vuln_variant/runtime_manifest.json` — structured runtime evidence
  (`entrypoint_kind=api_remote`, `target_path_reached=true`, full test matrix,
  `fixed_version_alternate_trigger_confirmed=true`,
  `bypass_in_isolation=false`,
  `classification=alternate_trigger_on_fixed_intended_behaviour`).

**Environment:** Docker; `mariadb:10.11`; `wordpress:php8.2-apache`
(WordPress 7.0); Blocksy theme 2.1.48; Blocksy Companion Pro 2.1.48 (fixed =
original; vulnerable = guard B reverted). Unauthenticated requests issued
from the host to the container IPs (no WordPress auth cookies).

## Recommendations / Next Steps

The 57624 fix is sound for its stated scope but **does not, by itself,
remediate the unauthenticated-RCE impact** because the frontend
`ContentBlocksRenderer` path remains an unauthenticated eval-triggering
surface. To close the gap:

1. **Remediate CVE-2026-57630 (companion IDOR)** — the realistic unauthenticated
   injection path for code-editor block content. Without it, an attacker
   cannot place the code-editor block, so the residual surface is not
   attacker-reachable.
2. **Defence-in-depth at the eval boundary:** in addition to the context guard,
   re-check authoring capability *at eval time* — e.g. only execute a
   code-editor block when the owning content block was last edited by a user
   who still holds `manage_options` / the `custom_post_type` capability, or
   gate the whole `ct_content_block` post type's code-editor execution behind
   an explicit, audit-logged opt-in. This prevents an IDOR-injected (or
   formerly-privileged-then-revoked) code-editor block from executing even via
   the legitimate renderer.
3. **Consider disabling the code-editor block in content blocks by default**
   on sites that do not need server-side PHP execution, exposing it only as an
   explicit, high-privilege feature.
4. **Add an automated test** that asserts a code-editor block inside a content
   block rendered through `ContentBlocksRenderer` on the frontend executes
   *only* when the authoring capability check passes — and that an
   IDOR-equivalent injection (content block authored by a disallowed actor)
   does **not** execute.
5. **WAF / interim mitigation:** block unauthenticated requests to
   `/ct_content_block/<slug>/` and to `admin-ajax.php` actions
   `blc_retrieve_popup_content` / `blc_retrieve_mega_menu_content` when the
   referenced content block contains a code-editor block, until both 57624 and
   57630 are patched site-wide.

## Additional Notes

- **Idempotency:** `reproduction_steps.sh` was executed twice consecutively;
  both runs ended with `exit 0` and the identical differential matrix
  (`TEST1: vuln=1 fixed=1`, `TEST2: vuln=1 fixed=0`, `TEST3: vuln=0 fixed=0`).
  `setup_variant.php` deletes prior variant posts by title before recreating
  them, so the script can be re-run safely.
- **Classification honesty:** the fixed-version execution (TEST1) is **intended
  behaviour** per the vendor's documented fix design. It is recorded as a
  confirmed alternate trigger that survives the fix, **not** as a standalone
  security bypass of CVE-2026-57624. The exploitable residual risk is gated by
  the separate CVE-2026-57630 (IDOR), which is the realistic unauthenticated
  injection path and is out of scope for this variant stage.
- **Limitation:** the real `<=2.1.46` commercial build is not downloadable, so
  the differential vulnerable build was constructed by reverting guard B from
  the real 2.1.48 (guard A retained). A fully-vulnerable build with both guards
  absent would additionally expose the popup/mega-menu nopriv AJAX endpoints
  as unauthenticated alternate triggers; this could not be tested directly.
- **No other eval sinks:** the only `eval()` of attacker-influenced block
  content is `get_eval_content()`. The `qubely.php` `eval()` is a fixed class
  string. The "Custom Code Snippets" extension is a separate admin-gated
  feature (out of 57624 scope). No `register_rest_route` renders content
  blocks.
