Conditionally Implementing HSTS, SSL, and Secure Cookies in Rails Based on Domain - ruby-on-rails

I run an application that hosts websites from multiple domains from a single application and server. I am moving some of those domains to SSL, but others are staying at http. I'm running Rails 4.x. I believe I CAN'T just use the
config.force_ssl = true
because that would implement it for all domains, which I don't want.
I know in the ApplicationController I can do something like
force_ssl if: :ssl_required?
def ssl_required?
return [secure_domain1, domain2, domain3].include? request.domain
end
But as I understand it, that doesn't implement HSTS or secure cookies. My two questions are:
Is there a better way to implement it than what I have above?
If I do go the above route, is there a way to conditionally send secure
cooking and implement HSTS for only those domains?
If there is no easy way to enable HSTS or secure cookies, and having those is worth the hassle, I can always split my app and host it on two different servers, with one instance containing all https domains and the other containing only http domains.
Thanks for your thoughts

Using Rails to do that is actually not a bad idea, but using NGINX for that would be even better as comments have pointed out. If you really want to pursuit the Rails solution you could do something like that in the very same def:
force_ssl if: :ssl_required?
def ssl_required?
if [secure_domain1, domain2, domain3].include? request.domain
#HSTS for Rails <=4
response.headers['Strict-Transport-Security'] = 'max-age=315360000; includeSubdomains; preload'
#HSTS for Rails >=5
response.set_header('Strict-Transport-Security', 'max-age=315360000; includeSubdomains; preload')
cookies[:secure] = true
true
else
false
end
end
You could always tune your HSTS header to the desired max-age or use a more idiomatic approach putting #{365.days.to_i} instead of the simple string header.

Related

Cannot display my rails 4 app in iframe even if 'X-Frame-Options' is 'ALLOWALL'

