# Patch Analysis — CVE-2026-56290 (Page Builder CK arbitrary file upload to RCE)

## Target & Tested Source

- **Product / repository**: `joomlack/page_builder_ck` — Joomla extension `com_pagebuilderck` (Page Builder CK), free variant.
- **Tested version**: **3.6.1** (the fixed release), installed from the official extension package `pagebuilderck_3.6.1.zip` (`<version>3.6.1</version>` in `pagebuilderck.xml`). Runtime: Joomla 5.2 + PHP 8.2 + Apache 2.4 + MariaDB 10.11 (Docker: `joomla:5.2-php8.2-apache`, `mariadb:10.11`).
- **Source identity**: No git repository is published for this extension; the source is the extension package. The vulnerable state (≤ 3.5.10) was recreated by reverting every `CKFof::checkSecurity()` call from the fixed install.

## What the fix changes

The root cause is that the front-end file-upload handler `CKBrowse::ajaxAddPicture()` (in `administrator/components/com_pagebuilderck/helpers/ckbrowse.php`) performs an **arbitrary file upload** — attacker-controlled destination folder (`path`), filename **including extension** (the extension allowlist at `ckbrowse.php:264` is commented out: `// if (CKFile::getExt($filename) != 'jpg')`), and file content — protected only by a Joomla anti-CSRF token that any unauthenticated visitor can harvest from the site's public pages. There is **no authorization check** in the helper itself (`CKBrowse::ajaxAddPicture()` calls only `Session::checkToken('get')`).

The fix (v3.6.0 / v3.6.1) adds authorization checks via `CKFof::checkSecurity()`, which calls `$user->authorise('core.edit', 'com_pagebuilderck')` and throws a 403 `JERROR_ALERTNOAUTHOR` exception when the user is not authorized (`CKFof::checkSecurity()`, `helpers/ckfof.php:50-57`). The checks are applied in two layers:

### Layer 1 — Central dispatch (the load-bearing fix)
`CKController::execute()` in `administrator/components/com_pagebuilderck/helpers/ckcontroller.php` (lines 230-232):

```php
public function execute($task) {
    if (! $task) $task = 'display';
    if ($task !== 'display') CKFof::checkSecurity();   // <-- added by the fix
    if (is_callable(array($this, $task))) {
        return $this->$task();
    }
    ...
}
```

Every request to `index.php?option=com_pagebuilderck&task=<controller.task>` is routed through `site/pagebuilderck.php` → `CKController::getInstance('Pagebuilderck')` → `$controller->execute($task)`. Because the site controllers (`site/controllers/*.php`) only `include_once` the corresponding **administrator** controller (e.g. `site/controllers/browse.php` includes `JPATH_ADMINISTRATOR . '/components/com_pagebuilderck/controllers/browse.php'`), the same `PagebuilderckControllerBrowse` class — and the same `execute()` — is used for front-end and back-end requests. This single line therefore guards **every non-`display` controller task** for the component, regardless of entry point.

### Layer 2 — Defense-in-depth in each method
`CKFof::checkSecurity()` was also added at the top of individual controller methods, e.g.:
- `controllers/browse.php`: `ajaxCreateFolder()`, `ajaxAddPicture()`, `getFiles()`
- `controllers/fonts.php`: `save()`
- `controllers/pixabay.php`: `upload()`
- `controller.php` (base): `ajaxDoRestoration()`, `ajaxParamsCall()`, `ajaxLoadPageHtml()`, `ajaxSaveFavorite()`, `ajaxLoadLibraryHtml()`, `checkAjaxUserEditRight()`, and ~15 other ajax methods
- `controllers/styles.php`: `import()`, `export()`
- `helpers/pagebuilderck.php`: `ajaxSetPluginOption()`, `ajaxGetPluginOption()`

## What invariant the fix relies on

1. **Single dispatch chokepoint**: every `option=com_pagebuilderck` request with a `task` flows through `CKController::execute()`. There is no alternate routing into controller methods (the site entry point always calls `execute()`; plugins/modules do not dispatch controller tasks).
2. **`display` is the only exempt task**: `display()` only renders views and performs no privileged file write, so exempting it from the authz check is safe.
3. **`core.edit` is false for guests**: `Joomla\CMS\User::authorise('core.edit', 'com_pagebuilderck')` returns false for unauthenticated users, so `checkSecurity()` throws 403 for them.
4. **`ajaxAddPicture` is the only RCE-capable sink**: it is the only handler that accepts a `$_FILES` upload with an attacker-controlled extension. All other file-write sinks force safe extensions or fixed paths (see below).

## What code paths / inputs the fix does NOT cover (and why it still does not matter)

The variant survey examined every other file-write primitive in the extension to see whether it could deliver a `.php` web shell while bypassing the fix. None can:

