# CVE-2026-57624 — Patch Analysis (Variant Stage)

## 1. The fix under analysis

The CVE-2026-57624 fix is the two-guard change at the top of the
`blocksy-companion-pro/code-editor` Gutenberg block `render_callback` in
`framework/premium/features/code-editor.php` (`Blocksy\CodeEditor`). In the
shipped 2.1.48 (patched) build the callback reads:

```php
register_block_type('blocksy-companion-pro/code-editor', [
    'api_version' => 3,
    'render_callback' => function ($attributes, $content, $block) {
        if (is_admin()) {                                       // guard A
            return '';
        }

        if (empty($block->parsed_block['ct_allow_code_editor'])) { // guard B
            return '';
        }

        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);       // SINK
        }
        if (empty($attributes['code'])) { return ''; }
        return $this->get_eval_content($attributes['code']);    // SINK
    }
]);
```

`get_eval_content()` executes the supplied source with
`eval('?' . '>' . $inline_code . $ending)` — i.e. the block innerHTML / `code`
attribute is executed as PHP.

The vulnerable build used for differential testing is the **same real 2.1.48
code with guard B removed** (`if (empty($block->parsed_block['ct_allow_code_editor'])) { return ''; }`
reverted), restoring the pre-fix render-callback behaviour. Guard A (`is_admin()`)
is retained in the vulnerable build (see §4). The fixed build is the original
2.1.48 zip (guard A + guard B present). Build sanity:
`sha256sum` of the in-container fixed `code-editor.php` =
`7e5ccb97ab2559f8583cd97be5df1e87a64493f87a6a7e8822e54c9b3e0a8c2b`; the
vulnerable build has zero matches for `parsed_block['ct_allow_code_editor']`.

> Changelog note: `2.1.39 — Fix: Code editor block - restrict execution to
> explicit renderer context` (guard B) and `2.1.38 — Improvement: Code Editor
> block - restrict post types for which it can be executed`. The CVE disclosure
> marks `<= 2.1.46` as vulnerable; the patched reference build used here is
> 2.1.48 (guard present). The CVE/repro RCA attributes the fix to 2.1.47; the
> exact version boundary is not material to the variant analysis — what matters
> is guard A + guard B being present (fixed) vs guard B absent (vulnerable).

## 2. Where the `ct_allow_code_editor` flag is set

Only one place sets the flag:

`framework/premium/features/render-custom-post-type.php`
(`CustomPostTypeRenderer`):

```php
private function parse_blocks_with_code_editor_mark($content) {
    add_filter('blocksy:block-parser:result',
        [$this, 'mark_code_editor_blocks_and_unhook']);
    return parse_blocks($content);
}
private function mark_code_editor_blocks($blocks) {
    foreach ($blocks as &$block) {
        if ($block['blockName'] === 'blocksy-companion-pro/code-editor') {
            $block['ct_allow_code_editor'] = true;
        }
        if (! empty($block['innerBlocks'])) {
            $block['innerBlocks'] = $this->mark_code_editor_blocks($block['innerBlocks']);
        }
    }
    return $blocks;
}
```

`mark_code_editor_blocks_and_unhook()` removes the filter after the **first**
`parse_blocks()` call, so the mark is applied only to blocks parsed during a
`CustomPostTypeRenderer::get_content()` render. `ContentBlocksRenderer extends
CustomPostTypeRenderer` (`framework/premium/features/content-blocks/renderer.php`),
so every content-block render that goes through `ContentBlocksRenderer->get_content()`
marks its code-editor blocks as trusted.

`ContentBlocksRenderer` is invoked from:

| Caller | File | Frontend? (`is_admin()`) | Flag set? |
|--------|------|--------------------------|-----------|
| `ContentBlocks::output_hook()` (shortcode `[blocksy-content-block]`, `display_hooks()` hook locations, inline popups) | `content-blocks.php` | **Yes (false)** | **Yes** |
| `ContentBlocksPopupsLogic::render_popup()` (inline popup, `load_content_with_ajax=no`) | `content-blocks/popups.php` | **Yes (false)** | **Yes** |
| `ContentBlocksPopupsLogic::render_popup_with_assets()` (AJAX popup, `blc_retrieve_popup_content`) | `content-blocks/popups.php` | **No (true, admin-ajax.php)** | Yes (but guard A blocks) |
| Mega-menu `Api::retrieve_mega_menu_content` (`blc_retrieve_mega_menu_content`) + custom-menu-walker | `mega-menu/includes/api.php`, `custom-menu-walker.php` | **No (true, admin-ajax.php)** for the AJAX endpoint; **Yes (false)** for the inline walker | Yes |
| `ContentBlocksTemplates` (single/archive template rendering) | `content-blocks/templates.php` | Yes (false) | Yes |
| `ContentBlocksAssetsManager::enqueue_hook()` (asset pre-render) | `content-blocks/assets-manager.php` | depends | Yes |

