{"repro_id":"REPRO-2026-00269","version":6,"title":"SP Page Builder for Joomla allows unauthenticated arbitrary file upload via asset.uploadCustomIcon, enabling PHP upload and remote code execution.","repro_type":"security","status":"published","severity":"critical","cvss_score":10.0,"description":"SP Page Builder (com_sppagebuilder) for Joomla exposes the `asset.uploadCustomIcon` task without authentication or CSRF checks, allowing unauthenticated attackers to upload a ZIP archive that is extracted under the web root. By including PHP (or by bypassing extension filters), attackers can achieve remote code execution.","root_cause":"# CVE-2026-48908 — Root Cause Analysis\n\n## Summary\n\nSP Page Builder for Joomla (`com_sppagebuilder`) exposes the controller task\n`index.php?option=com_sppagebuilder&task=asset.uploadCustomIcon` to upload a\ncustom icon-font package. In versions **1.0.0 – 6.6.1** the task is reachable\nwithout authentication, authorization, or a valid anti-CSRF token. The uploaded\nZIP archive is extracted and copied into the web root under\n`/media/com_sppagebuilder/assets/iconfont/<name>/`, including its `fonts/`\nsubdirectory. Because the archive entries are not validated against an\nextension allow-list, an attacker can package a web shell and a `.htaccess`\nfile that registers an uppercase `.PHP` extension with the PHP handler, then\nexecute the shell over HTTP. This yields unauthenticated remote code execution\nas the web-server user.\n\n## Impact\n\n- **Package / component:** SP Page Builder (`com_sppagebuilder`) by JoomShaper —\n  `site/controllers/asset.php` (`SppagebuilderControllerAsset::uploadCustomIcon`).\n- **Affected versions:** 1.0.0 – 6.6.1.\n- **Patched version:** 6.6.2.\n- **Risk level:** Critical (CWE-284 → CWE-434 Unrestricted Upload of File with\n  Dangerous Type; advisory CVSS 4.0 10.0). Any unauthenticated remote attacker\n  can upload and execute attacker-controlled PHP code on the Joomla host.\n\n## Impact Parity\n\n- **Disclosed / claimed maximum impact:** Unauthenticated arbitrary file upload\n  leading to remote code execution.\n- **Reproduced impact from this run:** Full parity.\n  1. **Unauthenticated upload:** The PoC sent a multipart `custom_icon` ZIP to\n     the `asset.uploadCustomIcon` task with no Joomla session cookie and no CSRF\n     token; the server accepted it and extracted the contents under the web root.\n  2. **Code execution:** The uploaded `.htaccess` plus uppercase `.PHP` shell\n     executed the PoC’s `echo 7*6` marker (`SPPB-RCE-42`) and then `id`,\n     returning `uid=33(www-data) gid=33(www-data) groups=33(www-data)`.\n  3. **Negative control:** The same PoC against the fixed 6.6.2 install received\n     HTTP 403 with the message `You require admin access`, confirming the patch\n     closes the path.\n- **Parity:** `full`.\n- **Not demonstrated:** Further post-exploitation (e.g., privilege escalation\n  beyond `www-data`) is not part of the CVE claim.\n\n## Root Cause\n\nIn the vulnerable build the `SppagebuilderControllerAsset` class (in\n`site/controllers/asset.php`) has no authentication, authorization, or CSRF\nlogic in its constructor or in the `uploadCustomIcon()` method. The method\nreads the uploaded `custom_icon` file, validates only basic size/length limits,\nwrites it to a temporary ZIP, and calls the local `unpack()` method:\n\n```php\n$zip_file = $tmp_path . '/builderCustomIcon.zip';\n...\nif (File::upload($tmp_src, $zip_file, false, true))\n{\n    $extract = $this->unpack($zip_file);\n    ...\n    Folder::copy($extract_path . '/' . $font_path, $rootPath . '/' . $fontFamily . '/fonts');\n    File::copy($extract_path . '/' . $font_css, $rootPath . '/' . $fontFamily . '/' . $font_css);\n    ...\n}\n```\n\n`unpack()` uses `Joomla\\Archive\\Archive::extract()` to unpack the ZIP into a\nsubdirectory of the Joomla `tmp_path`. The controller then copies the entire\n`fonts/` (and CSS) contents into the public web-root directory\n`JPATH_ROOT/media/com_sppagebuilder/assets/iconfont/<fontFamily>/`. No\nextension allow-list is enforced on the archive entries before copying, so the\nattacker can include arbitrary files such as `.htaccess` and `.PHP`.\n\nThe server-side extension filter that *does* exist is a case-sensitive\nblock-list (rejects lowercase `.php`, `.php3`–`.php8`, `.pht`, `.phtml`,\n`.phar`, etc.), but the block-list misses uppercase `.PHP` and `.htaccess`.\nA default Apache PHP handler (`<FilesMatch \"\\.php$\">`) does not execute\nuppercase `.PHP`, so the PoC also drops a `fonts/.htaccess` containing:\n\n```apache\nAddType application/x-httpd-php .PHP\n```\n\nWhere `AllowOverride` is permitted, this reconfigures the directory to treat\n`.PHP` as PHP. The same HTTP request that uploaded the shell can then fetch it\nfrom the web-accessible iconfont directory and execute it.\n\nThe 6.6.2 fix adds a constructor to `SppagebuilderControllerAsset` that\nblocks unauthenticated/unauthorized requests before any method runs:\n\n```php\npublic function __construct($config = [])\n{\n    parent::__construct($config);\n    $user = Factory::getUser();\n    $authorised = $user->authorise('core.admin', 'com_sppagebuilder')\n                   || $user->authorise('core.manage', 'com_sppagebuilder');\n    if (!$authorised)\n    {\n        ...\n        $this->sendResponse($response, 403, true);\n    }\n    if (!$user->id)\n    {\n        ...\n        $this->sendResponse($response, 401, true);\n    }\n    if (!Session::checkToken())\n    {\n        ...\n        $this->sendResponse($response, 403, true);\n    }\n}\n```\n\nThis requires the user to be logged in, to have admin/manage privileges for the\ncomponent, and to present a valid Joomla CSRF token, making the upload task\nunreachable from an unauthenticated attacker.\n\n## Reproduction Steps\n\n1. **Script:** `bundle/repro/reproduction_steps.sh` (self-contained; exits 0 on\n   confirmation, non-zero on failure).\n2. **What it does:**\n   - Reads the durable project cache if one is configured, otherwise creates\n     `bundle/artifacts/sppb`.\n   - Clones the public PoC `https://github.com/papageo75/CVE-2026-48908-PoC.git`\n     once and reuses it across runs.\n   - For each role (`vuln` and `fixed`):\n     - Builds a Docker Compose stack with `joomla:5.2-php8.2-apache` and\n       `mysql:8.0`, downloads the SP Page Builder ZIP (`6.6.1` for the\n       vulnerable role, `6.6.2` for the fixed role), and installs it through\n       Joomla’s web installer using the real administrator workflow.\n     - Waits for the Joomla health check to return HTTP 200.\n     - Runs the PoC against the running container from the same Docker network\n       (`http://joomla`), using the real `asset.uploadCustomIcon` HTTP endpoint.\n   - Writes `bundle/repro/runtime_manifest.json` and per-role evidence logs under\n     `bundle/logs/`.\n3. **Expected evidence of reproduction:**\n   - Vulnerable (6.6.1): PoC prints `CODE EXECUTION CONFIRMED via '.htaccess+.PHP'`\n     and the output of `id` (`uid=33(www-data)`).\n   - Fixed (6.6.2): PoC prints `TARGET NOT VULNERABLE — SP Page Builder is patched`\n     and the first upload attempt returns `403 'require admin' = patched`.\n\n## Evidence\n\nLog file locations (written by `reproduction_steps.sh`):\n\n- `bundle/logs/vuln_compose.log` — Docker stack creation for the vulnerable run.\n- `bundle/logs/vuln_poc.log` — PoC transcript showing RCE on 6.6.1.\n- `bundle/logs/fixed_compose.log` — Docker stack creation for the fixed run.\n- `bundle/logs/fixed_poc.log` — PoC transcript showing 6.6.2 rejects the upload.\n- `bundle/logs/reproduction_steps.log` — full transcript of the latest run.\n- `bundle/repro/runtime_manifest.json` — structured runtime evidence.\n\nKey excerpts (from the latest verification run):\n\n**Vulnerable 6.6.1 — code execution confirmed:**\n```\n[*] try .htaccess+.PHP  -> EXECUTED\n[+] CODE EXECUTION CONFIRMED via '.htaccess+.PHP' (echo 7*6 -> 42)\n[+] webshell : http://joomla/media/com_sppagebuilder/assets/iconfont/.../fonts/....PHP?t=...&c=<cmd>\n[*] running: id\n------------------------------------------------------------\nuid=33(www-data) gid=33(www-data) groups=33(www-data)\n------------------------------------------------------------\n```\n\n**Fixed 6.6.2 — upload now requires admin authentication:**\n```\n[*] try .php            -> 403 'require admin' = patched\n[-] TARGET NOT VULNERABLE — SP Page Builder is patched (6.6.2+); the upload task now requires authenticated admin access.\n```\n\n**Environment:** Joomla 5.2 with PHP 8.2 (Apache), MySQL 8.0, SP Page Builder\n6.6.1 (vulnerable) and 6.6.2 (fixed), running in Docker on a single custom\nnetwork. The PoC is executed from an ephemeral `python:3-slim` container on the\nsame network, with no session state or credentials.\n\n## Recommendations / Next Steps\n\n- **Upgrade** to SP Page Builder 6.6.2 or later. The 6.6.2 patch adds the\n  constructor checks described above, which are the primary fix.\n- **Defence in depth on the web server:**\n  - Disable PHP execution in directories that are only expected to host static\n    assets (`/media/`, `/images/`, `/templates/*/css`, etc.).\n  - Set `AllowOverride None` on these directories so an uploaded `.htaccess` cannot\n    re-register handlers.\n  - Use a case-insensitive **extension allow-list** (e.g., only `.ttf`, `.woff`,\n    `.css`, `.json`) on uploaded archive contents; never rely on a block-list of\n    dangerous extensions.\n- **Assume-breach review:** search for unexpected `.php`/`.PHP`, `.htaccess`,\n  and newly created directories under\n  `/media/com_sppagebuilder/assets/iconfont/`, and look for new Super User\n  accounts or planted web shells.\n- **Backport:** if a site cannot upgrade immediately, apply the 6.6.2\n  constructor guard (authorization + user session + `Session::checkToken()`) to\n  `site/controllers/asset.php`.\n\n## Additional Notes\n\n- **Idempotency:** The reproduction script was run twice consecutively; both\n  runs exited 0 with the same positive RCE evidence on the vulnerable build and\n  the same 403 rejection on the fixed build.\n- **Limitations:** The full RCE chain depends on the Apache configuration\n  permitting `AllowOverride` in the iconfont directory and on PHP being enabled\n  there. If `AllowOverride` is disabled or the case-sensitive `.PHP` handler is\n  blocked, the bug still permits unauthenticated file write but not direct RCE;\n  this is still an unauthenticated arbitrary file upload vulnerability.\n- **References:**\n  - NVD — https://nvd.nist.gov/vuln/detail/CVE-2026-48908\n  - Technical write-up — https://mysites.guru/blog/sp-page-builder-zero-day-uploadcustomicon-rce/\n  - PoC repository — https://github.com/papageo75/CVE-2026-48908-PoC\n  - Vendor — https://www.joomshaper.com/page-builder\n","cve_id":"CVE-2026-48908","cwe_id":"CWE-284 Improper Access Control; NVD also lists CWE-434 Unrestricted Upload of File with Dangerous Type","source_url":"https://nvd.nist.gov/vuln/detail/CVE-2026-48908, https://mysites.guru/blog/sp-page-builder-zero-day-uploadcustomicon-rce/, https://github.com/papageo75/CVE-2026-48908-PoC, https://www.joomshaper.com/page-builder","package":{"name":"SP Page Builder (com_sppagebuilder)","ecosystem":"Joomla extension","affected_versions":"1.0.0 through 6.6.1","fixed_version":"6.6.2"},"reproduced_at":"2026-07-08T04:49:25.740650+00:00","duration_secs":4671.0,"tool_calls":489,"handoffs":2,"total_cost_usd":10.728627700000004,"agent_costs":{"hypothesis_generator":0.05573385,"judge":0.485224,"repro":8.562753920000002,"support":0.02669125,"vuln_variant":1.5982246799999995},"cost_breakdown":{"hypothesis_generator":{"accounts/fireworks/models/kimi-k2p7-code":0.05573385},"judge":{"gpt-5.5":0.485224},"repro":{"accounts/fireworks/models/kimi-k2p7-code":8.562753920000002},"support":{"accounts/fireworks/models/kimi-k2p7-code":0.02669125},"vuln_variant":{"accounts/fireworks/models/kimi-k2p7-code":1.5982246799999995}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-08T04:49:49.636413+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":9057,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":9983,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":14783,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":9544,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":7373,"category":"other"},{"path":"bundle/artifact_promotion_report.json","filename":"artifact_promotion_report.json","size":10374,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1479,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":725,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":545,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":6283,"category":"log"},{"path":"bundle/logs/vuln_poc.log","filename":"vuln_poc.log","size":1667,"category":"log"},{"path":"bundle/logs/fixed_poc.log","filename":"fixed_poc.log","size":652,"category":"log"},{"path":"bundle/logs/vuln_compose.log","filename":"vuln_compose.log","size":1072,"category":"log"},{"path":"bundle/logs/fixed_compose.log","filename":"fixed_compose.log","size":1097,"category":"log"},{"path":"bundle/logs/variant_vuln_test.log","filename":"variant_vuln_test.log","size":1661,"category":"log"},{"path":"bundle/logs/variant_fixed_test.log","filename":"variant_fixed_test.log","size":1172,"category":"log"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":3082,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":980,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":6992,"category":"documentation"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":682,"category":"other"},{"path":"bundle/logs/variant_vuln_poc_control.log","filename":"variant_vuln_poc_control.log","size":1667,"category":"log"},{"path":"bundle/logs/variant_fixed_poc_control.log","filename":"variant_fixed_poc_control.log","size":652,"category":"log"}]}