Restricting Download of Files

So I've got a webapp that stores something, lets say photos.

The photos are currently stored on my webserver's filesystem via attachment_fu. Users can upload photos fine, they get stored fine, and I can display them fine. I jiggered attachment_fu to use custom path/filenames based on the ID of the photo, I'm storing them some place like /public/photos/123.jpg

So far so good.

I have a 'view' section of the photo controller, along with a view that shows the photo itself along with all the associated information on it...owner, date, whatever. Users must be logged in to the site to view photos, so there is a before_filter that tests that. Great. Works fine.

Of course nothing prevents anyone on the Intarwebs from typing www.insecurephotos.com/photos/123.jpg in to their browser and having the webserver serve the file up directly. Big problem.

How do I solve this? Here are some ideas I am throwing around...

1) move the storage to outside rails_root and use send_file to stream it directly from the file. (Yucky!) 2) move the storage to the db and stream from there (Ugh, Puke!) 3) move the storage to Amazon S3. I don't know enough about this. Does S3 expose the item to the internet as a url? Can I stream the photo from S3 into rails, and then from rails to the user? There MUST not be a publicly available URL to the photo. 4) ? 5) Profit.

Any other ideas?

Thanks!

hi andrew!

andrew.ohnstad@gmail.com [2008-04-29 16:27]:

Of course nothing prevents anyone on the Intarwebs from typing www.insecurephotos.com/photos/123.jpg in to their browser and having the webserver serve the file up directly. Big problem.

this has come up before, maybe [1] is going to help. or i might point you directly at [2] :wink:

[1] <http://groups.google.com/group/rubyonrails-talk/browse_thread/thread/9dbeef7b6d54f1a9/63bdc9a664eb1fca&gt; [2] <http://prometheus.rubyforge.org/apache_secure_download/&gt;

cheers jens

Hi Jens,

Thanks for the quick reply.

I think x-sendfile is the right way to go, thanks for setting me on that path. Unfortunatley apache_secure_download won't work since I'm using Nginx.

I will install the X-sendfile plugin, and there is a monkeypatch to make it work with Nginx's X-Accel-Redirect header... (

).

I'm reading the X-Sendfile docs, and there is a reference to making it work inline. My question is how to make this show up in the view... Let's say I want to show a photo:

class PhotoController < ApplicationController

def show   @photo = Photo.find(params[:id])   x_send_file(@photo.public_filename, :type => 'image/ jpeg', :disposition => 'inline') end

And then in my view:

<img src=???> <%= @photo.title %> Uploaded by: <%= @photo.user.username %> Photographers' Notes: <%= @photo.notes %>

How do I get the X-Sendfile that is supposed to be inline, into the web page? What do I put in my image src? I tried the method here: http://mcubed.name/blog/articles/read/9 but that just makes another controller method that users can access directly to trigger the send_data and I'm back to square 1. I can't make the method private because lots of controller/view combos need to access photos, albums, pages, slides, prints, etc...

Thanks for any further help you can give.

andrew.ohnstad@gmail.com [2008-04-29 17:42]:

I think x-sendfile is the right way to go, thanks for setting me on that path. Unfortunatley apache_secure_download won't work since I'm using Nginx.

ok, that was too quick a shot, sorry :wink:

there are "secure download" modules for mongrel and apache, but i don't know of any for nginx. though you could still use apache to serve your static files.

How do I get the X-Sendfile that is supposed to be inline, into the web page? What do I put in my image src? I tried the method here: http://mcubed.name/blog/articles/read/9 but that just makes another controller method that users can access directly to trigger the send_data and I'm back to square 1. I can't make the method private because lots of controller/view combos need to access photos, albums, pages, slides, prints, etc...

well, that's exactly what you have to do. and it's fine, because your actions are protected by whatever your authorization mechanism is, right?

the only drawback i see is that you would have to invoke the complete rails stack for image display (except the final rendering). but this might not be a problem if you're only going to display a few images per page.

cheers jens

I'm sure there is something really simple and stupid I am missing but...

