# CVE-2026-57624 — Blocksy Companion Pro Unauthenticated Remote Code Execution

## Summary

Blocksy Companion Pro (by Creative Themes) ships a premium "Code Editor"
Gutenberg block (`blocksy-companion-pro/code-editor`) registered in
`framework/premium/features/code-editor.php`. The block's `render_callback`
extracts PHP source from the block innerHTML (or the `code` attribute) and
passes it to `get_eval_content()`, which executes it with `eval('?>' . $code)`.
In versions <= 2.1.46 the callback performed **no execution-context check**, so
the block executed arbitrary PHP whenever it was rendered through the standard
WordPress block renderer (`do_blocks()`) — for example when an unauthenticated
visitor requested the single-view URL of a published `ct_content_block` whose
`post_content` contained a code-editor block. This yields unauthenticated
remote code execution (CVSS 10.0, CWE-94). The vendor fix in 2.1.47 adds a
guard that only allows execution when the block is rendered through the
legitimate Blocksy content-block renderer (which sets an explicit
`ct_allow_code_editor` flag); the standard `do_blocks` path no longer executes
the code.

## Impact

- **Package / component affected:** `blocksy-companion-pro` plugin,
  `Blocksy\CodeEditor` class in `framework/premium/features/code-editor.php`
  (the `blocksy-companion-pro/code-editor` Gutenberg block render callback).
- **Affected versions:** Blocksy Companion Pro <= 2.1.46 (fixed in 2.1.47).
- **Risk level:** Critical (CVSS 3.1 10.0, `AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H`).
- **Consequences:** An unauthenticated remote attacker can execute arbitrary PHP
  — and therefore arbitrary operating-system commands — in the context of the
  web server (`www-data`), leading to full site compromise, data exfiltration,
  and lateral movement on the host.

## Impact Parity

- **Disclosed / claimed maximum impact:** Unauthenticated Remote Code Execution
  (Patchstack: "Required privilege: Unauthenticated", CVSS 10.0, CWE-94).
- **Reproduced impact from this run:** Unauthenticated remote code execution
  confirmed end-to-end. An HTTP GET with no cookies/credentials to a published
  content-block single-view URL caused the code-editor block's `render_callback`
  to `eval()` attacker-controlled PHP that ran `shell_exec("id; whoami; hostname;
  uname -a")`. The command output (`uid=33(www-data) ...`, hostname, kernel
  string) appeared in the HTTP response and was written to a marker file by the
  `www-data` user inside the WordPress container.
- **Parity:** `full` — the claimed unauthenticated code execution was
  demonstrated with concrete command output, plus a fixed-version negative
  control proving the patch blocks it.
- **Not demonstrated:** n/a (code execution was demonstrated, not merely a
  crash). The only element not reproduced in isolation is the *unauthenticated
  injection* of the code-editor block content into a content block; that is the
  companion IDOR CVE-2026-57630 (a separate, concurrently-disclosed CVE). The
  CVE-2026-57624 RCE primitive — unauthenticated `eval()` execution of the
  block content via the standard `do_blocks` path — is fully reproduced.

## Root Cause

`CodeEditor::__construct()` registers the block with a `render_callback` that
calls `get_eval_content()`:

```php
register_block_type('blocksy-companion-pro/code-editor', [
    'api_version' => 3,
    'render_callback' => function ($attributes, $content, $block) {
        // (vulnerable <=2.1.46: NO context guard here)
        if (! empty($content)) {
            $inline_code = str_replace('<pre class="wp-block-code"><code>', '',
                              str_replace('</code></pre>', '',
                                html_entity_decode(htmlspecialchars_decode($content))));
            return $this->get_eval_content($inline_code);
        }
        if (empty($attributes['code'])) return '';
        return $this->get_eval_content($attributes['code']);
    }
]);
```

`get_eval_content()` runs `eval('?' . '>' . $inline_code . $ending)` — i.e. it
executes the block's innerHTML as PHP. Because there was no check on *where* the
block was being rendered, the code ran in every rendering context, including the
plain `do_blocks()` single-post rendering that an unauthenticated visitor
triggers by requesting the content block's URL.

