Drupal 11.4.8 - High - Remote - Upload - CKEditor 5 image upload still skips FileIsImage
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · drupal-ckeditor-fileisimage
Class: Unrestricted upload / stored XSS Reach: Remote
Drupal core 11.4.8 (61044508) - Drupal
SA-CORE-2026-009 added FileIsImage on JSON:API and REST image fields. CKEditor5ImageController::getImageUploadValidators still returns FileExtension, FileSizeLimit, and FileImageDimensions only. FileImageDimensions returns without a violation when the file is not a valid image. Filename extension is the remaining check.
A user of a text format with CKEditor 5 image upload can POST non-image bytes named x.png to /ckeditor5/upload-image/{editor} and get 201 plus a public files URL.
| ID | no CVE yet |
| Class | Unrestricted upload / stored XSS if sniffed |
| Reach | Remote (authenticated format user) |
| CWE | CWE-434, CWE-79 |
| CVSS | Medium: 5.4 CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N |
| Product | Drupal core |
| Affected | 11.4.8 (61044508071749629aaf3380aa85971e94df9e84) |
| Auth | authenticated user of a text format with CKEditor 5 image upload (basic_html / full_html on Standard) |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 only |
Needs an account that can use a format whose editor has image_upload.status true. On Standard that is basic_html and full_html. A complete stranger with no account cannot hit this route (_entity_access: editor.use plus CSRF).
| Privilege | Method / sink | Kind | Lab-proven or inferred |
|---|---|---|---|
Write non-image bytes to a public .png under sites/default/files/inline-images/ | POST /ckeditor5/upload-image/{editor} multipart upload | integrity | Lab-proven. x.png returned 201 and url=/sites/default/files/inline-images/x.png |
| Read those bytes back at the public files URL | GET of the JSON url | recon | Lab-proven. 200, 49 bytes, body contained DRUPAL-CKEDITOR-FILEISIMAGE-WITNESS |
| Stored XSS in a visitor if the web server or browser sniffs the file as HTML/JS | Same public URL, Content-Type not image | integrity (visitor origin) | Inferred. This lab did not change Apache MIME. Drupal stored the bytes. |
| Upload a real image through the same route | Control control.png 1x1 PNG | intended | Lab-proven. 201 |
Closed on this leftover: PHP/RCE via .php (lab-proven 422 FileExtension); unauthenticated upload; JSON:API/REST ImageItem (those paths already add FileIsImage); LPE; a shell on the default image-extension allowlist.
I pinned Drupal 11.4.8 after the 2026 SA-CORE wave. JSON:API already adds the missing constraint:
if (\is_a($field_definition->getItemDefinition()->getClass(), ImageItem::class, TRUE) && !array_key_exists('FileIsImage', $validators)) {
$validators['FileIsImage'] = [];
}
REST FileUploadResource.php does the same. CKEditor 5 was left on filename + size + dimensions:
return [
'FileExtension' => [
'extensions' => implode(' ', $allowed_extensions),
],
'FileSizeLimit' => [
'fileLimit' => $max_filesize,
],
'FileImageDimensions' => [
'maxDimensions' => $max_dimensions,
],
];
FileImageDimensionsConstraintValidator returns without a violation when the file is not a valid image. Hub drupal:11.4 floated to 11.4.9; this pin is composer-exact drupal/recommended-project:11.4.8. 11.4.0 dropped Article from Standard, so the image applies core/recipes/article_content_type after install so /node/add/article loads CKEditor 5.
Wrong turns already recorded: treating FileImageDimensions as a MIME check (it is dimensions on a valid image, and a no-op otherwise); claiming a PHP shell on the default allowlist (.php is 422); using the floating hub tag and watching 11.4.9 come up. Theatre: a reverse shell. The oracle is 201 on fake x.png plus 422 on the same bytes named x.php.
Same pin leftovers: oEmbed second-hop SSRF.
python3 drupal-ckeditor-fileisimage-Abraxas-Labs.py
or
cd lab
./run.sh
Target only http://127.0.0.1:19500. Live Drupal 11.4.8 Standard on php:8.4-apache-trixie, SQLite.
SUCCESS drupal-ckeditor-fileisimage png-http=201 php-http=422 control-png-http=201 editor=basic_html stored-http=200 stored-has-witness=yes url=/sites/default/files/inline-images/x.png DRUPAL-CKEDITOR-FILEISIMAGE-WITNESS
Add 'FileIsImage' => [] to CKEditor5ImageController::getImageUploadValidators. Keep FileExtension so .php stays 422. Image media types in Media Library should use ImageItem rather than a generic FileItem so they inherit the same constraint.
61044508071749629aaf3380aa85971e94df9e84)CKEditor5ImageController.phpGNU Affero GPL v3.0. See LICENSE. Loopback lab only. No warranty.