I have a before_filter to check if the user is logged on to the site. Once they are logged in, they should be able to access photos, but they MUST not be able to download a photo directly, they have to get the photo as an element in a rails view.

Since a photo can exist in the views of other controllers, I have written a method in my Photo controller...

you could try 'security by obfuscation' and make the file names hard to guess. Maybe you could 'secure' it with a username in a .htaccess file [?]

andrew.ohnstad@gmail.com [2008-04-29 18:46]:

Once they are logged in, they should be able to access photos, but they MUST not be able to download a photo directly,

oh, sorry again! i didn't fully understand you there, i guess.

Access to the photos won't work unless they are logged in. BUT once they are already logged in, then they can type in "www.site.com/photo/stream/123" to their browser, and the photo pops up all by itself.

well, there's really not much you can do against it, AFAICT.

Should I be checking the referrer in the "stream" method and sending/denying the file based on that?

that might be a possibility, but be aware that the referer is easily spoofed.

the only thing i can think of right now is to use one-time URLs for your photos. however, they might still be stored in the browser's (or a proxy's) cache and be retrieved from there.

something like this might work:

---- snip ----   # in the controller:   def stream     if Time.now.to_i == params[:timestamp].to_i && params[:token] == calculate_token(params[:id], params[:timestamp])       send_file ...     else       render :nothing => true, :status => :forbidden     end   end

  # private controller method *and* helper method:   def calculate_token(id, timestamp)     Digest::SHA1.hexdigest("#{SECRET}--#{id}--#{timestamp}")   end

  # in the view:   <% timestamp = Time.now.to_i -%>   <img src=<%= url_for(:action => 'stream', :id => photo.id, :timestamp => timestamp, :token => calculate_token(photo.id, timestamp))%>> ---- snip ----

this is untested and not fully elaborated, though. additionally, you probably would have to allow for a small delay when checking the timestamp in 'stream', which again opens the door for potential stealing. for *real* one-time URLs you would have to store the token somewhere (i.e., in the DB) and toggle some attribute which renders that particular token invalid after first use.

anyway, if you don't mind me asking: why do you care so much about preventing users from saving your photos? why not just make sure that only authorized users may view them and be done with it?

cheers jens

Roger Pack [2008-04-29 19:20]:

you could try 'security by obfuscation' and make the file names hard to guess.

the file names are not the problem, the URL is always <controller>/stream/<id> and can be seen in the HTML source and the image properties.

Maybe you could 'secure' it with a username in a .htaccess file [?]

that won't work either since the web server isn't involved at that point. the files are read directly from disk.

cheers jens

andrew.ohns...@gmail.com [2008-04-29 18:46]:> Once they are logged in, they should be able to access photos, > but they MUST not be able to download a photo directly,

oh, sorry again! i didn't fully understand you there, i guess.

> Access to the photos won't work unless they are logged in. BUT > once they are already logged in, then they can type in > "www.site.com/photo/stream/123" to their browser, and the photo > pops up all by itself.

well, there's really not much you can do against it, AFAICT.

---Snip---

this is untested and not fully elaborated, though. additionally, you probably would have to allow for a small delay when checking the timestamp in 'stream', which again opens the door for potential stealing. for *real* one-time URLs you would have to store the token somewhere (i.e., in the DB) and toggle some attribute which renders that particular token invalid after first use.

Yeah, that's what I was getting at. I was afraid this was going to become a difficult issue to fix. :slight_smile: Thanks for your help with the code, it gives me a starting point to roll something myself. I was hoping Amazon S3 could fit in here, but everything I have read indicates I'll be no better off with S3, that S3 resources are accessed by URLs too. So in addition to what you suggest, I'm going to play with S3 ACLs, downloading the file from S3 to temp space on the web server, rendering it, and then deleting the temp file. Yuck.

anyway, if you don't mind me asking: why do you care so much about preventing users from saving your photos? why not just make sure that only authorized users may view them and be done with it?