## 3. What the fix explicitly covers

1. **Standard `do_blocks()` single-view rendering** of a `ct_content_block`
   (the original repro surface: unauthenticated `GET /ct_content_block/<slug>/`).
   That path uses WP core's block renderer; it never calls
   `ContentBlocksRenderer`, so `ct_allow_code_editor` is never set → guard B
   returns `''`. **Empirically confirmed blocked on the fixed build** (TEST2:
   no marker, no `BLOCKSY_VARIANT_RCE` in response).

2. **AJAX-context rendering** via `admin-ajax.php`
   (`wp_ajax_nopriv_blc_retrieve_popup_content`,
   `wp_ajax_nopriv_blc_retrieve_mega_menu_content`). Those endpoints do call
   `ContentBlocksRenderer` (flag set), but `is_admin()` is `true` for
   `admin-ajax.php` requests → guard A returns `''`. **Empirically confirmed
   blocked on the fixed build** (TEST3: no marker).

3. **`WP_REST_Block_Renderer_Controller`** — already not a vector: requires
   `edit_posts` (HTTP 401 unauthenticated) and the code-editor block
   registers no `attributes` schema (`rest_additional_properties_forbidden`),
   so the `code` attribute path is not reachable unauthenticated (verified in
   the parent RCA). No `register_rest_route` exists in the plugin outside the
   freemius vendor, so there is no custom REST rendering of content blocks.

## 4. What the fix does NOT cover (residual surface)

**Frontend `ContentBlocksRenderer` rendering** — i.e. content blocks that are
displayed on a normal page load through the legitimate Blocksy renderer:

- a published page/post that embeds a content block via the
  `[blocksy-content-block id=..]` shortcode;
- a content block registered as a **hook** with a frontend location
  (`blocksy:single:content:*`, `blocksy:loop:card:after`, custom hooks, …)
  fired by `ContentBlocks::display_hooks()` on the `wp` action;
- a content block registered as a **popup** with `load_content_with_ajax=no`
  (rendered inline via the `blocksy:footer:offcanvas-drawer` filter);
- the inline mega-menu walker rendering a content-block-backed menu item;
- single/archive content-block templates rendered for the current request.

In all of these:
- `is_admin()` is **false** (real frontend HTTP request) → guard A passes;
- `ContentBlocksRenderer->get_content()` calls
  `parse_blocks_with_code_editor_mark()` → guard B's flag **is set** → guard B
  passes;
- → `get_eval_content()` runs `eval()` on the code-editor block content.

**Empirically confirmed executing on the FIXED (patched) build** (TEST1:
unauthenticated `GET /variant-shortcode-page/` → marker file
`/tmp/rce_marker_variant.txt` written by `www-data`, and
`BLOCKSY_VARIANT_RCE::uid=33(www-data) gid=33(www-data) groups=33(www-data)`
in the HTTP response body). The same path also executes on the vulnerable
build (as expected).

### Is this a bypass?

**No — not in isolation.** This is the **intended** post-fix behaviour. The
vendor changelog explicitly states the fix's purpose as *"Code editor block -
restrict execution to explicit renderer context"*. The `ContentBlocksRenderer`
**is** the explicit renderer context; the code-editor block is a feature whose
documented purpose is to execute admin-authored PHP inside content blocks
rendered through that renderer. An unauthenticated visitor triggering that
execution by viewing a page that displays an admin-authored code-editor block
(popup/hook/shortcode) is the designed use case. Per the variant guidance
("do not call normal/documented behaviour a bypass"), this is **not** a
security-policy bypass of CVE-2026-57624.

The **residual risk** is that the CVE-2026-57624 fix alone does **not** fully
remediate the unauthenticated-RCE impact: the `ContentBlocksRenderer` frontend
path remains an unauthenticated request path that triggers `eval()` of a
code-editor block. That surface is gated by **content-block authoring**
(`manage_options` capability for `ct_content_block` create/edit, enforced in
`ContentBlocks::__construct()` → `get_wp_capability_by('custom_post_type')`
and in the `wp_ajax_blocksy_content_blocksy_create` / `_toggle` handlers which
are `wp_ajax_`-only and capability-checked). That authoring boundary is
breached separately by the companion IDOR **CVE-2026-57630** (unauthenticated
content-block injection). Combined: `CVE-2026-57630` (inject a code-editor
block into a popup/hook/shortcode content block) + the residual
`ContentBlocksRenderer` frontend path → **unauthenticated RCE persists on
patched (2.1.47+/2.1.48) builds**, because the 57624 fix only closed the
`do_blocks` single-view path, not the `ContentBlocksRenderer` frontend path.

