# CVE-2026-48908 — Root Cause Analysis

## Summary

SP Page Builder for Joomla (`com_sppagebuilder`) exposes the controller task
`index.php?option=com_sppagebuilder&task=asset.uploadCustomIcon` to upload a
custom icon-font package. In versions **1.0.0 – 6.6.1** the task is reachable
without authentication, authorization, or a valid anti-CSRF token. The uploaded
ZIP archive is extracted and copied into the web root under
`/media/com_sppagebuilder/assets/iconfont/<name>/`, including its `fonts/`
subdirectory. Because the archive entries are not validated against an
extension allow-list, an attacker can package a web shell and a `.htaccess`
file that registers an uppercase `.PHP` extension with the PHP handler, then
execute the shell over HTTP. This yields unauthenticated remote code execution
as the web-server user.

## Impact

- **Package / component:** SP Page Builder (`com_sppagebuilder`) by JoomShaper —
  `site/controllers/asset.php` (`SppagebuilderControllerAsset::uploadCustomIcon`).
- **Affected versions:** 1.0.0 – 6.6.1.
- **Patched version:** 6.6.2.
- **Risk level:** Critical (CWE-284 → CWE-434 Unrestricted Upload of File with
  Dangerous Type; advisory CVSS 4.0 10.0). Any unauthenticated remote attacker
  can upload and execute attacker-controlled PHP code on the Joomla host.

## Impact Parity

- **Disclosed / claimed maximum impact:** Unauthenticated arbitrary file upload
  leading to remote code execution.
- **Reproduced impact from this run:** Full parity.
  1. **Unauthenticated upload:** The PoC sent a multipart `custom_icon` ZIP to
     the `asset.uploadCustomIcon` task with no Joomla session cookie and no CSRF
     token; the server accepted it and extracted the contents under the web root.
  2. **Code execution:** The uploaded `.htaccess` plus uppercase `.PHP` shell
     executed the PoC’s `echo 7*6` marker (`SPPB-RCE-42`) and then `id`,
     returning `uid=33(www-data) gid=33(www-data) groups=33(www-data)`.
  3. **Negative control:** The same PoC against the fixed 6.6.2 install received
     HTTP 403 with the message `You require admin access`, confirming the patch
     closes the path.
- **Parity:** `full`.
- **Not demonstrated:** Further post-exploitation (e.g., privilege escalation
  beyond `www-data`) is not part of the CVE claim.

## Root Cause

In the vulnerable build the `SppagebuilderControllerAsset` class (in
`site/controllers/asset.php`) has no authentication, authorization, or CSRF
logic in its constructor or in the `uploadCustomIcon()` method. The method
reads the uploaded `custom_icon` file, validates only basic size/length limits,
writes it to a temporary ZIP, and calls the local `unpack()` method:

```php
$zip_file = $tmp_path . '/builderCustomIcon.zip';
...
if (File::upload($tmp_src, $zip_file, false, true))
{
    $extract = $this->unpack($zip_file);
    ...
    Folder::copy($extract_path . '/' . $font_path, $rootPath . '/' . $fontFamily . '/fonts');
    File::copy($extract_path . '/' . $font_css, $rootPath . '/' . $fontFamily . '/' . $font_css);
    ...
}
```

`unpack()` uses `Joomla\Archive\Archive::extract()` to unpack the ZIP into a
subdirectory of the Joomla `tmp_path`. The controller then copies the entire
`fonts/` (and CSS) contents into the public web-root directory
`JPATH_ROOT/media/com_sppagebuilder/assets/iconfont/<fontFamily>/`. No
extension allow-list is enforced on the archive entries before copying, so the
attacker can include arbitrary files such as `.htaccess` and `.PHP`.

The server-side extension filter that *does* exist is a case-sensitive
block-list (rejects lowercase `.php`, `.php3`–`.php8`, `.pht`, `.phtml`,
`.phar`, etc.), but the block-list misses uppercase `.PHP` and `.htaccess`.
A default Apache PHP handler (`<FilesMatch "\.php$">`) does not execute
uppercase `.PHP`, so the PoC also drops a `fonts/.htaccess` containing:

```apache
AddType application/x-httpd-php .PHP
```

Where `AllowOverride` is permitted, this reconfigures the directory to treat
`.PHP` as PHP. The same HTTP request that uploaded the shell can then fetch it
from the web-accessible iconfont directory and execute it.

The 6.6.2 fix adds a constructor to `SppagebuilderControllerAsset` that
blocks unauthenticated/unauthorized requests before any method runs:

```php
public function __construct($config = [])
{
    parent::__construct($config);
    $user = Factory::getUser();
    $authorised = $user->authorise('core.admin', 'com_sppagebuilder')
                   || $user->authorise('core.manage', 'com_sppagebuilder');
    if (!$authorised)
    {
        ...
        $this->sendResponse($response, 403, true);
    }
    if (!$user->id)
    {
        ...
        $this->sendResponse($response, 401, true);
    }
    if (!Session::checkToken())
    {
        ...
        $this->sendResponse($response, 403, true);
    }
}
```

This requires the user to be logged in, to have admin/manage privileges for the
component, and to present a valid Joomla CSRF token, making the upload task
unreachable from an unauthenticated attacker.