Well, photo is an obfuscated example of what I really want to show. They're image files, but not exactly photos. Every image file MUST be accompanied by regulatory text and some special details that define how the "photo" may be reproduced, where it can be shown, etc. The client is very insistent that the "photo" should not be able to be downloaded into a blank browser window without the correct regulations also being shown.

Does the client understand that the raw image files will be sitting in the user's browser cache, ready to look at sans verbiage at any time, and that's simply the way the web works? :slight_smile:

If that's still not acceptable, you should embed the image, either in an enclosing wrapper image (with text-as-image) or as a PDF. Both can be done dynamically if necessary.

FWIW,

andrew.ohnstad@gmail.com [2008-04-29 19:43]:

So in addition to what you suggest, I'm going to play with S3 ACLs, downloading the file from S3 to temp space on the web server, rendering it, and then deleting the temp file. Yuck.

you must be kiddin'... such a crazy thing never occurred to me :wink:

to avoid at least that I/O and bandwidth overhead you could work with symlinks. in 'show' create a link from "/path/to/photo/<id>" to "/path/to/real_photo/<id>" and in 'stream' remove it again. that's an interesting idea, actually. (of course you have to cater for race-conditions and stuff...)

Every image file MUST be accompanied by regulatory text and some special details that define how the "photo" may be reproduced, where it can be shown, etc. The client is very insistent that the "photo" should not be able to be downloaded into a blank browser window without the correct regulations also being shown.

ok, now i understand. well, good luck then!

cheers jens

Yes, they do. They actually aren't all that concerned with what the authorized user does with it, once it is on their PC, but they want to make sure that any OTHER PC shows the required "stuff" at least once.

The scanario they are concerned about is this...

User 1 and User 2 are both authorized to view image Z.

User 1 uses the application, finds the image, views it (via a rails view - including the usage rules/whatever), right clicks it, gets the image URL, and emails the image URL to user 2. Then user 2 (who is also authorized to view the image) ALT-Tabs over to their web browser (where they are already logged into the application) and right-click/ pastes the URL into their address bar. They hit return, the application checks that they are authorized to see the image (true) and then sends the image iteself (www.url.com/photo/stream/Z.jpg) without it going through the view.

For example, they don't care if someone right-click/save-as a image to their local PC. But their assertion is that the website itself should never render an image in a plain window.

I think I'll tell them to stuff it. Validating that the user is authorized should be enough.

Now THAT is a potential winner. So is hitting the client with a clue- by-four.

The scanario they are concerned about is this...

User 1 and User 2 are both authorized to view image Z.

User 1 uses the application, finds the image, views it (via a rails view - including the usage rules/whatever), right clicks it, gets the image URL, and emails the image URL to user 2. Then user 2 (who is also authorized to view the image) ALT-Tabs over to their web browser (where they are already logged into the application) and right-click/ pastes the URL into their address bar.

If you serve the images through a controller, you could embed a unique key in the URL, and verify that the key is associated with the session using it. An emailed URL -- session/key discrepancy -- would redirect to the full page view.

I think I'll tell them to stuff it. Validating that the user is authorized should be enough.

But I'd certainly agree with that :slight_smile:

FWIW, and good luck,

Hassan Schroeder [2008-04-29 21:27]:

The scanario they are concerned about is this...

User 1 and User 2 are both authorized to view image Z.

User 1 uses the application, finds the image, views it (via a rails view - including the usage rules/whatever), right clicks it, gets the image URL, and emails the image URL to user 2. Then user 2 (who is also authorized to view the image) ALT-Tabs over to their web browser (where they are already logged into the application) and right-click/ pastes the URL into their address bar.

If you serve the images through a controller, you could embed a unique key in the URL, and verify that the key is associated with the session using it. An emailed URL -- session/key discrepancy -- would redirect to the full page view.

assuming that the requirements are not that strict (anymore), i would probably go with the referer solution: if 'stream' has been called with no (or an inappropriate) referer just redirect to 'show'. that's not impossible to overcome, but is easily implemented (as easy as can be :wink: and still fits the typical use case, i suppose.

cheers jens