Secure File Download URLs in Rails

Carlos Ramirez III
4 min readJun 30, 2016

--

It’s common for web applications to have functionality that allows users to upload or download files. When a file is uploaded to a remote host such as Amazon S3, it is accessed via a public URL. Frequently these files are innocuous and meant to be publicly visible such as a user’s profile image. But sometimes they can be more sensitive and private such as a financial document or personally identifying paperwork. In these cases, we need something more secure than a public URL that anyone can access over the Internet.

Use Case

Consider an application where users input financial information and a report is generated. Perhaps the report contains a social security number as well as other sensitive and identifying personal information.

The report document should be downloadable only by the user, and the URL should not be accessible by anyone on the Internet at large.

Solution

We will review the solution for three of the most popular file upload gems: CarrierWave, Paperclip, and Refile.

We’ll work with a simple demo application containing User and Document models. The Document model has an attached file, and a Rails scaffold will be generated for basic CRUD functionality.

I have created a repository with the full source code for the demo. There are separate branches for each upload provider. Be sure to review the README of the repository for information on running the demo(s).

CarrierWave

Basic Setup

Following the CarrierWave installation instructions,

  1. Generate a new uploader
  2. Generate a migration that adds a new column for the uploader to the documents table and mount the uploader to the Document class
  3. Configure CarrierWave for S3 hosting
  4. Update the scaffolded views and DocumentsController to allow a file to be uploaded

This diff shows the code changes.

Notice that clicking the download link opens up the direct URL to the file asset on S3. This URL can be accessed by any other browser and allows any person who knows it to download the file.

Secure URLs

Creating a secure URL can be done by altering some CarrierWave settings.

# in config/initializers/carrierwave.rbconfig.fog_public = false

Setting fog_public to false ensures that newly uploaded assets are created with restricted access (i.e. no longer publicly readable).

NOTE: Previously uploaded files will still retain their old access settings. The new settings do not apply retroactively.

Notice that the asset URL contains some AWS-specific parameters such as a signature. If you try removing those parameters, AWS does not allow you to access the asset. This is the authentication at work; even if someone tried to guess the URL for your asset, they would not be able to access it without providing the credentials that CarrerWave automatically includes in the asset URL for us.

Expiring URLs

To take things a step further, let’s generate a URL that will expire after a given amount of time.

# in config/initializers/carrierwave.rbconfig.fog_authenticated_url_expiration = 5

The fog_authenticated_url_expiration controls how long the URL will last (in seconds).

Instead of linking the asset URL directly in our view, let’s use a controller action to wrap the download. Why do we do this? Because the download URL may expire before the user clicks on it in the view. Using a separate controller action lets the URL be generated as needed by the user.

class Documents::DownloadsController < ApplicationController   before_action :set_document   
def show
redirect_to @document.file.file.authenticated_url end
private
def set_document @document = current_user.documents.find(params[:document_id]) endend

I’ve created a brand new controller and namespaced it under Documents rather than adding a new custom action to the existing DocumentsController. Read more

This diff sums up all of the changes.

Notice that we fetch the Document collection using current_user.documents. This will prevent other user’s from changing the primary key in the URL in order to access another user’s documents. You can further enhance this security by implementing authorization using a gem like Pundit.

Paperclip

The setup here is a little different, but the concept remains the same.

Basic Setup

All of the configuration happens right in the Document class.

View diff

Secure & Expiring URLs

As before, add additional settings to restrict access to the asset.

# inside app/models/document.rbhas_attached_file :file,                  # ...,                  s3_permissions: :private

Use the same Documents::DownloadsController as before with the following method to generate and return a secure and expiring URL

redirect_to @document.file.expiring_url(10)

The Paperclip wiki contains more information.

Refile

Refile is a fundamentally different solution than the previous two.

Basic Setup

As it is its spiritual successor, the setup is nearly identical as CarrierWave.

Secure URLs

As per this GitHub issue, Refile downloads are secure out of the box since they are streamed from a small Sinatra application which is mounted within the main Rails application.

We can still put the asset download behind its own controller endpoint, however there is currently no way to secure the asset itself using S3 authenticated reads and/or expiring URLs.

The thread above contains a discussion and sample implementation for using CloudFront authenticated URLs, but unfortunately there is no support out of the box for this.

Conclusion

As you can see, securing your assets on a remote host such as Amazon S3 is quite easy to do in Rails, and regardless of which file upload provider you choose, the basic concept for doing so remains the same.

Your users will thank you for taking steps to protect their sensitive documents and data.

Want more? The rest of my Ruby on Rails guides can be found here.

--

--