Here's a story:
User A should be able to upload an image.
User A should be able to set a privacy. ("Public" or "Private").
User B should not be able to access "Private" images of User A.
I'm planning to user Paperclip for dealing with uploads.
If I store the images under "RAILS_ROOT/public/images", anyone who could guess the name of the files might access the files. (e.g., accessing http://example.com/public/images/uploads/john/family.png )
I need to show the images using img tags, so I cannot place a file except public.
How can I ensure that images of a user or group is not accessible by others?
(If I cannot achieve this with Paperclip, what is a good solution?)
You may make your rails server output the contents of image files. This is done via a controller action (most of actions print HTML, but this one will print JPG, for example).
Then you may use your authorization system to restrict access on controller level!
class ImagesController
#Default show Image method streams the file contents.
#File doesn't have to be in public/ dir
def show
send_file #image.filename, :type => #image.content_type,
:disposition => 'inline'
end
# Use your favorite authorization system to restrict access
filter_access_to :show, :require => :view, :attribute_check => :true
end
In HTML code you may use:
<img src="/images/show/5" />
I would have Paperclip use S3 on the back-end, set uploaded files to private, and then use "Query String Request Authentication Alternative" to generate the URLs for my image tags.
http://docs.amazonwebservices.com/AmazonS3/2006-03-01/index.html?RESTAuthentication.html
Here's how I did this in a similar application.
Store your images on Amazon S3 instead of the local file system. Paperclip supports this.
Set your :s3_permissions to "private" in your Paperclip options
In your Image model, define a method that let's you output an authorized, time-limited url for the image.
Mine looks like this:
def s3_url(style = :original, time_limit = 30.minutes)
self.attachment.s3.interface.get_link(attachment.s3_bucket.to_s, attachment.path(style), time_limit)
end
You can then show images to people only if they're authorized to see them (implement that however you like)–and not have to worry about people guessing/viewing private images. It also keeps them from passing URLs around since they expire (the URL has a token in it).
Be warned that it takes time for your app to generate the authorized urls for each image. So, if you have several images on a page, it will affect load time.
If you want to host files yourself, you can perform authentication at the controller level as has been suggested. One of my applications has an AssetController that handles serving of files from the 'private' directory, for example.
One thing I wanted to add is that you should review this guide for setting up X-Sendfile, which will let your application tell the web server to handle actually sending the files. You'll see much better performance with this approach.
Related
To prevent anyone with a url accessing restricted files, I need to check user role permissions to view those ActiveStorage model attachments beforehand. I've created an ActiveStorage::BaseController with a before action that does all the needed checks.
I currently find the original parent model that the attachment belongs to by using the signed_id in the params:
signed_id = params[:signed_id].presence || params[:signed_blob_id].presence
blob_id = ActiveStorage::Blob.find_signed(signed_id)
attachment = ActiveStorage::Attachment.find_by(blob_id: blob_id)
record = attachment.record
# record.some_model method_or_check etc.
This works really well and I'm able to do all the stuff I want to do, however, sometimes the signed_id is not in the params and instead I just get:
{"encoded_key"=>
"[LONG KEY HERE]",
"filename"=>"sample",
"format"=>"pdf"}
I have no idea why this happens unpredictably as I'm always using rails_blob_path(model.attachment, disposition: 'attachment') or rails_blob_path(model.attachment, disposition: 'preview') everywhere in the code and testing under the same conditions.
Is it possible to ensure the signed_id is always present or alternatively, is there a way to get back to the parent model using the encoded_key as rails obviously does this to display the attachment anyway?
I am not sure you need it
If you're worried that someone might view the file if he/she knows URL, you can make time expiration limit for that URL
# config/initializers/active_storage.rb
Rails.application.config.active_storage.service_urls_expire_in = 1.minute
It is unlikely that an attacker will be able to generate URL in such a way as to be able to view that file
I am working on a phone billing application.
I have created simple Rails App with 3 tables Client, Invoice, CallRecord.
I can browse my dummy data and all works fine.
I would like to add a feature to send invoices to clients via email.
I was thinking about creating a simple link/button on the invoice#show view: Send invoice.
I was looking at WickedPDF gem as it will generate pdf from HTML.
I have three questions:
1) Is WickedPDF good for what I am looking for?
I am not trying to serve PDF to a user browsing the website. I don't want to make it downloadable.
I just need to generate it and save it when the Send Invoice is clicked, if it hasn't been generated already. And then email it using rails mailer to a client keeping the generated copy on the server(all this will be done in a custom controller action).
2) Where would be the best place to store this generated pdf's? Custom folder in Assets folder, public folder?
3)How would you go about saving files using rails? I have done a bit of rails but I have never done anything this direction.
You can actually use Wicked-PDF directly without the
controllers by:
pdf = WickedPdf.new.pdf_from_string('<h1>Hello There!</h1>')
or
WickedPdf.new.pdf_from_string(
render_to_string('templates/pdf.html.erb', :layout => 'pdfs/layout_pdf'),
:footer => {
:content => render_to_string(:layout => 'pdfs/layout_pdf')
}
) # which is awesome
Storing them should be in the public folder in a similar format:
public/users/:user_id/:invoice_id
you should consider doing that as a delayed job using sidekiq or something because the generation would take time and the user should not wait until it is generated.
I want to secure some files that are on my site, but also I want to give permissions to some users to download that files. What is the best way to do it?
I have only one idea - generate temporary random URL's like /files/hdad8fda299asnfqe/foo.bar
But I hardly want that other users can not use others URLs.
Do you know any gem that can help me or suggest another way to secure files?
The only way to secure downloads for real is to move them outside of the 'public' path and feed them to the user through a controller that checks authorization. The most efficient way to do this (that I know of) is to use xsendfile.
So for example first you make a new directory in your app, let's say "downloads" (you can name it whatever of course, and you can put it anywhere in your system that your Rails server can access.)
mkdir downloads
Now make a nice new route for the downloads:
match '/downloads/*filename' => 'downloads#download'
Then create a new controller (or just an action in an existing controller)
class DownloadsController
def download
filename = [params[:filename], params[:format]].join('.')
path = Rails.root.join( 'downloads', filename )
if File.exists?(path) && user.can_download?( filename )
send_file( path, x_sendfile: true )
else
raise ActionController::RoutingError, "resource not found"
end
end
end
Then you can navigate to
/downloads/path/my_file.txt
And let User#can_download? handle the permission checking..
It's a little different if the files belong to an object rather than being loose files in the filesystem, you'd have to ask for ids rather than filenames and determine the path from the associated model, but the principle is the same.
If I will send 100 email to the registered user and I want to know if users open email or not
How can I do this using Ruby on Rails?
The only way to do this, is to use html email with a tracker image. You need to include a user specific image into the code.
class TrackingController < ApplicationController
def image
# do something with params[:id]
send_file "/path/to/an/image"
end
end
add the following route:
# Rails 2
map.tracking_image "tracking_image/:id.gif", :controller => 'tracking', :action => image
# Rails 3
match 'products/:id', :to => 'tracking#image', :as => "tracking_image"
# Rails 4 (match without verb is deprecated)
get 'producsts/:id' => 'tracking#image', as: 'tracking_image'
# or
match 'producsts/:id' => 'tracking#image', as: 'tracking_image', via: :get
in your email template something like this:
<%= image_tag tracking_image_url(#user.id) %>
But be aware, that this it's not guaranteed that the user reads the email and loads the image, some email clients don't load images, until the user wants to. And If he doesn't you can't do anything about this. Also if the user uses text mail only this won't work neither.
Short answer, You can't. Slightly longer answer You can't reliably.
Using something like VERP you can automate the the bounce processing, to get a fairly good idea if the far end mail server accepted the email. But after that all bets are off. You can't really tell what the email server did with it, (route it to junk/spam folder, put in inbox, silently drop it on the floor/bit bucket, etc..). You could enable read-receipt headers in your email, but that is client specific (and people like me eat/deny them). You can look into using a web bug, for example customize each email with an HTML file, that pulls a remote image, that has a unique id associated with it, but again client specific, most will not load remote images. So unless the email bounces there is no 100% reliable way to tell what happens to the email after it leaves your server.
I am not very familiar with ruby but have written multiple mass mailer apps. You can use a webbug image to get an approximate open rate. Basically it is just a one pixel or transparent image with some tracking information:
<img src="http://mysite/trackingimage.gif?email=x&customer=y">
What I do is make a directory called trackingimage.gif with an index in it that reads and stores the url params and then relocates to the real image.
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.
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.
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.
This is late in the day to be answering, but another option altogether would be to store the files in MongoDB's GridFS, served through a bit of Rack Middleware that requires auth to be passed. Pretty much as secure as you like, and the URLs don't even need obfuscation.
The other benefit of this is in the availability of the files and the future scalability of the system.
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:
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)
Adds some clutter to views and routes (a small amount, maybe a bit more than I'd like)
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...)
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}"
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...