CVE-2026-48555
Description
Spatie Laravel Media Library before version 11.23.0 contains a server-side request forgery vulnerability that allows remote attackers to cause the server to issue arbitrary outbound HTTP requests by passing user-controlled URLs to the addMediaFromUrl() method in InteractsWithMedia.php.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Spatie Laravel Media Library before 11.23.0 allows SSRF via user-controlled URLs passed to addMediaFromUrl() method.
Vulnerability
Spatie Laravel Media Library before version 11.23.0 contains a server-side request forgery (SSRF) vulnerability in the addMediaFromUrl() method of InteractsWithMedia.php. The method accepts user-controlled URLs and makes outbound HTTP requests without proper validation, allowing an attacker to force the server to send arbitrary requests [1][4].
Exploitation
An attacker with network access and ability to call addMediaFromUrl() with a crafted URL can cause the server to issue HTTP requests to internal or external systems. No prior authentication is required if the method is exposed to unauthenticated users. The attacker can specify any URL scheme (e.g., http, https) and target internal services such as localhost or cloud metadata endpoints [4].
Impact
Successful exploitation enables the attacker to perform SSRF attacks, potentially accessing internal resources, reading sensitive data, or propagating attacks to other internal systems. The CVSS v3 score is 7.4 (High) [4]. The vulnerability may also be chained with other issues to escalate impact [3].
Mitigation
The fix was released in version 11.23.0 of the package [1]. Upgrading to 11.23.0 or later prevents the SSRF by validating URLs and hardening filename processing [2][3]. No workaround is documented if upgrading is not possible. The vulnerability is not listed on CISA's KEV as of the publication date.
- Release 11.23.0 · spatie/laravel-medialibrary
- Harden filename validation against malicious extensions (#3939) · spatie/laravel-medialibrary@608ea03
- Harden filename validation against malicious extensions by freekmurze · Pull Request #3939 · spatie/laravel-medialibrary
- Spatie Laravel Media Library < 11.23.0 SSRF via addMediaFromUrl()
AI Insight generated on May 29, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2(expand)+ 1 more
- (no CPE)
- (no CPE)range: <11.23.0
Patches
1608ea03703d3Harden filename validation against malicious extensions (#3939)
7 files changed · +203 −9
config/media-library.php+23 −0 modified@@ -15,6 +15,7 @@ use Spatie\MediaLibrary\Conversions\ImageGenerators\Webp; use Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob; use Spatie\MediaLibrary\Downloaders\DefaultDownloader; +use Spatie\MediaLibrary\MediaCollections\FileAdder; use Spatie\MediaLibrary\MediaCollections\Models\Media; use Spatie\MediaLibrary\MediaCollections\Models\Observers\MediaObserver; use Spatie\MediaLibrary\ResponsiveImages\Jobs\GenerateResponsiveImagesJob; @@ -40,6 +41,28 @@ */ 'max_file_size' => 1024 * 1024 * 10, // 10MB + /* + * Uploads whose file name contains any of these extensions will be rejected. + * The check looks at every extension in the file name, so a file named + * `malicious.php.jpg` is blocked as well. Matching is case-insensitive + * and a leading dot is optional. + * + * The default list lives on the `FileAdder` class so the shipped config + * and the in-code fallback (used when the config is cached without the + * key) cannot drift. Override here to extend or shrink it. + */ + 'disallowed_extensions' => FileAdder::$defaultDisallowedExtensions, + + /* + * When this is set to an array of extensions, only uploads whose final + * extension is in the list will be accepted. Matching is case-insensitive + * and a leading dot is optional. The `disallowed_extensions` list above + * is still enforced, so an interior dangerous segment (such as the `php` + * in `shell.php.jpg`) is rejected even if the final extension is allowed. + * Leave `null` to disable allowlisting. + */ + 'allowed_extensions' => null, + /* * This queue connection will be used to generate derived and responsive images. * Leave empty to use the default queue connection.
docs/api/adding-files.md+2 −0 modified@@ -49,6 +49,8 @@ This method only accepts URLs that start with `http://` or `https://` public function addMediaFromUrl(string $url) ``` +**Security note.** `addMediaFromUrl` fetches whatever URL you pass to it from your server. It validates only that the URL starts with `http://` or `https://`, not that the destination is safe to reach. Passing user supplied URLs directly therefore exposes your application to server side request forgery (SSRF), letting an attacker make requests to internal hosts, RFC 1918 ranges, loopback, or cloud metadata endpoints (such as `http://169.254.169.254/`). Only call `addMediaFromUrl` with URLs you control, or that you have validated against an allowlist of hosts. + ### addMediaFromDisk ```php
docs/basic-usage/retrieving-media.md+12 −0 modified@@ -87,6 +87,18 @@ $yourModel ->toMediaCollection(); ``` +**Security note.** By default, Media Library rejects uploads whose file name contains a potentially executable extension such as `.php` or `.phtml`. The check looks at every extension segment in the name, so `malicious.php.jpg` is blocked too. Passing your own callable to `sanitizingFileName` fully replaces the default sanitizer (including this protection), so make sure your callable does not let dangerous file names through. + +The blocked extensions can be configured (and an opt-in allowlist enabled) in `config/media-library.php`: + +```php +// Reject these extensions anywhere in the file name. +'disallowed_extensions' => ['php', 'phtml', 'phar', 'htaccess', /* ... */], + +// When set, only accept uploads whose final extension is in this list. +'allowed_extensions' => ['jpg', 'jpeg', 'png', 'pdf'], +``` + You can also retrieve the size of the file via `size` and `human_readable_size` : ```php
docs/converting-other-file-types/using-image-generators.md+2 −0 modified@@ -39,6 +39,8 @@ $this->addMediaConversion('thumb') The only requirement to perform a conversion of a SVG file is [Imagick](http://php.net/manual/en/imagick.setresolution.php). +**Security note.** SVG files are XML documents. When Imagick renders an SVG it can, depending on your ImageMagick build and delegate configuration, resolve external entities or remote references contained in the file. If you accept SVG uploads from untrusted users, harden your ImageMagick `policy.xml` (for example, by disabling the `SVG`, `URL`, and `HTTPS` coders), keep ImageMagick and its delegates up to date, and consider sanitizing or rejecting SVG uploads at the application layer. See the [ImageMagick security policy documentation](https://imagemagick.org/script/security-policy.php) for details. + ## Video The video image generator uses the [PHP-FFMpeg](https://github.com/PHP-FFMpeg/PHP-FFMpeg) package that you can install via Composer:
src/MediaCollections/Exceptions/FileNameNotAllowed.php+6 −2 modified@@ -4,8 +4,12 @@ class FileNameNotAllowed extends FileCannotBeAdded { - public static function create(string $orignalName, string $sanitizedName): self + public static function create(string $originalName, string $sanitizedName, ?string $extension = null): self { - return new static("The file name `{$orignalName}` was sanitized to `{$sanitizedName}`. This sanitized file name is not allowed because it is a PHP file."); + $reason = $extension !== null + ? "The extension `{$extension}` is not allowed because it poses a security risk." + : 'Its extension is not allowed because it poses a security risk.'; + + return new static("The file name `{$originalName}` was sanitized to `{$sanitizedName}`. {$reason}"); } }
src/MediaCollections/FileAdder.php+80 −7 modified@@ -5,7 +5,6 @@ use Closure; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; use Spatie\MediaLibrary\Conversions\ImageGenerators\Image as ImageGenerator; use Spatie\MediaLibrary\HasMedia; @@ -467,21 +466,95 @@ protected function ensureDiskExists(string $diskName): void } } + /** + * Default list of executable extensions that are blocked anywhere in an + * uploaded file name. Referenced by `config/media-library.php` so the + * shipped config and the in-code fallback cannot drift. + * + * @var array<int, string> + */ + public static array $defaultDisallowedExtensions = [ + 'php', 'php3', 'php4', 'php5', 'php6', 'php7', 'php8', + 'phtml', 'phtm', 'pht', 'phps', 'phar', + 'shtml', 'shtm', 'stm', + 'htaccess', 'htpasswd', + 'cgi', 'pl', 'asp', 'aspx', 'jsp', 'jspx', + ]; + public function defaultSanitizer(string $fileName): string { $sanitizedFileName = preg_replace('#\p{C}+#u', '', $fileName); $sanitizedFileName = str_replace(['#', '/', '\\', ' '], '-', $sanitizedFileName); - $phpExtensions = [ - '.php', '.php3', '.php4', '.php5', '.php7', '.php8', '.phtml', '.phar', - ]; + $this->guardAgainstDisallowedFileName($fileName, $sanitizedFileName); + + return $sanitizedFileName; + } + + protected function guardAgainstDisallowedFileName(string $originalFileName, string $sanitizedFileName): void + { + $extensions = $this->extensionsFromFileName($sanitizedFileName); + + $offending = array_intersect($extensions, $this->disallowedExtensions()); - if (Str::endsWith(strtolower($sanitizedFileName), $phpExtensions)) { - throw FileNameNotAllowed::create($fileName, $sanitizedFileName); + if ($offending !== []) { + throw FileNameNotAllowed::create($originalFileName, $sanitizedFileName, reset($offending)); } - return $sanitizedFileName; + $allowedExtensions = $this->allowedExtensions(); + + if ($allowedExtensions === []) { + return; + } + + $finalExtension = strtolower(pathinfo($sanitizedFileName, PATHINFO_EXTENSION)); + + if (! in_array($finalExtension, $allowedExtensions, true)) { + throw FileNameNotAllowed::create($originalFileName, $sanitizedFileName, $finalExtension ?: null); + } + } + + /** + * Returns every dot-separated segment after the first one, so the + * disallowed-extension check can catch a dangerous extension that is + * not the final one (for example, the `php` segment in `shell.php.jpg`). + * + * @return array<int, string> + */ + protected function extensionsFromFileName(string $fileName): array + { + $parts = explode('.', strtolower($fileName)); + + array_shift($parts); + + return $parts; + } + + /** @return array<int, string> */ + protected function disallowedExtensions(): array + { + $extensions = config('media-library.disallowed_extensions') ?? self::$defaultDisallowedExtensions; + + return $this->normalizeExtensions($extensions); + } + + /** @return array<int, string> */ + protected function allowedExtensions(): array + { + return $this->normalizeExtensions(config('media-library.allowed_extensions') ?? []); + } + + /** + * @param array<int, string> $extensions + * @return array<int, string> + */ + protected function normalizeExtensions(array $extensions): array + { + return array_map( + fn (string $extension) => ltrim(strtolower($extension), '.'), + $extensions, + ); } /**
tests/MediaCollections/FileAdderTest.php+78 −0 modified@@ -36,3 +36,81 @@ $adder->defaultSanitizer('media-libraryJQwPHp'); })->throwsNoExceptions(); + +it('blocks a disallowed extension anywhere in the file name', function (string $fileName) { + $adder = app(FileAdder::class); + + $adder->defaultSanitizer($fileName); +})->throws(FileNameNotAllowed::class)->with([ + 'shell.php.jpg', + 'shell.PHP.jpg', + 'shell.php6', + 'shell.pht', + 'shell.phtml', + 'shell.shtml', + 'archive.phar', + '.htaccess', + 'config.htaccess', +]); + +it('allows files with multiple or non-dangerous extensions', function (string $fileName) { + $adder = app(FileAdder::class); + + $adder->defaultSanitizer($fileName); +})->throwsNoExceptions()->with([ + 'archive.tar.gz', + 'report.docx', + 'video.mp4', + 'image.jpeg', + 'backup.2026.05.zip', + 'document.pdf', +]); + +it('respects a custom disallowed extensions config', function () { + config()->set('media-library.disallowed_extensions', ['exe']); + + $adder = app(FileAdder::class); + + $adder->defaultSanitizer('installer.exe'); +})->throws(FileNameNotAllowed::class); + +it('rejects files outside the allowlist when one is configured', function () { + config()->set('media-library.allowed_extensions', ['jpg', 'png', 'pdf']); + + $adder = app(FileAdder::class); + + $adder->defaultSanitizer('archive.zip'); +})->throws(FileNameNotAllowed::class); + +it('accepts files inside the allowlist when one is configured', function () { + config()->set('media-library.allowed_extensions', ['jpg', 'png', 'pdf']); + + $adder = app(FileAdder::class); + + expect($adder->defaultSanitizer('photo.jpg'))->toEqual('photo.jpg'); +}); + +it('still blocks dangerous interior extensions when an allowlist is configured', function () { + config()->set('media-library.allowed_extensions', ['jpg']); + + $adder = app(FileAdder::class); + + $adder->defaultSanitizer('shell.php.jpg'); +})->throws(FileNameNotAllowed::class); + +it('treats allowlist entries case-insensitively', function () { + config()->set('media-library.allowed_extensions', ['JPG', '.PNG']); + + $adder = app(FileAdder::class); + + expect($adder->defaultSanitizer('photo.jpg'))->toEqual('photo.jpg'); + expect($adder->defaultSanitizer('photo.png'))->toEqual('photo.png'); +}); + +it('rejects files without an extension when an allowlist is configured', function () { + config()->set('media-library.allowed_extensions', ['jpg']); + + $adder = app(FileAdder::class); + + $adder->defaultSanitizer('Makefile'); +})->throws(FileNameNotAllowed::class);
Vulnerability mechanics
Root cause
"The addMediaFromUrl() method does not validate the destination host of user-supplied URLs, allowing server-side request forgery."
Attack vector
An attacker with the ability to pass a URL to `addMediaFromUrl()` can cause the server to issue arbitrary outbound HTTP requests. The method only validates that the URL starts with `http://` or `https://`, not that the destination is safe to reach [ref_id=1]. This exposes internal hosts, RFC 1918 ranges, loopback addresses, or cloud metadata endpoints (such as `http://169.254.169.254/`) to SSRF attacks [patch_id=3107107]. The attacker does not need to bypass authentication beyond whatever access is required to invoke the media-upload functionality.
Affected code
The vulnerability resides in the `addMediaFromUrl()` method in `InteractsWithMedia.php` and the `defaultSanitizer()` method in `src/MediaCollections/FileAdder.php`. The patch also updates `config/media-library.php` and the documentation files `docs/api/adding-files.md` and `docs/basic-usage/retrieving-media.md` to document the SSRF risk and hardened filename validation.
What the fix does
The patch adds a security note to the `addMediaFromUrl()` documentation explicitly warning that passing user-supplied URLs exposes the application to SSRF [patch_id=3107107]. It also hardens filename validation by replacing the old trailing-extension blocklist with a per-segment check, so files like `shell.php.jpg` are rejected in addition to `shell.php` [ref_id=2]. The blocked extensions list is expanded and made configurable via `disallowed_extensions` and `allowed_extensions` keys in `config/media-library.php`.
Preconditions
- inputThe attacker must be able to invoke the addMediaFromUrl() method with a user-controlled URL.
- configThe application must not validate the destination host against an allowlist before calling addMediaFromUrl().
Generated on May 29, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.