Joomla, Author overwrites media without core.edit
CVE-2026-48947 taught the webservice media API to require core.edit on override. The site JSON controller still passes override into createFile. Default Author can replace images/joomla_black.png. Labbed 6.1.3.
- Name
- Joomla, Author overwrites media without core.edit
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- medium
- Disclosure Status
- public
- Vendor
- Joomla
- Affected
- Joomla CMS 6.1.3 (0f2d4de) site com_media JSON API. Default Author (group 3). No API token. Webservice /api/v1/media/files already checks core.edit.
- Published
- 12 Oct 2026
- Updated
- 12 Oct 2026
- Tags
- 0-day, joomla, privilege-leftover, media, cwe-863, cwe-639, authenticated
PUT learned. POST did not.
I am @abraxas_null. The proof of concept is on GitHub: abraxas/joomla-media-override (loopback client). The lab stack is lab/: docker-compose.yml, run.sh, poc.py. Authorized lab only. It talks to loopback.
This is Joomla CMS 6.1.3 (0f2d4de4729eb951e0878fec98c7990da07915b2). No CVE yet. CWE-863 / CWE-639. 6.5 Medium (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N). Remote privilege leftover. Default Author. No API token. Not a .php upload. Not Super User.
CVE-2026-48947 taught /api/index.php/v1/media/files to require core.edit when override is set. The site JSON API still does not. Default Author has core.create on com_media and no core.edit. The site dispatcher treats core.create as enough and loads the administrator ApiController. postFiles() then passes override straight into createFile(). PUT still 403s. POST without override still 409s. The leftover is POST with override: true.
Same pin: levels API, menu items API, articles catid.
Why Joomla
Same 6.1.3 hunt. Uploads and webservices ACL both pointed at com_media. When a CVE names "webservice endpoints," I open the site dispatcher. Joomla has a habit of loading the administrator API controller from the frontend once core.create is true.
Pinning 6.1.3
Tag 6.1.3, 0f2d4de4729eb951e0878fec98c7990da07915b2. Image joomla:6.1.3-php8.3-apache plus mariadb:11.4. This leftover does not need core.login.api. Default Author is enough. That is the difference from the three API-token leftovers on this pin.
The skip list is half the hunt
- CVE-2026-48947 webservice media overwrite.
api/.../MediaController.phpallowAddcheckscore.editwhenoverrideis set. Closed. This post is the internal controller. - CVE-2026-73373 SHTML in the forbidden list. Closed. Extension denylist is not this leftover.
- CVE-2026-71574 mutating webservice
core.manage. Different surface. - Groups
core.admin. Closed. - MFA 73337. Closed.
I did not re-file the webservice path.
Surfaces that returned nothing
PUT on the same file as Author: 403. putFiles() already requires core.edit. POST without override when the name exists: 409, original SHA unchanged. That is create-if-absent working. jos_assets for com_media has no core.edit for group 3. The Author stays Author. .php is not this lab. The denylist is inferred closed.
Webservice /api/index.php/v1/media/files with override is the patched path. I did not treat a 403 there as this finding.
The leftover
Default Author on com_media:
{"core.admin":{"7":1},"core.manage":{"6":1},"core.create":{"3":1},"core.delete":{"5":1}}Site Dispatcher.php 56-80 lets core.create through and loads the administrator ApiController. postFiles() 187-204:
if (!$this->app->getIdentity()->authorise('core.create', 'com_media')) {
throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED'), 403);
}
$override = $content->get('override', false);
if ($mediaContent) {
$name = $this->getModel()->createFile($adapter, $name, $path, $mediaContent, $override);Fixed webservice:
if ($this->input->json->get('override', false)) {
return $user->authorise('core.edit', 'com_media');
}One check. They put it in one controller.
What an attacker can do
Log in as a stock Author and replace a file that is already on the site. Logo, header, an image linked from an article. Visitors keep hitting the same public URL and get the new bytes. This is defacement / bait-and-switch of stored media, not a site takeover.
Class: Privilege leftover. Reach: Remote.
| Privilege | Method / path | Kind | Lab-proven or inferred |
|---|---|---|---|
| Overwrite an existing media file | Site POST /index.php?option=com_media&task=api.files&format=json&path=local-images:/ with JSON override: true |
integrity | Lab-proven. HTTP 200, disk SHA changed, public GET /images/joomla_black.png served the new PNG that carries JOOMLA-MEDIA-OVERRIDE-WITNESS |
| Deface whatever still points at that URL | Same public path, no cache-bust | integrity | Lab-proven the public bytes flipped. Inferred for other adapters under images/ |
| Create a new allowed file | Same POST without override | intended | Author has core.create on com_media |
| Edit via PUT | PUT same task, path includes the filename |
n/a | Lab-proven closed. HTTP 403 |
| Create when the name exists, no override | POST override: false |
n/a | Lab-proven closed. HTTP 409 |
Upload .php or other denied types |
createFile / isMediaFile denylist |
n/a | Inferred closed. This lab only sends a PNG |
| Call the webservice media API with override | /api/index.php/v1/media/files |
n/a | Inferred closed. CVE-2026-48947 |
| Super User / RCE | n/a | n/a | Closed. Author stays Author |
The lab
Loopback 127.0.0.1:19540. Auto-install 6.1.3. CLI user:add a default Author. Confirm com_media has no core.edit for group 3. Frontend login. CSRF from csrf.token. POST without override: 409. PUT: 403. POST override: true with a 1x1 PNG that embeds the witness in a tEXt chunk: 200, disk SHA changes, public GET returns the witness.
SUCCESS joomla-media-override post-http=200 override=yes bytes-changed=yes put-http=403 no-edit=yes conflict-http=409 JOOMLA-MEDIA-OVERRIDE-WITNESSpython3 joomla-media-override-Abraxas-Labs.pyor cd lab && ./run.sh. Loopback only.
Wrong turns, in order
I treated PUT as the leftover. PUT is already 403. The SA named overwrite. Overwrite on this controller is POST plus override.
I treated the webservice path as still open. CVE-2026-48947 closed it. The leftover is the site/admin JSON controller the frontend dispatcher loads.
I read 409 without override as a miss. That is the intended create-if-absent path. The SHA is unchanged until the override POST.
Theatre: a reverse shell. The oracle is POST override 200 plus PUT 403 plus public GET of the witness PNG.
The fix
Same override -> core.edit check as MediaController::allowAdd inside ApiController::postFiles() before createFile(). Keep PUT on core.edit.
Pack
- GitHub: abraxas/joomla-media-override
- Loopback client: joomla-media-override-Abraxas-Labs.py
- Lab: lab/
Abraxas Labs · abraxaslabs.tech · github.com/abraxas · @abraxas_null · abraxas.null@proton.me