Deliver Paperclip attachments to authorized users only

Updated . Posted . Visible to the public.

When Paperclip attachments should only be downloadable for selected users, there are three ways to go.
The same applies to files in Carrierwave.

  1. Deliver attachments through Rails

The first way is to store Paperclip attachments not in the default public/system, but in a private path like storage inside the current release. You should prefer this method when dealing with sensitive data.

Make sure that folder is linked to the current release when deploying:

  • On our OpsComplete deployments, you only need to add storage to the linked_dirs in config/deploy.rb

  • For Capistrano 2, add this to your config/deploy.rb:

    namespace :paperclip do
    
      desc "Create a storage folder for Paperclip attachment in shared path"
      task :create_storage do
        run "mkdir -p #{shared_path}/storage"
      end
      
      desc "Link the Paperclip storage folder into the current release"
      task :link_storage do
        run "ln -nfs #{shared_path}/storage #{release_path}/storage"
      end
    
    end
    
    before "deploy:setup", 'paperclip:create_storage'
    after "deploy:update_code", "paperclip:link_storage"
    
  • For Capistrano 3, add the following (untested) to your config/deploy.rb:

    namespace :paperclip do
    
      desc "Create a storage folder for Paperclip attachment in shared path"
      task :create_storage do
        run "mkdir -p #{shared_path}/storage"
      end
    
    end
    
    append :linked_dirs, 'storage'
    before "deploy:updating", 'paperclip:create_storage'
    

Also make sure that storage is ignored in your .gitignore or mayhem will ensue.

Now write a controller action that sends the attachment to authorized users. That action should be protected by Consul Show archive.org snapshot or a before_action.

class NotesController
  power :notes # Authorization through Consul
  def attachment
    note = Note.find(params[:id])
    send_file note.attachment.path
  end
end

Connect the action in your config/routes.rb:

map.resources :notes, :member => { :attachment => :get }

Your attachment can be configured like this:

class Note < ActiveRecord::Base
  has_attached_file :attachment, :path => ":rails_root/storage/:rails_env/attachments/:id/:style/:basename.:extension"
end

To link to the protecting controller actions, e.g. in a view:

link_to 'Download attachment', [:attachment, @note]
  1. Store attachments in hashed paths

The method above is very secure, but it hits the framework stack everytime an attachment is downloaded. E.g. when you display an image gallery with 40 thumbnails, that would generate 40 expensive Rails requests.

An alternative is to do store Paperclip attachments in the default public/system, but generate the path from a hash function, e.g. attachments/519b6d87b1225ba3/9/beach.jpg. The hash function hashes the containing record id, class name and a secret that must be unique to every application you deploy. This way attachments will only be accessible to users who know the link. However, you can never take authorization away (rarely an issue with images) and if you have directory indexes activated in Apache, you screwed up big time.

Copy the code below to config/initializers/paperclip_hashed_path.rb:

Paperclip.interpolates :hashed_path do |attachment, style|
  secret = 'CHANGE-THIS-FOR-EVERY-APPLICATION! jivEutIgtepsIvgikUpCoshBypMowjuegbisIgJo'
  hash = Digest::SHA512.hexdigest("--#{Rails.env}--#{attachment.class.name}--#{attachment.instance.id}--#{mounted_as}--#{secret}--")
      
  # 4 bit per character, so 128 bit for the full hash. Check if still appropriate!
  hash.slice(0, 32) 
end

Remember to change the secret in line 2 for every application you deploy! Changing a duplicated secret is very painful. We have a note on how to create strong secrets.

You can now use the hashed_path interpolation in your Paperclip attachments:

class Note < ActiveRecord::Base

  attachment_virtual_path = "/system/attachments/:rails_env/:hashed_path/:id/:style/:basename.:extension"
  attachment_real_path = ":rails_root/public" + attachment_virtual_path

  has_attached_file :attachment, :path => attachment_real_path, :url => attachment_virtual_path

end

Paperclip will now automatically store attachments using hashed paths, and generate URLs accordingly. You don't need a protecting controller action here, because all security lies in the hashed secret.

To link to the protecting controller actions, e.g. in a view:

link_to 'Download attachment', [:attachment, @note]

After you deploy, make sure you do not get a directory index when you access http://yourpage.com/system/attachments.

You can configure your apache server to expire file-URLs after some time, so they are only available as long as you want to provide it to the user. This gives some additional security to the not-that-incredibly-secure hashed paths storage approach. Read here for details.

  1. Further reading

Common mistakes when storing file uploads with Rails

Henning Koch
Last edit
Klaus Weidinger
License
Source code in this card is licensed under the MIT License.
Posted by Henning Koch to makandra dev (2010-10-05 17:17)