I am trying to test a responsive design. I am using Rails 4.
I know it sets 'X-Frame-Options' to SAME ORIGIN. So I overrided it in development.rb using
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'ALLOWALL'
}
and it worked. I checked out the network request in the Chrome console and it is as follows:
But still websites like responsive.is and responsinator.com give me below error:
Refused to display 'http://localhost:3000/' in a frame because it set 'X-Frame-Options' to 'SAMEORIGIN'. about:blank:1
Whats going on??
Try just to delete this header 'X-Frame-Options'.
Maybe this way in controller:
before_filter :allow_iframe_requests
...
def allow_iframe_requests
response.headers.delete('X-Frame-Options')
end
I had the same problem as you, and searched for a solution to this problem all night.
I finally found out why it happens. It's because of the Chrome cache.
You can see the header['X-Frame-Options'] is ALLOWALL but it doesn't work.
Just try to open a "New Incognito Window" and go the same page and it works!
This problem only happened in development mode in my test. It worked fine in production mode.
Rails 4 added a default X-Frame-Options HTTP header value of SAMEORIGIN. This is good for security, but when you do want your action to be called in an iframe, you can do this:
To Allow all Origins:
class MyController < ApplicationController
def iframe_action
response.headers.delete "X-Frame-Options"
render_something
end
end
To Allow a Specific Origin:
class MyController < ApplicationController
def iframe_action
response.headers["X-FRAME-OPTIONS"] = "ALLOW-FROM http://some-origin.com"
render_something
end
end
Use :after_filter
When you need to use more than one of your action in an iframe, it's a good idea to make a method and call it with :after_filter:
class ApplicationController < ActionController::Base
private
def allow_iframe
response.headers.delete "X-Frame-Options"
end
end
Use it in your controllers like this:
class MyController < ApplicationController
after_filter :allow_iframe, only: [:basic_embed, :awesome_embed]
def basic_embed
render_something
end
def awesome_embed
render_something
end
# Other Actions...
end
Do a Hard-Refresh in your browser, or use another browser to view changes
Via: Rails 4: let specific actions be embedded as iframes
When 'Load denied by X-Frame-Options' using Heroku & Firefox
I had a similar issue where I kept getting this error only on Firefox. I had a PHP web page hosted # MochaHost serving a Rails app hosted # Heroku (so RoR app has a page with an iframe which is pointing to the PHP web page and this working on all browsers except on Firefox).
I was able to solve the problem by setting a default header for all of my requests in the specific environment file:
# config/enviroments/production.rb
config.action_dispatch.default_headers = { 'X-Frame-Options' => 'ALLOWALL' }
Edit
(as sheharyar suggested)
Ideally, you shouldn't set a default header and do this only for actions that have to be rendered in an iFrame. If your entire app is being served inside an iFrame, you should explicitly mention the Origin:
# config/enviroments/production.rb
config.action_dispatch.default_headers = { 'X-Frame-Options' => 'ALLOW-FROM http://some-origin.com' }
Try ALLOW-FROM http://example.com instead? ALLOWALL might be ok in Chrome if you have a sufficiently new version of Chrome [2]
[1] https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
[2] https://stackoverflow.com/a/16101968/800526
I found another cause for this. Assuming the ALLOWALL or similar fix is implemented, the next gotcha is attempting to use http content in a https website which causes security risks and is blocked by mozilla, IE and probably other browsers. It took me 6 hours to identify this, hopefully by sharing I can reduce someones pain...
It can be checked by:
using your browser web-tools which should display an error.
web logs will lack any connection with your supplying site.
replace your contents url with a banks https home page should demonstrate the iframe otherwise works.
The solution is to ask the source if they have https content or find another supplier.
ref:
https://developer.mozilla.org/en/docs/Security/MixedContent
https://developer.mozilla.org/en-US/docs/Security/MixedContent/How_to_fix_website_with_mixed_content
I just wanted to give an updated answer here on dealing with embedding a Rails app in an iframe.
Its not a great idea to simply delete X-Frame-Options headers without having some other kind of security enforced to prevent against Clickjacking (which is the vulnerability X-Frame-Options is largely trying to protect you from).
The problem is that the X-Frame-Options 'ALLOW-FROM' option is not accepted on most major browsers anymore.
As of writing this, May 28th 2020, the best solution for preventing Clickjacking and hosting your app in an iframe is to implement a Content-Security-Policy and set a 'frame_ancestors' policy. The 'frame_ancestors' key designates what domains can embed your app as an iframe. Its currently supported by major browsers and overrides your X-Frame-Options.
You can set up a Content-Security-Policy with Rails 5.2 in an initializer (example below), and for Rails < 5.2 you can use a gem like the Secure Headers gem: https://github.com/github/secure_headers
You can also override the policy specifications on a controller/action basis if you'd like.
Content-Security-Policies are great for advanced security protections. Check out all the things you can configure in the Rails docs: https://edgeguides.rubyonrails.org/security.html
A Rails 5.2 example for a Content-Security-Policy:
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
policy.frame_ancestors :self, 'some_website_that_embeds_your_app.com'
end
An example of a controller specific change to a policy:
# Override policy inline
class PostsController < ApplicationController
content_security_policy do |p|
p.frame_ancestors :self, 'some_other_website_that_can_embed_posts.com'
end
end
If you want to have this change take effect in all environments, place it in application.rb.

Redirect http://domain.com/subpage to http://www.domain.com/subpage in Rails

