views:

73

answers:

4

To make a long explanation short, suffice it to say that my Rails app allows users to upload images to the app that they will want to keep in the app (meaning, no hotlinking).

So I'm trying to come up with a way to obfuscate the image URLs so that the address of the image depends on whether or not that user is logged in to the site, so if anyone tried hotlinking to the image, they would get a 401 access denied error.

I was thinking that if I could route the request through a controller, I could re-use a lot of the authorization I've already built into my app, but I'm stuck there.

What I'd like is for my images to be accessible through a URL to one of my controllers, like:

http://railsapp.com/images/obfuscated?member_id=1234&pic_id=7890

If the user where to right-click on the image displayed on the website and select "Copy Address", then past it in, it would be the SAME url (as in, wouldn't betray where the image is actually hosted).

The actual image would be living on a URL like this:

http://s3.amazonaws.com/s3username/assets/member_id/pic_id.extension

Is this possible to accomplish? Perhaps using Rails' render method? Or something else? I know it's possible for PHP to return the correct headers to make the browser think it's an image, but I don't know how to do this in Rails...

UPDATE: I want all users of the app to be able to view the images if and ONLY if they are currently logged on to the site. If the user does not have a currently active session on the site, accessing the images directly should yield a generic image, or an error message.

A: 

The problem you have is that as far as I know you need the images on S3 to be World-readable for them to be accessible. At some point in the process an HTTP GET is going to have to be performed to retrieve the image, which is going to expose the real URL to tools that can sniff HTTP, such as Firebug.

Incidentally, 37signals don't consider this to be a huge problem because if I view an image in my private Backpack account I can see the public S3 URL in the browser address bar. Your mileage may vary...

John Topley
Have just seen Andrew's answer above. That's clearly the way to go.
John Topley
+1  A: 

S3 allows you to construct query strings for requests which allow a time-limited download of an otherwise private object. You can generate the URL for the image uniquely for each user, with a short timeout to prevent reuse.

See the documentation, look for the section "Query String Request Authentication Alternative". I'd link directly, but the frame-busting javascript prevents it.

Andrew Aylett
Thanks, but I understand that this could mean that if the url of the image was given to somebody not logged on to the system who tried to access it while the actual user was online and on their account, because the image hasn't timed out yet, they WOULD be able to see the image. In this regard, I don't think this would work for me. I'm curious to know what you think of the solution I came up with, though, as an alternative.
neezer
How would that hypothetical attacker get the URL? If someone has enough access to pass it to them, they have enough access to copy-and-paste the image :). Note that the URL changes each time you generate it; you could probably make the timeout as short as a minute (must be long enough for the containing page to load, needn't necessarily be any longer).
Andrew Aylett
+1  A: 

Should the images be available to only that user or do you want to make it available to a group of users (friends)?

In any case if you want to stop hotlinking you should not store the image files under DocumentRoot of your webserver.

If the former, you could store the image on the server as MD5(image_file_name_as_exposed_to_user + logged_in_username_from_cookie). When the user requests image_file_name_as_exposed_to_user, in your rails app, construct the image filename as previously mentioned and then open the file in rails app and write it out (after first setting Content-Type in response header appropriately). This is secure by design.

If the image could be shared with friends, then you should not incorporate username in constructed filename but rest of the advice should work.

mar
A: 

Thanks for your responses, but I'm still skeptical as to whether or not "timing out" the URL from Amazon is a very effective way to go.

I've updated my question above to be a little more clear about what I'm trying to do, and trying to prevent.

After some experimentation, I've come up with a way to do what I want to do in my Rails App, though this solution is not without downsides. Effectively what I've done is to construct my image_tag with a URL that points to a controller, and takes a path parameter. That controller first tests whether or not the user is authorized to see the image, then it fetches the content of the image in a separate request, and stores the content in an instance variable, which is then passed to a repond_to view to return the image, successfully obfuscating the actual image's URL (since that request is made separately).

Cons:

  1. Adds to request time (I feel that the additional time it takes to do this double-request is acceptable considering the privacy this method gives me)
  2. Adds some clutter to views and routes (a small amount, maybe a bit more than I'd like)
  3. If the user is authorized, and tries to access the image directly, the image is downloaded immediately rather than displayed in the browser (anyone know how to fix this? Modify HTTP headers? Only seems to do this with the jpg, though...)
  4. You have to make a separate view for each file format you intend to serve (two for me, jpg and png)

Are there any other cons or considerations I should be aware of with this method? So far what I've listed, I can live with...

(Refactoring welcome.)


application_controller.rb

class ApplicationController < ActionController::Base

  def obfuscate_image
    respond_to do |format|
      if current_user
        format.jpg { @obfuscated_image = fetch_url "http://s3.amazonaws.com/#{Settings.bucket}/#{params[:path]}" }
      else
        format.png { @obfuscated_image = fetch_url "#{root_url}/images/assets/profile/placeholder.png" }
      end
    end
  end

  protected

  # helps us fetch an image, obfuscated
  def fetch_url(url)
    r = Net::HTTP.get_response(URI.parse(url))
    if r.is_a? Net::HTTPSuccess
      r.body
    else
      nil
    end
  end
end

views/application/obfuscate_image.png.haml & views/application/obfuscate_image.jpg.haml

= @obfuscated_image

routes.rb

map.obfuscate_image 'obfuscate_image', :controller => 'application', :action => 'obfuscate_image'

config/environment.rb

Mime::Type.register "image/png", :png
Mime::Type.register "image/jpg", :jpg

Calling an obfuscated image

= image_tag "/obfuscate_image?path=#{@user.profile_pic.path}"
neezer