| Sink | Location | Writes attacker-controlled extension? | Outcome |
|---|---|---|---|
| `CKBrowse::ajaxAddPicture()` | `helpers/ckbrowse.php:322` | **Yes** (allowlist commented out) | RCE-capable — protected by Layer 1 + Layer 2 |
| `PagebuilderckFrontHelper::loadAllCss()` | `helpers/pagebuilderckfront.php:196` | No — forced `.css`; `$clearPath` is `preg_replace("/[^a-zA-Z0-9]+/","",...)` (no traversal) | Out-of-`execute()` (front rendering) but cannot write `.php` |
| `PagebuilderckLoaderFolder::writeLabelsFile()` | `helpers/loaders/folder.php:354` | No — forced `labels.txt` | Reachable only via the `ajaxParamsCall` gadget, which is itself guarded by `checkSecurity()`; `.txt` is not executed by Apache |
| Fonts controller `save()` | `controllers/fonts.php:161,176` | No — forced `.woff2`/`.ttf` font extensions | Guarded by `checkSecurity()` |
| Pixabay controller `upload()` | `controllers/pixabay.php:97` | No — extension allowlist `[jpg,jpeg,webp,png,gif]`; URL constrained to `pixabay.com`/`cdn.pixabay.com` | Guarded by `checkSecurity()` |
| Backup / favorite writers | `controller.php:204`, `helpers/pagebuilderck.php:287` | No — fixed paths under `backup/` / `favorites/`, `.pbck` extension | Guarded by `checkSecurity()` |
| `frontedition.createModule()` / `savemodules()` | `site/controllers/frontedition.php` | N/A — DB writes only (no file write) | Method body has no `checkSecurity()`, but Layer 1 `execute()` catches `createModule` (non-`display`) → 403 for guests |

The `display` task exemption was specifically tested (`task=display&view=browse`): on the fixed version it returns 403 (view-level ACL) and, critically, writes **no** `.php` file to the media tree (PHP-file count in `media/com_pagebuilderck` stayed at 0 across all Phase-1 probes). So the exemption is not a bypass.

## Is the fix complete?

**Yes — the fix is comprehensive for the file-upload-to-RCE root cause.** The central `execute()` authorization check covers every non-`display` controller task (including all alternate ajax endpoints surveyed), and `ajaxAddPicture` — the only sink that can deliver a `.php` web shell — is protected at both the dispatch level (Layer 1) and the method level (Layer 2). No out-of-`execute()` write path can produce a `.php` file with attacker-controlled content.

## Behavior before vs. after the fix (empirically observed)

| Probe (task) | Vulnerable (≤ 3.5.10, fix reverted) | Fixed (3.6.1) |
|---|---|---|
| `browse.ajaxAddPicture` (upload `.php`) | HTTP 200, **RCE confirmed** (`RCE_PROOF_…www-data`), shell written to `media/com_pagebuilderck/gfonts/exploit_shell.php` | HTTP **403**, no file written, no RCE marker |
| `browse.ajaxCreateFolder` | HTTP 200 (reachable) | HTTP 403 |
| `browse.getFiles` | (reachable) | HTTP 403 |
| `fonts.save` (local=1) | HTTP 403* | HTTP 403 |
| `pixabay.upload` | HTTP 200 (reachable, img ext forced) | HTTP 403 |
| `frontedition.createModule` | HTTP 500 (reachable, DB error) | HTTP 500 |
| `page.save` | (reachable) | HTTP 403 |
| `display` (view=browse) | renders, no `.php` | HTTP 403, no `.php` |
| `ajaxParamsCall` (gadget → writeLabelsFile) | HTTP 200 (reachable, `.txt` forced) | HTTP 403 |
| `browse.ajaxAddPicture` via `/administrator/index.php` | — | HTTP 200 (admin login redirect, no upload) |

\* `fonts.save` returned 403 on the vulnerable state too — it has an additional guard (e.g. token/user check) independent of `checkSecurity`; regardless, it forces font extensions and cannot deliver `.php`, so it is not an RCE path.

On the **fixed** version, across all probes the PHP-file count under `media/com_pagebuilderck` remained **0** and no `RCE_PROOF_` marker was written (`rce_marker_phase1 = none`). On the **vulnerable** version, the web shell was written and executed (`rce_marker_phase2 = …/exploit_shell.php`). This confirms the fix blocks the RCE and that the test harness correctly recreates the vulnerable state.

## Target threat-model scope

Page Builder CK is a Joomla site-builder extension. Its ajax controllers are designed to be called from the authenticated administrator/editor UI. The CSRF token (`checkAjaxToken` / `Session::checkToken('get')`) was the **only** pre-fix gate, and because Joomla embeds the session CSRF token in public pages (`<script type="application/json" class="joomla-script-options new">… "csrf.token":"…" …`), that gate is trivially satisfied by unauthenticated visitors. The fix correctly recognizes that a CSRF token is not an authorization boundary and adds a real `core.edit` authorization check. No `SECURITY.md` or out-of-scope statement in the extension contradicts treating unauthenticated arbitrary file upload to RCE as a security vulnerability; this matches the CVE's critical (CVSS 10.0) classification.

## Recommendation (for the Coding stage — no gap to close for RCE)

The fix is complete for the RCE root cause. As defense-in-depth (not because a bypass exists), the maintainer could additionally:
1. **Enforce an extension allowlist** in `CKBrowse::ajaxAddPicture()` (uncomment and correct the commented-out `getExt` check) so that even if an authorization check were ever regressed, a `.php` upload would be rejected at the sink.
2. **Restrict the `path` parameter** to the configured images directory (reject `..` traversal and absolute paths) in `ajaxAddPicture` and `createFolder`.
3. **Add `checkSecurity()` to `frontedition::createModule()`/`savemodules()`** method bodies for consistency (they currently rely solely on Layer 1).
4. Keep `CKFile::makeSafe()` but do not treat it as a security boundary (it sanitizes filename characters but does not block executable extensions).