I assume this is not really a difficult issue but I am using so many different solutions and don't really know what it best.
I am using Ruby on Rails and have my apps on Heroku and I want to 301 redirect everything on my naked domain (#) to my www-domain. E.g.
http://domain.com --> http://www.domain.com
http://domain.com/subpage --> http://www.domain.com/subpage
Right now I am handling this with my DNS by first deleting both # and www entries. Then I set a redirect of the entire website to http://www.domain.com (which re-creates the DNS entries for both # and www). Lastly I change my www DNS-entry to CNAME and the name of the heroku-app (http://myapp.herokuapp.com).
This seems to be forwarding http://domain.com/subpage to http://www.domain.com (without the subpage).
What I am looking for now is the proper/recommended way to handle this in a simple/elegant way.
DNS?
Routes?
.htaccess? (if so, how do I alter .htaccess in RubyOnRails)
Thanks in advance!
These questions may be related to your question, maybe have a look at those. It seems (from what I have read), that heroku does not allow access to anything like .htaccess or anything, so it seems like Rack middleware would be the best option.
Where is your domain registered? If with GoDaddy, they offer a service that will handle the 301 redirect for you, but it requires that you sign up for one of their hosting plans. The lowest cost will do (~$5 per month). Not free, but painless and requires no coding, etc.
(notice : did not try anything of this, but it should work)
One might argue that the nice way is to use the DNS or your webservers capabilities to do that. However, it is possible to do it with rails if you need it.
All in all, it has the advantage that you will easily keep any params / path in the process, as you told you wanted. Plus, the logic is inside the app and won't be lost if you need to scale up / change domain name. On the bad side, your full stack will be hit anytime someone uses the bare domain name.
you can try a before_filter in your application controller :
before_filter :redirect_to_www
def redirect_to_www
redirect_to subdomain: 'www' unless request.subdomain == 'www'
end
if you want to avoid a "magic redirection" and make it clear for everyone, on rails 3 you can do this directly in the routes:
constraints ->(request){ request.subdomain != 'www' } do
match '(*all)' => redirect( subdomain: 'www' )
end
you should also add the subdomain to your default url options (application controller):
def default_url_options( option = {} )
{subdomain: 'www'}
end
In the end what I had to do was to point the #-DNS to my normal web host (since # demands an IP and Heroku will only accept CNAME) and the www-DNS to Heroku.
At my normal webhost I put a .htaccess that redirected all traffic there to my www-domain like this:
RewriteEngine On
### re-direct to www
RewriteCond %{HTTP_HOST} ^domain.com
RewriteRule (.*) http://www.domain.com/$1 [R=301,L]

Ruby on Rails 2.3.8: is there a way to always forward to the https version of an url?

So, in some of my database records, there is an object, "content" that has a body, much like a document would. Sometimes the body has URL that point to images on my app server. Many of these URLs are HTTP.
Is there anyway to redirect all HTTP requests to HTTPS?
I'm using Rails 2.3.8
and the Paperclip gem
Because some browser security settings will block HTTP assets when viewing a HTTPS page, you don't want to do this at the web server (e.g. mod_rewrite) level since some browsers won't make it that far.
You don't want to handle this at the view or controller level - the model is where you enforce business rules like keep it all HTTPS.
1) Prevent HTTP links from being saved
class Content < ActiveRecord::Base
...
before_validation :force_https
def force_https
unless body.nil?
self.body.gsub! /http:\/\/my\.app\.server/, 'https://my.app.server'
end
end
...
end
2) Clean up existing content
Fire up console and run this:
Content.all.each do |c|
c.update_attribute 'body',
c.body.gsub(/http:\/\/my\.app\.server/, 'https://my.app.server')
end

Rails redirect with https

