# Variant RCA Report — CVE-2026-56290 (Page Builder CK arbitrary file upload to RCE)

## Summary

This stage surveyed the Page Builder CK (`com_pagebuilderck`) extension for a **bypass** of the v3.6.0/3.6.1 security fix, or an **alternate trigger** of the same unauthenticated-arbitrary-file-upload-to-RCE root cause. **No bypass was found.** The fix is comprehensive: a single central authorization check in `CKController::execute()` (`if ($task !== 'display') CKFof::checkSecurity();`) guards every non-`display` controller task for the component, and the only sink that can deliver a `.php` web shell — `CKBrowse::ajaxAddPicture()`, whose extension allowlist is commented out — is protected at both the dispatch level and the method level. Empirically, on the fixed v3.6.1 install, **every** surveyed variant returned 403 (or a non-RCE error) and **no `.php` file was written** to the media tree; on the vulnerable state (fix reverted, ≤ 3.5.10) the original `browse.ajaxAddPicture` endpoint reproduced full RCE, confirming the harness and the fix's coverage. Alternate ajax endpoints (`fonts.save`, `pixabay.upload`, `browse.ajaxCreateFolder`, `frontedition.createModule`, the `ajaxParamsCall` gadget) share the broader "missing-authorization" weakness on the vulnerable version but cannot deliver a `.php` web shell because they force safe extensions (`.woff2/.ttf`, image extensions, `.txt`, `.css`) or perform DB-only writes, and they are all blocked by the fix.

## Fix Coverage / Assumptions

- **Invariant the fix relies on**: every `index.php?option=com_pagebuilderck&task=…` request is dispatched through `CKController::execute()`. The site entry point (`site/pagebuilderck.php`) always calls `$controller->execute($input->get('task'))`, and the site controllers merely `include_once` the administrator controllers, so front-end and back-end share the same `execute()`. There is no alternate routing into controller methods.
- **Code paths explicitly covered**: all non-`display` controller tasks — `browse.ajaxAddPicture`, `browse.ajaxCreateFolder`, `browse.getFiles`, `fonts.save`, `pixabay.upload`, `page.save`, `frontedition.createModule`, `ajaxParamsCall`, and ~15 other ajax methods in `controller.php` — via Layer 1 (`execute()`) plus Layer 2 (per-method `checkSecurity()`).
- **What the fix does NOT cover**: the `display` task is exempt from the authz check. This is safe because `display()` only renders views and writes no `.php` file (verified: `task=display&view=browse` writes nothing to `media/com_pagebuilderck`). Out-of-`execute()` write paths (front-end CSS compiler `PagebuilderckFrontHelper::loadAllCss()`, the `writeLabelsFile()` loader) are not gated by `execute()`, but they force `.css` / `.txt` extensions and sanitize the path, so they cannot achieve PHP RCE.
- **Assumption that holds**: `Joomla\CMS\User::authorise('core.edit', 'com_pagebuilderck')` is false for guests, so `checkSecurity()` throws 403 for unauthenticated callers. The pre-existing CSRF token gate (`checkAjaxToken`) is not an authorization boundary (the token is harvestable from public pages) and is correctly supplemented by `checkSecurity()`.

## Variant / Alternate Trigger

No bypass of the fix was found. The following materially distinct entry points / data paths were tested (entry point: unauthenticated HTTP to `index.php?option=com_pagebuilderck&task=<controller.task>&<guest_csrf_token>=1`):

| ID | Task | Sink (file / function) | Fixed v3.6.1 | Vulnerable |
|----|------|------------------------|--------------|------------|
| C1 | `browse.ajaxAddPicture` | `helpers/ckbrowse.php` `CKBrowse::ajaxAddPicture()` — arbitrary-extension `$_FILES` upload (`file_put_contents(JPATH_SITE.$imgpath.$filename, $data)`, allowlist commented at `ckbrowse.php:264`) | 403, no `.php` | **200 → RCE** |
| C2 | `browse.ajaxCreateFolder` | `CKBrowse::createFolder()` — folder creation | 403 | 200 (reachable) |
| C3 | `browse.getFiles` | `CKBrowse::getImagesInFolder()` — file listing | 403 | reachable |
| C4 | `fonts.save` (local=1) | `controllers/fonts.php` `save()` — writes font files (forced `.woff2/.ttf`) | 403 | 403 (extra guard; not RCE) |
| C5 | `pixabay.upload` | `controllers/pixabay.php` `upload()` — remote fetch+write (img ext allowlist, pixabay-only URL) | 403 | 200 (reachable; not RCE) |
| C6 | `frontedition.createModule` | `site/controllers/frontedition.php` `createModule()` — DB module insert (no file write; no method-level `checkSecurity`) | 500 | 500 (reachable; not RCE) |
| C7 | `page.save` | `site/controllers/page.php` `save()` → model DB write | 403 | reachable |
| C8 | `display` (view=browse) | `CKController::display()` — view rendering (the `display` exemption) | 403 (view ACL), **no `.php` written** | renders, no `.php` |
| C9 | `ajaxParamsCall` | `controller.php` `ajaxParamsCall()` — arbitrary helper-method dispatch gadget (target `PagebuilderckLoaderFolder::writeLabelsFile`, forced `.txt`) | 403 | 200 (reachable; `.txt` only) |
| C10 | `browse.ajaxAddPicture` via `/administrator/index.php` | same sink, admin entry point | 200 (admin login redirect, no upload) | — |

