Files outside Public Directory
December 17th, 2006 -
A current project I am working on, actually two projects, require files to be protected. To do so, you need to keep the files outside of the public directory, yet allow your users, if they have permission to be able to have access to those files when they so desire. You could use a redirect in an htaccess file, but then how Rubyish is that.
I’ve grown accustomed to using File Column, but I don’t like having to use form_tag in forms as the unhacked way of using it. So, lately I have been using Rick Olson’s Acts as Attachment plugin for all my file upload needs and I have to say I am quite impressed with it.
In this mini article, I’ll show you how I’m currently using it to store files outside the public directory, yet still allow access for users to download those files, if they have permission to access them.
Let’s look at the model setup first:
acts_as_attachment :storage => :file_system, :file_system_path => 'files/downloads'
validates_as_attachment
This is a simple setup for acts as attachment. :storage takes the value of :file_system which means we are going to use the file system instead of the database to store our files. The :file_system_path takes the value of where you are going to store your files when they are uploaded. Simple enough, right?
Next, let’s look at the routes:
map.files 'files/*path', :controller => 'pages', :action => 'download'
This part is important. ‘*path’ will take our path that we send to it, via the download method within our “page” (or whatever you choose to name it). If you don’t include this part, you will get an error when you try to allow your users to access their files for download or your server could be open to attack.
Now, let’s look at the view:
<%= link_to page.filename, :controller => "pages",
:action => "download",
:path => page.public_filename
%>
This is just a simple link_to, passing the page.filename as the name of our file to be displayed with as a link. The pages controller contains our download method that will be called to handle sending the file for the user to download. :path is contains the page.public_filename which contains the public path to the file.
Let’s take a look at the controller now:
def download
path = params[:path].join('/').reverse.chomp('/').reverse
send_file(path)
end
This is the download method and it is quite simple. We take the path that is passed from our link in the view. The path variable is an array and in this case, the array would look something like this: [“files”, “downloads”, “1”, “file.doc”]. We take that array, join it with our forward slashes. We then pass this string to the send_file method, which will then send the file to the user as a download.
Simple, huh? Next thing you would need to do, is setup user permissions so that specific users only have access to their files, but that is out of the scope of this little tutorial.
I’d love to hear others are doing this and if you have found this to be helpful and if you have suggestions of something you’d like to see here, just leave me a comment and I’ll try to put something together.
Comments
Robert
December 19th, 2006
Jonathan, not sure I quite understand what you are pointing out?
Derek
December 19th, 2006
Robert, I assume you’re a Windows user. Jonathan is suggesting that you’ve opened a back door allowing malicious users access to anything they desire on your machine, including the password file.
I do notice your sentence-length caveat hopefully dissuading readers to do just that. It’d probably be best if you set up a directory and only allowed users to access that area. I just put my protected folder in app_root/attachments
Derek
December 19th, 2006
Blah. Having reread my comment, I see it looks unclear. The point isn’t where the protected files are kept, but the mechanism used to give access. Don’t just #join an array received from the user.
Robert
December 19th, 2006
Derek: Thanks for giving more of an explanation, I appreciate it. I realized that Jonathan was pointing out that one could gain access, if some sort of measures were not taken to restrict users to their own files inside the files/download folder that lives in the Rails application root. Is there something else that I am missing, other than a poor explanation that one should have permissions and authentication in place, in the article?
Ryan Heneise
December 19th, 2006
Derek & Jonathan: I’ve been playing with this code for a little while, and so far I can’t figure out how to pass a malicious URL to the download method. As far as I can tell, this is because Rails routing strips out any slashes in the URL following “files/” and splits it into an array.
When you type /files/download/1/test.png, what is passed to params[:path] is not the URL string, but an array: {“path”=>[“download”, “1”, “test.png”]}
So you can try to type /files/download/1/../../test.png, but routing will strip out the slashes, leaving you with an invalid URL.
Ryan Heneise
December 19th, 2006
Looking a little more closely: The key is in the routing definition:
map.files ‘files/*path’, :controller => ‘pages’, :action => ‘download’
It’s the *path that makes this work.
If you didn’t define the route correctly, and ran send_file(params[:some_random_param_from_url]), then you might have a problem.
Robert
December 20th, 2006
I’ve been messing around with this, this morning, and I’ve turned off my authorization and was able to traverse up, but never outside the Rails Root. Nor was I able to traverse up to the Rails Root and then back down into the app, or any other folder, other than the public.
Derek/Jonathan – is there something I’m just not seeing here? It looks to me that Rails is stripping out any /../../../ that goes beyond the Rails Root.
Commenting has been turned off.
Jonathan
December 19th, 2006
http://host/files/../../../../../../etc/passwd ?