## Reproduction Steps

1. **Script:** `bundle/repro/reproduction_steps.sh` (self-contained; exits 0 on
   confirmation, non-zero on failure).
2. **What it does:**
   - Reads the durable project cache if one is configured, otherwise creates
     `bundle/artifacts/sppb`.
   - Clones the public PoC `https://github.com/papageo75/CVE-2026-48908-PoC.git`
     once and reuses it across runs.
   - For each role (`vuln` and `fixed`):
     - Builds a Docker Compose stack with `joomla:5.2-php8.2-apache` and
       `mysql:8.0`, downloads the SP Page Builder ZIP (`6.6.1` for the
       vulnerable role, `6.6.2` for the fixed role), and installs it through
       Joomla’s web installer using the real administrator workflow.
     - Waits for the Joomla health check to return HTTP 200.
     - Runs the PoC against the running container from the same Docker network
       (`http://joomla`), using the real `asset.uploadCustomIcon` HTTP endpoint.
   - Writes `bundle/repro/runtime_manifest.json` and per-role evidence logs under
     `bundle/logs/`.
3. **Expected evidence of reproduction:**
   - Vulnerable (6.6.1): PoC prints `CODE EXECUTION CONFIRMED via '.htaccess+.PHP'`
     and the output of `id` (`uid=33(www-data)`).
   - Fixed (6.6.2): PoC prints `TARGET NOT VULNERABLE — SP Page Builder is patched`
     and the first upload attempt returns `403 'require admin' = patched`.

## Evidence

Log file locations (written by `reproduction_steps.sh`):

- `bundle/logs/vuln_compose.log` — Docker stack creation for the vulnerable run.
- `bundle/logs/vuln_poc.log` — PoC transcript showing RCE on 6.6.1.
- `bundle/logs/fixed_compose.log` — Docker stack creation for the fixed run.
- `bundle/logs/fixed_poc.log` — PoC transcript showing 6.6.2 rejects the upload.
- `bundle/logs/reproduction_steps.log` — full transcript of the latest run.
- `bundle/repro/runtime_manifest.json` — structured runtime evidence.

Key excerpts (from the latest verification run):

**Vulnerable 6.6.1 — code execution confirmed:**
```
[*] try .htaccess+.PHP  -> EXECUTED
[+] CODE EXECUTION CONFIRMED via '.htaccess+.PHP' (echo 7*6 -> 42)
[+] webshell : http://joomla/media/com_sppagebuilder/assets/iconfont/.../fonts/....PHP?t=...&c=<cmd>
[*] running: id
------------------------------------------------------------
uid=33(www-data) gid=33(www-data) groups=33(www-data)
------------------------------------------------------------
```

**Fixed 6.6.2 — upload now requires admin authentication:**
```
[*] try .php            -> 403 'require admin' = patched
[-] TARGET NOT VULNERABLE — SP Page Builder is patched (6.6.2+); the upload task now requires authenticated admin access.
```

**Environment:** Joomla 5.2 with PHP 8.2 (Apache), MySQL 8.0, SP Page Builder
6.6.1 (vulnerable) and 6.6.2 (fixed), running in Docker on a single custom
network. The PoC is executed from an ephemeral `python:3-slim` container on the
same network, with no session state or credentials.

## Recommendations / Next Steps

- **Upgrade** to SP Page Builder 6.6.2 or later. The 6.6.2 patch adds the
  constructor checks described above, which are the primary fix.
- **Defence in depth on the web server:**
  - Disable PHP execution in directories that are only expected to host static
    assets (`/media/`, `/images/`, `/templates/*/css`, etc.).
  - Set `AllowOverride None` on these directories so an uploaded `.htaccess` cannot
    re-register handlers.
  - Use a case-insensitive **extension allow-list** (e.g., only `.ttf`, `.woff`,
    `.css`, `.json`) on uploaded archive contents; never rely on a block-list of
    dangerous extensions.
- **Assume-breach review:** search for unexpected `.php`/`.PHP`, `.htaccess`,
  and newly created directories under
  `/media/com_sppagebuilder/assets/iconfont/`, and look for new Super User
  accounts or planted web shells.
- **Backport:** if a site cannot upgrade immediately, apply the 6.6.2
  constructor guard (authorization + user session + `Session::checkToken()`) to
  `site/controllers/asset.php`.

## Additional Notes

- **Idempotency:** The reproduction script was run twice consecutively; both
  runs exited 0 with the same positive RCE evidence on the vulnerable build and
  the same 403 rejection on the fixed build.
- **Limitations:** The full RCE chain depends on the Apache configuration
  permitting `AllowOverride` in the iconfont directory and on PHP being enabled
  there. If `AllowOverride` is disabled or the case-sensitive `.PHP` handler is
  blocked, the bug still permits unauthenticated file write but not direct RCE;
  this is still an unauthenticated arbitrary file upload vulnerability.
- **References:**
  - NVD — https://nvd.nist.gov/vuln/detail/CVE-2026-48908
  - Technical write-up — https://mysites.guru/blog/sp-page-builder-zero-day-uploadcustomicon-rce/
  - PoC repository — https://github.com/papageo75/CVE-2026-48908-PoC
  - Vendor — https://www.joomshaper.com/page-builder