## 5. Fix assumptions

| Assumption | Holds? | Notes |
|------------|--------|-------|
| The unauthenticated eval trigger is the `do_blocks` single-view of a `ct_content_block`. | Partially | That is the surface the fix closes. Other unauthenticated request paths (frontend `ContentBlocksRenderer` rendering) also trigger eval — intentionally. |
| `ct_allow_code_editor` is only set inside `ContentBlocksRenderer`/`CustomPostTypeRenderer`. | Yes | Only `mark_code_editor_blocks()` sets it; filter self-removes after one `parse_blocks()` call. No other `add_filter('blocksy:block-parser:result', …)` exists. |
| `ContentBlocksRenderer` contexts are admin-authored (trusted). | Only as strong as the content-block authoring boundary | Breached by CVE-2026-57630 (IDOR). The 57624 fix does not re-validate authoring at eval time. |
| AJAX contexts are covered by `is_admin()`. | Yes | `admin-ajax.php` ⇒ `is_admin()` true ⇒ guard A blocks. Verified for popup + mega-menu nopriv endpoints. |
| The `code` attribute path is not unauthenticated. | Yes | REST block renderer requires `edit_posts`; no attributes schema. |
| There are no other `eval()`/code-exec sinks on attacker-controlled input. | Yes (for 57624 scope) | The only `eval()` of block content is `get_eval_content()`. The `qubely.php` `eval()` is a fixed class-definition string, not attacker-controlled. The "Custom Code Snippets" extension is a separate, admin-gated feature (out of 57624 scope). |

## 6. Behaviour before vs after the fix

| Request path (unauthenticated) | <= 2.1.46 / vuln build (guard B removed) | 2.1.48 fixed (guard A + B) |
|--------------------------------|------------------------------------------|----------------------------|
| `GET /ct_content_block/<slug>/` (do_blocks single-view) | **eval executes** | **blocked** (guard B) |
| `GET /<page-with-[blocksy-content-block]/>` (ContentBlocksRenderer frontend) | **eval executes** | **eval executes** (intended) |
| `GET /<page-with-inline-popup/>` (ContentBlocksRenderer frontend) | **eval executes** | **eval executes** (intended) |
| `POST admin-ajax.php action=blc_retrieve_popup_content` (nopriv) | blocked by guard A (`is_admin`) in the vuln build used here* | **blocked** (guard A) |
| `POST admin-ajax.php action=blc_retrieve_mega_menu_content` (nopriv) | blocked by guard A* | **blocked** (guard A) |
| `GET /wp-json/wp/v2/block-renderer/...` | blocked (edit_posts) | blocked (edit_posts) |

\* The differential vulnerable build used here reverts only guard B and keeps
guard A. A hypothetical fully-vulnerable `<=2.1.46` build with **both** guards
absent would additionally execute eval on the popup/mega-menu nopriv AJAX
endpoints; that older build is not downloadable (commercial plugin) so it
could not be tested directly.

## 7. Target threat model / security policy

- No `SECURITY.md` is shipped in the plugin. `readme.txt` directs security
  reports to the **Patchstack VDP** (`https://patchstack.com/database/vdp/blocksy-companion`).
- The CVE disclosure (Patchstack) marks the issue as **Unauthenticated**, CVSS
  10.0, CWE-94 — i.e. the in-scope impact is *unauthenticated* code execution.
- The code-editor block is a documented premium feature for executing
  admin-authored PHP in content blocks; execution inside the legitimate
  `ContentBlocksRenderer` is the documented, intended behaviour. The
  out-of-scope (separate CVE) item is **unauthenticated content injection**
  (CVE-2026-57630, IDOR).

## 8. Completeness assessment

The fix is **complete for its stated scope** (closing the `do_blocks`
single-view unauthenticated eval path and AJAX-context rendering) and is
**sound** (no logic flaw in the guards was found; the flag cannot be set by an
attacker through any other filter, and there is no unauthenticated REST
rendering of content blocks).

The fix is **incomplete against the combined CVE-2026-57624 + CVE-2026-57630
threat**: an attacker who can inject a code-editor block into a content block
(via the IDOR) can render it through the `ContentBlocksRenderer` frontend path
(popup/hook/shortcode), and unauthenticated page loads will trigger `eval()`
on patched builds. Closing that requires remediating the IDOR (57630) and/or
additionally gating code-editor execution at eval time (see Recommendations in
`rca_report.md`).
