It's generally accepted that we should never simply trust a user's input. Otherwise, we're vulnerable to malicious input (CE-5). For a file upload from a user, it’s common to check their file size or type, especially when restricting them by those two attributes. But one that might get overlooked is the filename.
Checking the filename is especially important when it displayed on a web page to prevent HTML or XSS injection. That’s just on the frontend. On the backend, there are the other usual injections to worry about. Like SQL injection, when storing the name in a database. Or path injection, if storing in a path based on the name.
It might be tempting to dismiss this possibility as something that would never happen. After all, we must make the files first before we can upload them and there are file-naming rules. We can't simply make one with just any name they want! However, the rules won’t prevent malicious filenames!
OS file naming rules won't prevent injection
Operating systems have rules about what can be in a filename. But, different operating systems can have different rules. So, a filename prohibited in one operating system may be allowed in another.
For example, <img onerror="alert('XSS')" src="nowhere">.txt
. The file can't be created in Windows because it doesn't allow <
and >
(see Naming a file). However, Linux does allow them. For example, we could use touch to create the file:
touch '<img onerror="alert('"'XSS'"')" src="nowhere">.txt'
Actually, we don't even need to make the files
HTTP clients usually send file uploads using a multi-part POST request. We can use Wireshark to see what the request looks like. Here's one from cURL:
POST /attachments HTTP/1.1
Host: 127.0.0.1:3000
User-Agent: curl/7.76.1
Accept: */*
Content-Length: 250
Content-Type: multipart/form-data; boundary=------------------------6c44be865e1d13e6
--------------------------6c44be865e1d13e6
Content-Disposition: form-data; name="attachment"; filename="Lorem_Ipsum.txt"
Content-Type: text/plain
Lorem ipsum dolor sit amet, consectetur cras amet.
--------------------------6c44be865e1d13e6--
Notice the filename field is at the end of the Content-Disposition
header. We can use this as a template to make our own requests. Simply. copy and paste into a file, change the filename and update the Content-Length
. Then use Ncat to send the request. For example, to send the request in request.http
to http://localhost:3000
:
ncat localhost 3000 < request.http
Using HTTPS isn't enough either
The example above sent a forged request to an insecure server (i.e one that isn't using SSL/TLS). But changing over to HTTPS still won't be enough to prevent malicious filenames - Ncat also works with HTTPS connections:
ncat -C --ssl localhost 3000 < request.http
Even if it didn't, it we could go back to using a web browser to upload files over HTTPS.
Pentesting tools makes this easier
Using Wireshark and Ncat was how I first learnt to do this. It turns out pentesting tools like mitmproxy make this easier - it can intercept and modify the requests in-flight.
Burp Suite's proxy has similar features.
Prevent injection via filenames
There is just no way to guarantee a server never receiving an upload with a bad filename. The server must guard against it - usually by sanitising or encoding and escaping where it is used (OWASP CE-4). Doing so will help to prevent injection attacks via filenames.