Key code paths:
- Central fix: `administrator/components/com_pagebuilderck/helpers/ckcontroller.php:230-232` (`execute()`).
- Authorizer: `administrator/components/com_pagebuilderck/helpers/ckfof.php:50-57` (`checkSecurity` / `userCan`).
- RCE sink: `administrator/components/com_pagebuilderck/helpers/ckbrowse.php:205-322` (`ajaxAddPicture`).
- Controller method guard: `administrator/components/com_pagebuilderck/controllers/browse.php:38-47`.
- Site dispatch entry: `site/pagebuilderck.php` (always calls `execute()`).

## Impact

- **Package / component**: `com_pagebuilderck` (Page Builder CK), free variant.
- **Affected versions (as tested)**: fixed version 3.6.1 (no bypass); vulnerable state ≈ ≤ 3.5.10 (RCE reproduced).
- **Risk level**: Critical (CVSS 4.0 Base 10.0) for the underlying CVE on vulnerable versions; the fix reduces unauthenticated RCE to a hard 403.
- **Consequences on vulnerable versions**: unauthenticated full RCE (web shell upload + execution as `www-data`). On the fixed version: blocked.

## Impact Parity

- **Disclosed / claimed maximum impact (parent CVE)**: unauthenticated arbitrary file upload → full Remote Code Execution (CVSS 10.0, CWE-434/284).
- **Reproduced impact from this variant run**:
  - On the **vulnerable** state: full RCE confirmed — `browse.ajaxAddPicture` returned HTTP 200 and the uploaded `exploit_shell.php` executed, emitting `RCE_PROOF_Linux <host> … x86_64_www-data` (web shell at `media/com_pagebuilderck/gfonts/exploit_shell.php`).
  - On the **fixed** state: no variant reached the sink; all controller-task probes returned 403 (or non-RCE 500); zero `.php` files appeared in the media tree; no `RCE_PROOF_` marker. No bypass.
- **Parity**: `none` for a bypass (no bypass exists). For the parent claim on the vulnerable version, parity is `full` (RCE). The alternate triggers are **not** RCE-capable (forced safe extensions / DB-only), so they do not attain the parent's code-execution impact.
- **Not demonstrated**: No alternate path to code execution on the fixed version was demonstrated — because none exists for this root cause.

## Root Cause

The underlying bug is a missing authorization check on `com_pagebuilderck` ajax controller endpoints. `CKBrowse::ajaxAddPicture()` accepts a multipart file upload and writes it with `file_put_contents(JPATH_SITE . $imgpath . $filename, $object->data)`, where `$imgpath` (destination folder) and `$filename` (including extension — the `getExt` allowlist is commented out) are attacker-controlled, and the only pre-fix gate is a guest-harvestable CSRF token. The fix adds `CKFof::checkSecurity()` (→ `$user->authorise('core.edit', …)`, false for guests → 403) in `CKController::execute()` for every non-`display` task and in each method. The same root-cause class (missing authz) exists on other ajax endpoints in the vulnerable version, but only `ajaxAddPicture` accepts an attacker-controlled file **extension**, so only it can deliver a `.php` web shell → RCE. No fix commit SHA is available (the extension is distributed as a package, not via a public git repository).

## Reproduction Steps