I'm maintaining a Ruby on Rails site and I'm confused as to how to perform redirects to relative URLs using the https protocol.
I can successfully create a redirect to a relative URL using http, for example:
redirect_to "/some_directory/"
But I cannot discern how to create a redirect to a URL using the https protocol. I have only been able to do so by using absolute URLS, for example:
redirect_to "https://mysite.com/some_directory/"
I would like to keep my code clean, and using relative URLs seems like a good idea. Does anyone know how to achieve this in Rails?
The ActionController::Base#redirect_to method takes an options hash, one of the parameters of which is :protocol which allows you to call:
redirect_to :protocol => 'https://',
:controller => 'some_controller',
:action => 'index'
See the definition for #redirect_to and #url_for for more info on the options.
Alternatively, and especially if SSL is to be used for all your controller actions, you could take a more declarative approach using a before_filter. In ApplicationController you could define the following method:
def redirect_to_https
redirect_to :protocol => "https://" unless (request.ssl? || request.local?)
end
You can then add filters in your those controllers which have actions requiring SSL, e.g:
class YourController
before_filter :redirect_to_https, :only => ["index", "show"]
end
Or, if you require SSL across your entire app, declare the filter in ApplicationController:
class ApplicationController
before_filter :redirect_to_https
end
If you want your entire application to be served over https then since Rails 4.0 the best way to do this is to enable force_ssl in the configuration file like so:
# config/environments/production.rb
Rails.application.configure do
# [..]
# Force all access to the app over SSL, use Strict-Transport-Security,
# and use secure cookies.
config.force_ssl = true
end
By default this option is already present in config/environments/production.rb in in newly generated apps, but is commented out.
As the comment says, this will not just redirect to https, but also sets the Strict-Transport-Security header (HSTS) and makes sure that the secure flag is set on all cookies. Both measures increase the security of your application without significant drawbacks. It uses ActionDispatch:SSL.
The HSTS expire settings are set to a year by default and doesn't include subdomains, which is probably fine for most applications. You can configure this with the hsts option:
config.hsts = {
expires: 1.month.to_i,
subdomains: false,
}
If you're running Rails 3 (>=3.1) or don't want to use https for the entire application, then you can use the force_ssl method in a controller:
class SecureController < ApplicationController
force_ssl
end
That's all. You can set it per controller, or in your ApplicationController. You can force https conditionally using the familiar if or unless options; for example:
# Only when we're not in development or tests
force_ssl unless: -> { Rails.env.in? ['development', 'test'] }
You're probably better off using ssl_requirement and not caring if a link or redirect is or isn't using https. With ssl_requirement, you declare which actions require SSL, which ones are capable of SSL and which ones are required not to use SSL.
If you're redirecting somewhere outside of your Rails app, then specifying the protocol as Olly suggests will work.
If you want to globally controll the protocol of urls generated in controllers, you can override the url_options method in you application controller. You could force the protocol of the generated urls depending on the rails env like so :
def url_options
super
#_url_options.dup.tap do |options|
options[:protocol] = Rails.env.production? ? "https://" : "http://"
options.freeze
end
end
this example works in rails 3.2.1, i'm not exactly sure for earlier or future versions.
This answer is somewhat tangential to the original question, but I record it in case others end up here in similar circumstances to myself.
I had a situation where I needed to have Rails use https proto in url helpers etc. even though the origin of all requests is unencrypted (http).
Now, ordinarily in this situation (which is normal when Rails is behind a reverse proxy or load balancer etc.), the x-forwarded-proto header is set by the reverse proxy or whatever, so even though requests are unencrypted between the proxy & rails (probably not advisable in production by the way) rails thinks everything is in https.
I needed to run behind an ngrok tls tunnel. I wanted to have ngrok terminate the tls with letsencrypt certificates I specified. However when it does so, ngrok does not offer the ability to customize headers, including setting x-forwarded-proto (although this feature is planned at some point in the future).
The solution turned out to be quite simple: Rails does not depend on either the protocol of the origin or whether x-forwarded-proto is set directly, but on the Rack env var rack.url_scheme. So I just needed to add this Rack middleware in development:
class ForceUrlScheme
def initialize(app)
#app = app
end
def call(env)
env['rack.url_scheme'] = 'https'
#app.call(env)
end
end
In Rails 4 one can use the force_ssl_redirect before_action to enforce ssl for a single controller. Please note that by using this method your cookies won't be marked as secure and HSTS is not used.
If you want to force ALL traffic via https, then the best way in Rails 6 is to configure production.rb with:
config.force_ssl = false
If you need a more flexible solution, you can handle it with a simple before_action filter:
class ApplicationController < ActionController::Base
include SessionsHelper
include LandingpageHelper
include ApplicationHelper
include UsersHelper
include OrganisationHelper
before_action :enforce_ssl, :except => [:health]
def enforce_ssl
if ENV['ENFORCE_SSL'].to_s.eql?('true') && !request.ssl?
redirect_to request.url.gsub(/http/i, "https")
end
end
end
If you run your application on AWS ECS Fargate with health checks, then you need a more flexible solution because the health check from the AWS target group is not invoked via https. Of course, you want the health check to work and at the same time, you want to force SSL for all other controller methods.
The ENFORCE_SSL is just an environment variable that turns this feature on/off.
Add protocol to ..._url:
redirect_to your_url(protocol: 'https')
or with subdomain:
redirect_to your_url(protocol: 'https', subdomain: 'your_subdomain')
Relative URLs, by definition, use the current protocol and host. If you want to change the protocol being used, you need to supply the absolute URL. I would take Justice's advice and create a method that does this for you:
def redirect_to_secure(relative_uri)
redirect_to "https://" + request.host + relative_uri
end
Open the class that has redirect_to and add a method redirect_to_secure_of with an appropriate implementation. Then call:
redirect_to_secure_of "/some_directory/"
Put this method in the lib directory or somewhere useful.

One Rails application, several domain names

I want to point several domain names to the same Rails application. The content is different for each domain, but the functionality and the structure of the application is the same.
What is the best way to do this when it comes to server set up and routing? I will use nginx as a web server.
if layout needs to be changed only:
add to application controller
layout :setup_layout
def setup_layout
if request.host == "site1.host.tld"
"layout1"
else
"layout2"
end
end
the same logic you can use to get content, this is true if all sites will use one database.
In nginx conf add more hosts to server_name directive:
server_name site1.host.tld site2.host.tld

Resources