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!

[email protected] [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
[2] http://prometheus.rubyforge.org/apache_secure_download/

cheers
jens


Jens W., Dipl.-Bibl. (FH)
prometheus - Das verteilte digitale Bildarchiv für Forschung & Lehre
Kunsthistorisches Institut der Universität zu
KölnAlbertus-Magnus-Platz, D-50923
KölnTel.: +49 (0)221 470-6668, E-Mail: [email protected]
http://www.prometheus-bildarchiv.de/

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… (
http://spongetech.wordpress.com/2007/11/13/the-complete-nginx-solution-to-sending-flowers-and-files-with-rails/
).

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:

<%= @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.

[email protected] [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

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 [?]

On Tue, Apr 29, 2008 at 10:46 AM, [email protected]

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…

before_filter :check_logged_in

def stream
send_file("/path/outside/nginx/photos/#{params[:id]}", :type =>
‘image/jpg’, :disposition => ‘inline’)
end

So lets say I want to view two photos on a “scrapbook_page” Scrapbook
controller has:

before_filter :check_logged_in

def show
@photos=Photo.find_by_page_id(params[:id])
end

views/scrapbook_pages/show.rhtml

<%- @photos.each do |p| -%>
<img src=<%= url_for (:action => ‘stream’, :image_id => p.id )
<%- end -%>

This works fine. 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.

What I really want, is send_file for the view. :slight_smile: If I make the
stream method in photo private, then scrapbook_pages can’t access it
any more, nor can any other view. I’m lost here! Should I be
checking the referrer in the “stream” method and sending/denying the
file based on that?

How can I make my application check if the file is being requested as
an inline element as part of a webpage, or not?

Thanks,
Andrew

[email protected] [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 P. [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
/stream/ 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

On Tue, Apr 29, 2008 at 10:43 AM, [email protected]
[email protected] wrote:

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,

Hassan S. ------------------------ [email protected]

On Apr 29, 1:32 pm, Jens W. [email protected] wrote:

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.

[email protected] [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/” to
“/path/to/real_photo/” 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

On Apr 29, 2:01 pm, “Hassan S.” [email protected]
wrote:

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:

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.

On Tue, Apr 29, 2008 at 11:23 AM, [email protected]
[email protected] wrote:

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 S. ------------------------ [email protected]

On Apr 29, 2:05 pm, Jens W. [email protected] wrote:

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

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

Hassan S. [2008-04-29 21:27]:

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

This forum is not affiliated to the Ruby language, Ruby on Rails framework, nor any Ruby applications discussed here.

| Privacy Policy | Terms of Service | Remote Ruby Jobs