Serving files over HTTPS dynamically based on request.ssl? with Attachment_fu - ruby-on-rails

I see there is a :user_ssl option in attachment_fu which checks the amazon_s3.yml file in order to serve files via https://
In the s3_backend.rb you have this method:
def self.protocol
#protocol ||= s3_config[:use_ssl] ? 'https://' : 'http://'
end
But this then makes it serve ALL s3 attachments with SSL. I'd like to make it dynamic depending if the current request was made with https:// i.e:
if request.ssl?
#protocol = "https://"
else
#protocol = "http://"
end
How can I make it work in this way? I've tried modifying the method and then get the NameError: undefined local variable or method `request' for Technoweenie::AttachmentFu::Backends::S3Backend:Module error

The problem is that the method you're modifying (Technoweenie::AttachmentFu::Backends::AWS::S3.protocol) is static and does not have access to the file or request in question. The one you want to modify is Technoweenie::AttachmentFu::Backends::AWS::S3#s3_url(thumbnail). You'll have to add an options argument so your controller can pass in whether it wants SSL or not, since this model-level package has no understanding of controller-level issues like "current request" (nor should it).
The real answer, though, is "you probably don't want to do this." If the customer is saying something like "we have a freemium model wherein only our paying customers get SSL transfers of their photos," you should push back: "it's actually harder to cripple SSL file transfers, and it's likely to just introduce bugs down the road. Let's think of another freemium option to offer." If the customer doesn't really care, you might as well just turn SSL on for all uploads.