1. **Reference**: `bundle/vuln_variant/reproduction_steps.sh` (idempotent; run from any directory).
2. **What the script does**:
   - Starts Joomla 5.2 + MariaDB 10.11 in an isolated Docker compose project (`pbckvariant`, port 38081) so it never collides with the repro environment.
   - Installs Joomla via CLI, then installs the **fixed** Page Builder CK v3.6.1 from `bundle/pagebuilderck_3.6.1.zip`.
   - Stages a PHP web shell (`<?php echo "RCE_PROOF_".php_uname()."_".get_current_user(); ?>`) and a container-side `probe.sh` that harvests a guest CSRF token from the public home page and fires unauthenticated requests.
   - **Phase 1 (FIXED)**: fires 10 variant probes (C1–C10 above) against the unmodified fixed install and scans the media tree for any new `.php` / `RCE_PROOF_` marker.
   - **Phase 2 (VULNERABLE)**: backs up the fixed files, removes **every** `CKFof::checkSecurity()` call (simulating ≤ 3.5.10), applies the Joomla 5.2 `$_FILES` compatibility patch, **restarts the Joomla container to clear PHP OPcache** so the reverted code is actually served, re-runs the probes, then restores the fixed files and tears down the containers.
   - Writes `validation_verdict.json`, `variant_manifest.json`, `runtime_manifest.json`, `source_identity.json`, `root_cause_equivalence.json`, and `phase_results.json`.
   - Exit 0 = bypass on fixed; exit 1 = no bypass (the observed outcome).
3. **Expected evidence**: `bundle/logs/vuln_variant_evidence.log` (full probe log), `bundle/vuln_variant/phase_results.json` (per-test HTTP codes), `bundle/logs/fixed_version.txt` (tested version). The decisive evidence: on the fixed version `rce_marker_phase1 = none` and all controller-task probes are 403; on the vulnerable version `rce_marker_phase2 = …/exploit_shell.php` and C1 returns 200 with the `RCE_PROOF_` marker.

## Evidence

- **Log**: `bundle/logs/vuln_variant_evidence.log` — full timestamped probe output for both phases.
- **Per-test HTTP codes**: `bundle/vuln_variant/phase_results.json`:
  - Phase 1 (fixed): C1=403, C2=403, C3=403, C4=403, C5=403, C6=500, C7=403, C8=403, C9=403, C10=200; `rce_marker_phase1 = none`.
  - Phase 2 (vulnerable): C1=200, C2=200, C4=403, C5=200, C6=500, C9=200; `rce_marker_phase2 = /var/www/html/media/com_pagebuilderck/gfonts/exploit_shell.php`.
- **Key excerpt (vulnerable RCE)**: `[P2-C1] shell access on vulnerable: RCE_PROOF_Linux 8c02f7b0fa95 7.0.14-arch1-1 … x86_64_www-data` and `RCE marker file under media (Phase 2): /var/www/html/media/com_pagebuilderck/gfonts/exploit_shell.php`.
- **Key excerpt (fixed blocked)**: `[P1-C1] RESULT|…|http_code=403`; `PHP files in media tree after Phase 1: 0`; `RCE marker file under media (Phase 1): none`.
- **Environment**: Joomla 5.2, PHP 8.2, Apache 2.4, MariaDB 10.11, Page Builder CK 3.6.1 (free). Tested source identity recorded in `bundle/vuln_variant/source_identity.json`.

## Recommendations / Next Steps

- **For the RCE root cause: no gap to close** — the fix is complete. The Coding stage need not extend the authorization fix for a bypass (none exists).
- **Defense-in-depth (recommended, not required for correctness)**:
  1. Enforce an extension allowlist in `CKBrowse::ajaxAddPicture()` (fix the commented-out `getExt` check) so a `.php` upload is rejected at the sink even if an authz check is ever regressed.
  2. Constrain the `path` parameter to the configured images directory (reject `..` and absolute paths) in `ajaxAddPicture` and `ajaxCreateFolder`.
  3. Add `CKFof::checkSecurity()` to `frontedition::createModule()`/`savemodules()` method bodies for consistency (they currently rely solely on the central `execute()` check).
  4. Do not treat `CKFile::makeSafe()` as a security boundary — it sanitizes filename characters but does not block executable extensions.

## Additional Notes

- **Idempotency**: the script was executed twice end-to-end; both runs returned exit 1 with identical per-test HTTP codes and identical verdict (`bypass_on_fixed=false, vuln_rce=true`), and the Docker compose project (`pbckvariant`) is torn down (`down -v`) at the end of every run, leaving no running containers and no changes to the project cache / repo checkout.
- **OPcache caveat**: PHP OPcache initially served stale (fixed) bytecode after the on-disk `sed` revert in Phase 2, causing the first vulnerable-state run to still return 403. The script now restarts the Joomla container after the revert to flush OPcache; a `grep` confirms `checkSecurity` is gone from `ckcontroller.php` before probing. Reproducers of this extension on Joomla 5.x should account for OPcache when hot-editing extension PHP.
- **Bounded search**: fewer than three *RCE-capable* candidates exist because `ajaxAddPicture` is the only sink accepting attacker-controlled file extensions; the remaining candidates were surveyed and ruled out as RCE paths (forced safe extensions / DB-only writes), so no additional attempts would yield a bypass for this root cause.
