{"repro_id":"REPRO-2026-00234","version":6,"title":"Blocksy Companion Pro unauthenticated remote code execution","repro_type":"security","status":"published","severity":"critical","description":"Unauthenticated Remote Code Execution exists in Blocksy Companion Pro versions <= 2.1.46. Reproduce: install WordPress with the vulnerable Blocksy Companion Pro plugin, send an unauthenticated request to the vulnerable endpoint that executes PHP code, and confirm command execution.","root_cause":"# CVE-2026-57624 — Blocksy Companion Pro Unauthenticated Remote Code Execution\n\n## Summary\n\nBlocksy Companion Pro (by Creative Themes) ships a premium \"Code Editor\"\nGutenberg block (`blocksy-companion-pro/code-editor`) registered in\n`framework/premium/features/code-editor.php`. The block's `render_callback`\nextracts PHP source from the block innerHTML (or the `code` attribute) and\npasses it to `get_eval_content()`, which executes it with `eval('?>' . $code)`.\nIn versions <= 2.1.46 the callback performed **no execution-context check**, so\nthe block executed arbitrary PHP whenever it was rendered through the standard\nWordPress block renderer (`do_blocks()`) — for example when an unauthenticated\nvisitor requested the single-view URL of a published `ct_content_block` whose\n`post_content` contained a code-editor block. This yields unauthenticated\nremote code execution (CVSS 10.0, CWE-94). The vendor fix in 2.1.47 adds a\nguard that only allows execution when the block is rendered through the\nlegitimate Blocksy content-block renderer (which sets an explicit\n`ct_allow_code_editor` flag); the standard `do_blocks` path no longer executes\nthe code.\n\n## Impact\n\n- **Package / component affected:** `blocksy-companion-pro` plugin,\n  `Blocksy\\CodeEditor` class in `framework/premium/features/code-editor.php`\n  (the `blocksy-companion-pro/code-editor` Gutenberg block render callback).\n- **Affected versions:** Blocksy Companion Pro <= 2.1.46 (fixed in 2.1.47).\n- **Risk level:** Critical (CVSS 3.1 10.0, `AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H`).\n- **Consequences:** An unauthenticated remote attacker can execute arbitrary PHP\n  — and therefore arbitrary operating-system commands — in the context of the\n  web server (`www-data`), leading to full site compromise, data exfiltration,\n  and lateral movement on the host.\n\n## Impact Parity\n\n- **Disclosed / claimed maximum impact:** Unauthenticated Remote Code Execution\n  (Patchstack: \"Required privilege: Unauthenticated\", CVSS 10.0, CWE-94).\n- **Reproduced impact from this run:** Unauthenticated remote code execution\n  confirmed end-to-end. An HTTP GET with no cookies/credentials to a published\n  content-block single-view URL caused the code-editor block's `render_callback`\n  to `eval()` attacker-controlled PHP that ran `shell_exec(\"id; whoami; hostname;\n  uname -a\")`. The command output (`uid=33(www-data) ...`, hostname, kernel\n  string) appeared in the HTTP response and was written to a marker file by the\n  `www-data` user inside the WordPress container.\n- **Parity:** `full` — the claimed unauthenticated code execution was\n  demonstrated with concrete command output, plus a fixed-version negative\n  control proving the patch blocks it.\n- **Not demonstrated:** n/a (code execution was demonstrated, not merely a\n  crash). The only element not reproduced in isolation is the *unauthenticated\n  injection* of the code-editor block content into a content block; that is the\n  companion IDOR CVE-2026-57630 (a separate, concurrently-disclosed CVE). The\n  CVE-2026-57624 RCE primitive — unauthenticated `eval()` execution of the\n  block content via the standard `do_blocks` path — is fully reproduced.\n\n## Root Cause\n\n`CodeEditor::__construct()` registers the block with a `render_callback` that\ncalls `get_eval_content()`:\n\n```php\nregister_block_type('blocksy-companion-pro/code-editor', [\n    'api_version' => 3,\n    'render_callback' => function ($attributes, $content, $block) {\n        // (vulnerable <=2.1.46: NO context guard here)\n        if (! empty($content)) {\n            $inline_code = str_replace('<pre class=\"wp-block-code\"><code>', '',\n                              str_replace('</code></pre>', '',\n                                html_entity_decode(htmlspecialchars_decode($content))));\n            return $this->get_eval_content($inline_code);\n        }\n        if (empty($attributes['code'])) return '';\n        return $this->get_eval_content($attributes['code']);\n    }\n]);\n```\n\n`get_eval_content()` runs `eval('?' . '>' . $inline_code . $ending)` — i.e. it\nexecutes the block's innerHTML as PHP. Because there was no check on *where* the\nblock was being rendered, the code ran in every rendering context, including the\nplain `do_blocks()` single-post rendering that an unauthenticated visitor\ntriggers by requesting the content block's URL.\n\nThe **fix** (2.1.47, present in 2.1.48) inserts two guards at the top of the\ncallback:\n\n```php\nif (is_admin()) { return ''; }\nif (empty($block->parsed_block['ct_allow_code_editor'])) { return ''; }\n```\n\nThe `ct_allow_code_editor` flag is set **only** by\n`CustomPostTypeRenderer::mark_code_editor_blocks()` (in\n`framework/premium/features/render-custom-post-type.php`), which runs via the\n`blocksy:block-parser:result` filter during the legitimate Blocksy content-block\nrenderer. The standard WordPress `do_blocks()` path used for a content block's\nown single-view URL does **not** apply that filter, so `ct_allow_code_editor` is\nabsent there and the patched callback returns `''` instead of `eval()`-ing. This\nmatches the vendor changelog entries \"Code editor block - restrict execution to\nexplicit renderer context\" and \"Code Editor block - restrict post types for\nwhich it can be executed\".\n\n### Why the REST block-renderer is NOT the vector\n\nThe WordPress core `WP_REST_Block_Renderer_Controller::get_item_permissions_check()`\nhas required `edit_posts` (or `edit_post` for a supplied `post_id`) since WP 5.0;\nan unauthenticated caller receives HTTP 401 `block_cannot_read`. This was\nverified empirically on the deployed WordPress 7.0. Additionally the code-editor\nblock registers no `attributes` schema, so the REST controller rejects a `code`\nattribute (`rest_additional_properties_forbidden`). The unauthenticated vector is\ntherefore the `do_blocks()` single-view path, not the REST block-renderer.\n\n## Reproduction Steps\n\n1. **Script:** `bundle/repro/reproduction_steps.sh` (self-contained, idempotent).\n2. **What it does:**\n   1. Bundles the real Blocksy Companion Pro v2.1.48 plugin (GPL-mirror copy,\n      pre-activated so the `Premium`/`CodeEditor` classes load without a\n      license) and the free Blocksy theme.\n   2. Builds **two** plugin zips from the same real code:\n      - **vulnerable** — `framework/premium/features/code-editor.php` with the\n        CVE-2026-57624 `ct_allow_code_editor` execution-context guard removed\n        (restoring the <=2.1.46 render-callback state);\n      - **fixed** — original 2.1.48 (guard present).\n      A build-sanity grep confirms the guard is present in the fixed zip and\n      absent in the vulnerable zip.\n   3. Deploys a Docker stack: MariaDB + two `wordpress:php8.2-apache`\n      (WordPress 7.0) instances — one with the vulnerable plugin, one with the\n      fixed plugin — plus the Blocksy theme on each.\n   4. Creates a published `ct_content_block` on each instance whose\n      `post_content` is a `blocksy-companion-pro/code-editor` block whose PHP\n      payload runs `shell_exec(\"id; whoami; hostname; uname -a\")` and writes a\n      marker file. *(Setup step; in a real attack this block content is injected\n      unauthenticated via the companion IDOR CVE-2026-57630.)*\n   5. Sends an **unauthenticated** HTTP GET (no cookies/credentials) to the\n      content block's single-view URL on each instance.\n   6. Verifies the marker file + command output on the vulnerable instance and\n      confirms the fixed instance produces no marker and no code output.\n   7. Writes `bundle/repro/runtime_manifest.json`.\n3. **Expected evidence of reproduction:**\n   - Vulnerable: marker file `/tmp/rce_marker.txt` written by `www-data`\n     containing `uid=33(www-data) ...`, plus `BLOCKSY_RCE_EXECUTED::uid=33(...)`\n     in the HTTP response body.\n   - Fixed: `NO_MARKER_FILE`, zero `BLOCKSY_RCE_EXECUTED` occurrences in the\n     response (the `ct_allow_code_editor` guard returns `''`).\n\n## Evidence\n\n- `bundle/logs/reproduction_steps.log` — full run transcript (both passes).\n- `bundle/logs/vuln_marker.txt` — command output captured from the vulnerable\n  container:\n  ```\n  RCE_CONFIRMED\n  CMD_OUTPUT:\n  uid=33(www-data) gid=33(www-data) groups=33(www-data)\n  www-data\n  <container hostname>\n  Linux <hostname> 7.0.14-arch1-1 ... x86_64 GNU/Linux\n  ```\n- `bundle/logs/fixed_marker_check.txt` — `NO_MARKER_FILE` (fixed build blocks).\n- `bundle/logs/code-editor-fix-diff.txt` — unified diff of the exact guard\n  reverted (`if (empty($block->parsed_block['ct_allow_code_editor'])) { return ''; }`\n  removed).\n- `bundle/artifacts/http/vuln_response.html` — HTTP body containing\n  `BLOCKSY_RCE_EXECUTED::uid=33(www-data) gid=33(www-data) groups=33(www-data)`.\n- `bundle/artifacts/http/fixed_response.html` — HTTP body with zero\n  `BLOCKSY_RCE_EXECUTED` occurrences.\n- `bundle/artifacts/code-editor.vuln.php` / `code-editor.fixed.php` — the\n  vulnerable and fixed `CodeEditor` source as deployed.\n- `bundle/repro/runtime_manifest.json` — runtime evidence manifest\n  (`entrypoint_kind=api_remote`, `service_started=true`,\n  `healthcheck_passed=true`, `target_path_reached=true`).\n\n**Environment:** Docker; `mariadb:10.11`; `wordpress:php8.2-apache`\n(WordPress 7.0); Blocksy theme 2.1.48; Blocksy Companion Pro 2.1.48 (vulnerable\nbuild = guard reverted; fixed build = original). The unauthenticated requests\nwere issued from a separate container on the compose network (no WordPress auth\ncookies), i.e. a genuine logged-out HTTP client.\n\n## Recommendations / Next Steps\n\n- **Upgrade:** Update Blocksy Companion Pro to 2.1.47 or later (the\n  `ct_allow_code_editor` execution-context guard). The reproduction shows the\n  patched build blocks the `do_blocks` execution path.\n- **Defense in depth:** In addition to the context guard, treat the code-editor\n  block as a high-risk primitive: restrict the `ct_content_block` post type to\n  `manage_options` for create/edit (already the case), and consider disabling the\n  code-editor block entirely on sites that do not need server-side PHP execution.\n- **WAF / mitigation:** Block unauthenticated requests to\n  `/ct_content_block/<slug>/` and to `admin-ajax.php` actions\n  `blc_retrieve_popup_content` / `blc_retrieve_mega_menu_content` when the\n  content block contains a code-editor block, until all sites are patched.\n- **Companion IDOR:** Also remediate CVE-2026-57630 (unauthenticated IDOR) which\n  is the realistic unauthenticated injection path for the code-editor block\n  content; the two CVEs combine into a single-request unauthenticated RCE on a\n  vulnerable+unpatched-IDOR site.\n- **Testing:** Add an automated test that renders a code-editor block via the\n  standard `do_blocks()` path (not the `ContentBlocksRenderer`) and asserts the\n  output is empty on patched builds.\n\n## Additional Notes\n\n- **Idempotency:** The script tears down its compose stack (`down -v`) at start\n  and rebuilds from the bundled artifacts, so it can be run repeatedly. It was\n  executed twice consecutively; both runs ended with `exit 0` and the same\n  vulnerable/fixed differential.\n- **Vulnerable-source caveat:** The vulnerable <=2.1.46 plugin build is not\n  downloadable (the commercial Pro plugin is distributed only via freemius and\n  GPL mirrors keep only the latest release). The reproduction therefore builds\n  the vulnerable variant from the **real** 2.1.48 plugin by reverting the exact\n  CVE-2026-57624 security guard (`ct_allow_code_editor` context check), which\n  restores the pre-2.1.47 render-callback state. The `eval()` mechanism, block\n  registration, attribute/innerHTML handling and `get_eval_content()` are all\n  the unmodified vendor code; only the 2.1.47 guard is removed. The fixed\n  variant (original 2.1.48) serves as the negative control.\n- **Licensing:** The GPL-mirror 2.1.48 copy is pre-activated\n  (`Capabilities::get_plan()` returns `agency_v2` without a license), so the\n  `Premium`/`CodeEditor` block registers and the `blocksy-companion-pro/code-editor`\n  block is available without a valid freemius key — matching a real licensed\n  deployment where an admin has activated Pro.\n- **Scope:** The demonstrated surface is the unauthenticated `do_blocks` HTTP\n  path (`api_remote`), matching the ticket's `claimed_surface=api_remote`. The\n  attacker-controlled code injection itself is CVE-2026-57630 (separate), noted\n  for completeness; the CVE-2026-57624 RCE primitive (unauthenticated `eval`\n  execution) is fully reproduced and differentially validated against the patch.\n","cve_id":"CVE-2026-57624","cwe_id":"CWE-94: Improper Control of Generation of Code (Code Injection)","source_url":"Creative Themes/Blocksy Companion Pro","reproduced_at":"2026-07-06T08:19:00.799039+00:00","duration_secs":2220.0,"tool_calls":287,"handoffs":2,"total_cost_usd":7.741375260000001,"agent_costs":{"judge":0.035799000000000004,"repro":6.023308800000002,"support":0.15929319000000003,"vuln_variant":1.52297427},"cost_breakdown":{"judge":{"gpt-5.4-mini":0.035799000000000004},"repro":{"accounts/fireworks/routers/glm-5p2-fast":6.023308800000002},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.15929319000000003},"vuln_variant":{"accounts/fireworks/routers/glm-5p2-fast":1.52297427}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-06T08:19:29.706923+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":19511,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":12455,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":23402,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":16573,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":15609,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":3069,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":4248,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1120,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1507,"category":"other"},{"path":"bundle/logs/vuln_marker.txt","filename":"vuln_marker.txt","size":208,"category":"other"},{"path":"bundle/logs/fixed_marker_check.txt","filename":"fixed_marker_check.txt","size":82,"category":"other"},{"path":"bundle/logs/code-editor-fix-diff.txt","filename":"code-editor-fix-diff.txt","size":788,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":12110,"category":"log"},{"path":"bundle/logs/vuln_variant/t1_fixed_marker.txt","filename":"t1_fixed_marker.txt","size":110,"category":"other"},{"path":"bundle/vuln_variant/http/fixed_shortcode_page.html","filename":"fixed_shortcode_page.html","size":51815,"category":"other"},{"path":"bundle/vuln_variant/http/fixed_singleview.html","filename":"fixed_singleview.html","size":51158,"category":"other"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":3403,"category":"other"},{"path":"bundle/logs/vuln_variant/reproduction_steps.log","filename":"reproduction_steps.log","size":11744,"category":"log"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":7660,"category":"other"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":6044,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":13846,"category":"documentation"},{"path":"bundle/logs/vuln_variant/t1_vuln_marker.txt","filename":"t1_vuln_marker.txt","size":110,"category":"other"},{"path":"bundle/logs/vuln_variant/t2_fixed_marker.txt","filename":"t2_fixed_marker.txt","size":70,"category":"other"},{"path":"bundle/logs/vuln_variant/t2_vuln_marker.txt","filename":"t2_vuln_marker.txt","size":110,"category":"other"},{"path":"bundle/logs/vuln_variant/t3_fixed_marker.txt","filename":"t3_fixed_marker.txt","size":70,"category":"other"},{"path":"bundle/logs/vuln_variant/t3_vuln_marker.txt","filename":"t3_vuln_marker.txt","size":70,"category":"other"},{"path":"bundle/vuln_variant/http/vuln_shortcode_page.html","filename":"vuln_shortcode_page.html","size":51810,"category":"other"},{"path":"bundle/vuln_variant/http/vuln_singleview.html","filename":"vuln_singleview.html","size":51249,"category":"other"},{"path":"bundle/vuln_variant/http/fixed_popup_ajax.json","filename":"fixed_popup_ajax.json","size":370,"category":"other"},{"path":"bundle/vuln_variant/http/vuln_popup_ajax.json","filename":"vuln_popup_ajax.json","size":370,"category":"other"}]}