This is significant issue that needs to be solved correctly, or the implications are quite nasty (particularly if you don't test in IE, the errors and warnings may slip by you). My solution is to put the following in ApplicationController
around_filter :set_attachment_fu_protocol
def set_attachment_fu_protocol
protocol = Technoweenie::AttachmentFu::Backends::S3Backend.instance_variable_get(:#protocol)
Technoweenie::AttachmentFu::Backends::S3Backend.instance_variable_set(:#protocol, request.protocol)
yield
ensure
Technoweenie::AttachmentFu::Backends::S3Backend.instance_variable_set(:#protocol, protocol)
end
This solution was designed to have the following properties:
Doesn't require patching attachment_fu
Sets the protocol for S3 Backend per request
Resets the protocol even if an exception occurs
Preserves the default :use_ssl setting if you are running from the console
Doesn't require the around_filter to be universal since it always resets it to the original state after each request

Related

How to fix Brakeman redirect issue with multiple rest endpoints

I'm currently working on a solution for doing redirects in RoR because I got an error within the brakeman report saying that I have to fix redirects in a proper way.
I understand what the message says and how to solve it within one controller action.
But now I got the following. During the instantiation of the new method I set the HTTP_REFERER header which can be used in the create action.
This is giving me a Brakeman warning which can be found on the following link
Suppose I got the following controller with multiple endpoints:
def new
#my_model_set = MyModel.new
#referer = request.env['HTTP_REFERER'] # We want to redirect to this referer after a create
end
def create
...
if #my_model_set.save
flash_message :success, t('notification.item_created', type: #my_model_set.model_name.human)
if params[:referer].present?
redirect_to params[:referer]
else
redirect_to admin_my_model_set_path
end
else
...
end
end
I already tried to fix this by using the redirect_back method from RoR but that's using the referer link of the create method which I don't want to use.
if #my_model_set.save
flash_message :success, t('notification.item_created', type: #my_model_set.model_name.human)
redirect_back(fallback_location: admin_my_model_set_path)
else
...
end
The main problem in your code is that params[:referer] can be set by your user (or an attacker forging a link for your user) to an arbitrary value by appending ?referer=https://malicious.site to the url. You will then redirect to that, which is an open redirect vulnerability.
You could also argue that the referer header is technically user input, and you will be redirecting to it, but I would say in most cases and modern browsers that would probably be an acceptable risk, because an attacker does not really have a way to exploit it (but it might depend on the exact circumstances).
One solution that immediately comes to mind for similar cases would be the session - but on the one hand this is a rest api if I understand correctly, so there is no session, and on the other hand, it would still not be secure against an attacker linking to your #new endpoint from a malicious domain.
I think you should validate the domain before you redirect to it. If there is a common pattern (like for example if all of these are subdomains of yourdomain.com), validate for that. Or you could have your users register their domains first before you redirect to it (see how OAuth2 works for example, you have to register your app domain first before the user can get redirected there with a token).
If your user might just come from anywhere to #new and you want to send them back wherever they came from - that I think is not a good requirement, you should probably not do that, or you should carefully assess the risk and consciously accept it if you want to for some reason. In most cases there is a more secure solution.

How to make a rails server wide object?

I am using the RedditKit gem and in order to access certain elements, I need to send a request to reddit api to create a "client" object. Below is my current logic:
## application_controller
before_action :redditkit_login
private
def redditkit_login
#client = RedditKit::Client.new ENV["reddit_username"], ENV["reddit_password"]
end
As you can see in my logic here, before EVERY REQUEST, I am subsequently making a new client object and then using that everywhere.
My question is, how do I only make one client object which can be used to serve ALL requests from anywhere?
My motive behind this is speed. For every request to server, I am making a new request to reddit and then responding to the original request. I want to have the client object readily available at all times.
You have a lot of options. A simple one would be to create a config/initializers/reddit_client.rb file and put in there:
RedditClient = RedditKit::Client.new ENV.fetch("reddit_username"), ENV("reddit_password")
(note I switched to ENV.fetch because it will error if the key is not found, which can be helpful).
You could also rename the file as app/models/reddit_client.rb. Although it's not really a model, that folder is also autoloaded so it should work as well.

Is it safe to accept URL parameters for populating the `url_for` method?

I am using Ruby on Rails 4.1.1 and I am thinking to accept parameters (through URL query strings) that are passed directly to the url_for method, this way:
# URL in the browser
http://www.myapp.com?redirect_to[controller]=users&redirect_to[action]=show&redirect_to[id]=1
# Controller
...
redirect_to url_for(params[:redirect_to].merge(:only_path => true))
Adopting the above approach users can be redirected after performing an action. However, I think people can enter arbitraryparams that can lead to security issues...
Is it safe to accept URL parameters for populating the url_for method? What are pitfalls? What can happen in the worst case?
By logging params during requests to my application I noted Rails adds always :controller and action parameters. Maybe that confirms url_for can be used the above way since it is protected internally and works as-like Rails is intended to.
This it is safe internally as Ruby On Rails will only be issuing a HTTP redirect response.
As you are using only_path this will protect you from an Open redirect vulnerability. This is where an email is sent by an attacker containing a link in the following format (say your site is example.com).
https://example.com?foo=bar&bar=foo&redirect=http://evil.com
As the user checks the URL and sees it is on the example.com domain they beleive it is safe so click the link. However, if there's an open redirect then the user ends up on evil.com which could ask for their example.com password without the user noticing.
Redirecting to a relative path only on your site fixes any vulnerability.
In your case you are giving users control of your controller, action and parameters. As long as your GET methods are safe (i.e. no side-effects), an attacker could not use this by creating a crafted link that the user opens.
In summary, from the information provided I don't see any risk from phishing URLs to your application.
Rails redirect_to sets the HTTP status code to 302 Found which tells the browser to GET the new path as you defined it by url_for. GET is a considered a safe method in contrast to
... methods such as POST, PUT, DELETE and PATCH [which] are intended for
actions that may cause side effects either on the server, or external
side effects ...
The only problem would have been if someone could gain access to methods such as create and destroy. Since these methods use HTTP methods other than GET (respectively POST and DELETE) it should be no problem.
Another danger here is if you go beyond CRUD methods of REST and have a custom method which responses to GET and changes the database state:
routes.rb
resources something do
member do
get :my_action
end
end
SomethingController
def my_action
# delte some records
end
For future ref:
Rails has a number of security measurements which may also interest you.
It's not exactly an answer, just wanted to point out that you shouldn't use something like
url_for(params)
because one could pass host and port as params and thus the url could lead to another site and it can get worse if it gets cached or something.
Don't know if it threatens anything, but hey, it's worth pointing out

My cookie token is strong enough in order to use that for user authentication purposes?

I am running Ruby on Rails 3 and I would know if the code that I am using in order to set the cookie value for user authentication purposes is strong enough.
In my model I have:
require 'digest'
class User < ActiveRecord::Base
...
def make_cookie_id_salt(string)
secure_hash("#{self.id}--#{string}")
end
def secure_hash(string)
Digest::SHA2.hexdigest(string)
end
end
In my controller I have:
cookies.signed[:current_user_id] = { :value => [#user.id, #user.make_cookie_id_salt(#user.id)], :expires => 15.days.from_now }
Is it strong enough? If no, how I can improve that (make an example!)?
Everything that gets put into cookies is stored as plain text.
If you set a cookie, and then check the cookies in your browser you will notice (in your case the cookie name would be current_user_id) that it is represented by a string of characters like: G8gcm9sbCB5b3VyIG93biBhdXRoIHRvIGt... (Not quite plain text, right? It is actually Base64 encoded, but you can easily read it - require('base64'); Base64.decode64(string)).
Rails stores a special _yourapp_session cookie that is used to check the cookies validity. If for example someone/something was trying to modify it, it would get rejected.
Now in your case it doesn't really matter if you try to hash something in the cookie or not.
It is just used for authentication (to look up a user in the database by his id) and you are not storing any unique secret data (Which you should not place in a cookie anyway, but it would be the only reason to hash something)
Of course someone could steal the cookie of a user (if he used a public computer and hasn't cleared his cache, etc.) and log in, but there's no way to prevent that (No matter what kind of hashing was used to obfsucate it)
In conclusion you should be fine with what you have.
Rather than try to create your own, I suggest using the Authlogic gem. In a few minutes of configuration you get a complete authentication solution, including cookies and much more. If you really want to roll your own, install the Authlogic gem and take a look at how they do it.
Devise is another option. It's extremely configurable, pretty DRY, with exhausting wiki.
For now-days I prefer it over Authlogic.

Rails: Obfuscating Image URLs on Amazon S3? (security concern)

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...

Resources