The **fix** (2.1.47, present in 2.1.48) inserts two guards at the top of the
callback:

```php
if (is_admin()) { return ''; }
if (empty($block->parsed_block['ct_allow_code_editor'])) { return ''; }
```

The `ct_allow_code_editor` flag is set **only** by
`CustomPostTypeRenderer::mark_code_editor_blocks()` (in
`framework/premium/features/render-custom-post-type.php`), which runs via the
`blocksy:block-parser:result` filter during the legitimate Blocksy content-block
renderer. The standard WordPress `do_blocks()` path used for a content block's
own single-view URL does **not** apply that filter, so `ct_allow_code_editor` is
absent there and the patched callback returns `''` instead of `eval()`-ing. This
matches the vendor changelog entries "Code editor block - restrict execution to
explicit renderer context" and "Code Editor block - restrict post types for
which it can be executed".

### Why the REST block-renderer is NOT the vector

The WordPress core `WP_REST_Block_Renderer_Controller::get_item_permissions_check()`
has required `edit_posts` (or `edit_post` for a supplied `post_id`) since WP 5.0;
an unauthenticated caller receives HTTP 401 `block_cannot_read`. This was
verified empirically on the deployed WordPress 7.0. Additionally the code-editor
block registers no `attributes` schema, so the REST controller rejects a `code`
attribute (`rest_additional_properties_forbidden`). The unauthenticated vector is
therefore the `do_blocks()` single-view path, not the REST block-renderer.

## Reproduction Steps

1. **Script:** `bundle/repro/reproduction_steps.sh` (self-contained, idempotent).
2. **What it does:**
   1. Bundles the real Blocksy Companion Pro v2.1.48 plugin (GPL-mirror copy,
      pre-activated so the `Premium`/`CodeEditor` classes load without a
      license) and the free Blocksy theme.
   2. Builds **two** plugin zips from the same real code:
      - **vulnerable** — `framework/premium/features/code-editor.php` with the
        CVE-2026-57624 `ct_allow_code_editor` execution-context guard removed
        (restoring the <=2.1.46 render-callback state);
      - **fixed** — original 2.1.48 (guard present).
      A build-sanity grep confirms the guard is present in the fixed zip and
      absent in the vulnerable zip.
   3. Deploys a Docker stack: MariaDB + two `wordpress:php8.2-apache`
      (WordPress 7.0) instances — one with the vulnerable plugin, one with the
      fixed plugin — plus the Blocksy theme on each.
   4. Creates a published `ct_content_block` on each instance whose
      `post_content` is a `blocksy-companion-pro/code-editor` block whose PHP
      payload runs `shell_exec("id; whoami; hostname; uname -a")` and writes a
      marker file. *(Setup step; in a real attack this block content is injected
      unauthenticated via the companion IDOR CVE-2026-57630.)*
   5. Sends an **unauthenticated** HTTP GET (no cookies/credentials) to the
      content block's single-view URL on each instance.
   6. Verifies the marker file + command output on the vulnerable instance and
      confirms the fixed instance produces no marker and no code output.
   7. Writes `bundle/repro/runtime_manifest.json`.
3. **Expected evidence of reproduction:**
   - Vulnerable: marker file `/tmp/rce_marker.txt` written by `www-data`
     containing `uid=33(www-data) ...`, plus `BLOCKSY_RCE_EXECUTED::uid=33(...)`
     in the HTTP response body.
   - Fixed: `NO_MARKER_FILE`, zero `BLOCKSY_RCE_EXECUTED` occurrences in the
     response (the `ct_allow_code_editor` guard returns `''`).

## Evidence

- `bundle/logs/reproduction_steps.log` — full run transcript (both passes).
- `bundle/logs/vuln_marker.txt` — command output captured from the vulnerable
  container:
  ```
  RCE_CONFIRMED
  CMD_OUTPUT:
  uid=33(www-data) gid=33(www-data) groups=33(www-data)
  www-data
  <container hostname>
  Linux <hostname> 7.0.14-arch1-1 ... x86_64 GNU/Linux
  ```
