Laravel Security: File Upload Vulnerability
Explaining that Laravel Image File Upload Vulnerability...
I wouldn’t normally email you outside the schedule like this, but since I’ve been asked a few times about the Laravel Image File Upload Vulnerability disclosure that came out yesterday, I felt it was important to let you all know what’s really going on it with.
Firstly, for those who haven’t seen it, a security researcher named Hosein Vita posted a proof of concept on Medium yesterday for what they called a “Laravel 8.x image upload bypass”.
This is the post: https://hosein-vita.medium.com/laravel-8-x-image-upload-bypass-zero-day-852bd806019b. They also mentioned CVE-2021-43617, which is an official vulnerability identification number, and a different vuln to what they disclosed… we’ll get to it soon.
I must admit that when I saw the vulnerability reported yesterday, I immediately assumed the worst and didn’t take the time to look into it… oops. It turns out this isn’t a vulnerability in the Laravel Framework. It’s actually a developer level vulnerability that comes from trusting user input! Yep, the same thing I’ve been going on about for the past two weeks! 🤣
This vulnerability requires on three important conditions:
Laravel validates file uploads based on their mime type and not their extension.
The vulnerable code uses the user-provided file extension.
The vulnerable code saves the file somewhere directly user accessible.
Mime type checking works by checking the start of the file for an identifier of the file type. GIFs start with
"GIF87a", while JPGs start with a binary string of
"FF D8 FF E0". Throw this at the start of an HTML (or PHP) file, and the mime type will return as an image.
Since the user is able to set their own extension, the file can report as an image to successfully validate, but when saved on disk, it is served as an HTML file. That leads to Stored Cross-Site Scripting (XSS), which in the proof-of-concept is used to grab a CSRF key, and the attacker has full access to do whatever they want.
The functions you need to look for in your code are:
$name = $file->getClientOriginalName();
$extension = $file->getClientOriginalExtension();
Anything you get from these two functions should not be trusted. You can store it in a database and display escaped on the page, but don’t use them in the actual stored file name.
The safe way to store uploaded files is to generate a safe unique name (my usual trick is a hash based on a unique Model ID), with the extension as guessed by the file mime type.
We can get the guessed extension like this:
$extension = $request->photo->extension();
This will ensure the file is saved as one of our allowed types and the web server and browser will treat it as such. Any sneaky injection attempts will be foiled.
This Isn’t Remote Code Execution (RCE)
This proof-of-concept gains XSS on the site, but not Remote Code Execution - which would make this a much more dangerous vulnerability. This is because the Laravel Framework actually blocks the upload of PHP files as a security measure.
That said, the CVE they reported identifies potential RCE with on Debian systems - since Debian allows for executing
.phar files alongside
.php in the browser. So these systems may be vulnerable to an RCE.
I’ve submitted a PR to Laravel to block
Mitigating The Vulnerability
There are a few ways to prevent or mitigate a vulnerability like this:
1. Don’t Trust User Input
The simple fix here is to not trust the file extension when saving uploaded files. Instead use the extension as reported by the mime type.
2. Upload Files Somewhere Else
The XSS and CSRF components of this proof-of-concept relies on this file being uploaded within the application. If you upload all user files elsewhere, such as a separate media domain, block storage (i.e. S3), Content Delivery Network, etc, it limits the exposure. Cookies may no longer be passed through, which removes any privileges the script has over your site.
This is also fantastic protection against potential RCE - if the file is running on someone else’s system, it’s their problem to protect, not yours!
3. Don’t Make Uploaded Files Directly Accessible
If uploaded files cannot be directly accessed in the browser, then they can’t be accessed to be executed. This may not be possible given your file upload needs, but it’s worth thinking about how you can safely store and access untrusted files without users accessing them.
4. Use A Third-Party Service
Linked to #2 - if you can find a third-party service that handles file uploads for you, then it’s on them to validate file types.
5. For Images: Check Dimensions
Clément posted a great comment with a useful trick for validating images:
As additional security check you can also verify that the file is an actual image by validating the width or height has a minimum of 1px
My one complaint about this disclosure is the researcher threw it online without any obvious attempts at disclosing the vulnerability responsibly to Laravel, or making it clear their proof-of-concept isn’t actually a vulnerability in the Laravel Framework. They were granted a CVE for the potential
phar issue with Debian, but they didn’t make that clear in their post, and instead it spreads fear about the framework. That’s not how we should be handling situations like this…
If you find a security vulnerability in Laravel (or any other project), you should first try to contact the maintainer privately, or reach out to other researchers in the industry for help1.
Laravel has a responsible disclosure policy on GitHub, which makes it very easy to contact Taylor Otwell to report any issues: https://github.com/laravel/framework/security/policy
Ok, I hope that has been helpful and has dispelled some of the fear and worry about this vulnerability report.
Please let me know if you have any questions in the comments (I’ll open them up to everyone).
Please feel free to reach out to me directly if you need help with any security issues.