- `bundle/logs/fixed_marker_check.txt` — `NO_MARKER_FILE` (fixed build blocks).
- `bundle/logs/code-editor-fix-diff.txt` — unified diff of the exact guard
  reverted (`if (empty($block->parsed_block['ct_allow_code_editor'])) { return ''; }`
  removed).
- `bundle/artifacts/http/vuln_response.html` — HTTP body containing
  `BLOCKSY_RCE_EXECUTED::uid=33(www-data) gid=33(www-data) groups=33(www-data)`.
- `bundle/artifacts/http/fixed_response.html` — HTTP body with zero
  `BLOCKSY_RCE_EXECUTED` occurrences.
- `bundle/artifacts/code-editor.vuln.php` / `code-editor.fixed.php` — the
  vulnerable and fixed `CodeEditor` source as deployed.
- `bundle/repro/runtime_manifest.json` — runtime evidence manifest
  (`entrypoint_kind=api_remote`, `service_started=true`,
  `healthcheck_passed=true`, `target_path_reached=true`).

**Environment:** Docker; `mariadb:10.11`; `wordpress:php8.2-apache`
(WordPress 7.0); Blocksy theme 2.1.48; Blocksy Companion Pro 2.1.48 (vulnerable
build = guard reverted; fixed build = original). The unauthenticated requests
were issued from a separate container on the compose network (no WordPress auth
cookies), i.e. a genuine logged-out HTTP client.

## Recommendations / Next Steps

- **Upgrade:** Update Blocksy Companion Pro to 2.1.47 or later (the
  `ct_allow_code_editor` execution-context guard). The reproduction shows the
  patched build blocks the `do_blocks` execution path.
- **Defense in depth:** In addition to the context guard, treat the code-editor
  block as a high-risk primitive: restrict the `ct_content_block` post type to
  `manage_options` for create/edit (already the case), and consider disabling the
  code-editor block entirely on sites that do not need server-side PHP execution.
- **WAF / 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
  content block contains a code-editor block, until all sites are patched.
- **Companion IDOR:** Also remediate CVE-2026-57630 (unauthenticated IDOR) which
  is the realistic unauthenticated injection path for the code-editor block
  content; the two CVEs combine into a single-request unauthenticated RCE on a
  vulnerable+unpatched-IDOR site.
- **Testing:** Add an automated test that renders a code-editor block via the
  standard `do_blocks()` path (not the `ContentBlocksRenderer`) and asserts the
  output is empty on patched builds.

## Additional Notes

- **Idempotency:** The script tears down its compose stack (`down -v`) at start
  and rebuilds from the bundled artifacts, so it can be run repeatedly. It was
  executed twice consecutively; both runs ended with `exit 0` and the same
  vulnerable/fixed differential.
- **Vulnerable-source caveat:** The vulnerable <=2.1.46 plugin build is not
  downloadable (the commercial Pro plugin is distributed only via freemius and
  GPL mirrors keep only the latest release). The reproduction therefore builds
  the vulnerable variant from the **real** 2.1.48 plugin by reverting the exact
  CVE-2026-57624 security guard (`ct_allow_code_editor` context check), which
  restores the pre-2.1.47 render-callback state. The `eval()` mechanism, block
  registration, attribute/innerHTML handling and `get_eval_content()` are all
  the unmodified vendor code; only the 2.1.47 guard is removed. The fixed
  variant (original 2.1.48) serves as the negative control.
- **Licensing:** The GPL-mirror 2.1.48 copy is pre-activated
  (`Capabilities::get_plan()` returns `agency_v2` without a license), so the
  `Premium`/`CodeEditor` block registers and the `blocksy-companion-pro/code-editor`
  block is available without a valid freemius key — matching a real licensed
  deployment where an admin has activated Pro.
- **Scope:** The demonstrated surface is the unauthenticated `do_blocks` HTTP
  path (`api_remote`), matching the ticket's `claimed_surface=api_remote`. The
  attacker-controlled code injection itself is CVE-2026-57630 (separate), noted
  for completeness; the CVE-2026-57624 RCE primitive (unauthenticated `eval`
  execution) is fully reproduced and differentially validated against the patch.
