42

In a Rails controller, I can set a cookie like this:

cookies[:foo] = "bar"

And specify that the "secure" (https-only) flag be on like this:

cookies[:foo, :secure => true] = "bar"

:secure is false by default. How can I have cookies be secure by default, application-wide?

This is on Rails 2.3.8

K M Rakibul Islam
  • 33,760
  • 12
  • 89
  • 110
John Bachir
  • 22,495
  • 29
  • 154
  • 227

7 Answers7

53

There's no need to monkeypatch ActionController/ActionDispatch, and force_ssl has side effects (e.g. when behind an ELB).

The most straightforward way to achieve secure cookies is to modify config/initializers/session_store.rb:

MyApp::Application.config.session_store( 
  :cookie_store, 
  key: '_my_app_session',
  secure: Rails.env.production?
)
Jared Beck
  • 16,796
  • 9
  • 72
  • 97
David Cain
  • 16,484
  • 14
  • 65
  • 75
  • 4
    This is the right answer (verified that this works in Rails 4.2.7.1). To test this easily, set `secure: true` and then you can verify the cookie is generated correctly. Then you can change it back to `Rails.env.production?` – Debajit Dec 03 '16 at 01:01
  • 4
    it looks like this will only affect the session cookie. The asking person requested secure as the default for all cookies. Can @david-cain verify whether that solution works for all cookies? – Mike P. Sep 15 '17 at 17:48
19

starting with rails 3.1, according to the rails security guide, you can simply set the following in your application.rb:

config.force_ssl = true

this forces the cookie to be sent over https only (and I assume everything else, too).

Markus
  • 5,667
  • 4
  • 48
  • 64
  • This confused me at first: The *main* purpose of `config.force_ssl` is to only allow the rails app to work over SSL, but it *also* enables the `secure` flag on cookies. Before you set this config make sure you're okay with the rest of its effects: https://stackoverflow.com/questions/15676596/what-does-force-ssl-do-in-rails – Shelvacu Dec 22 '20 at 22:46
4

Thanks @knx, you sent me down the right path. Here's the monkeypatch I came up with, which seems to be working:

class ActionController::Response
  def set_cookie_with_security(key, value)
    value = { :value => value } if Hash != value.class
    value[:secure] = true
    set_cookie_without_security(key, value)
  end
  alias_method_chain :set_cookie, :security
end

What do you think?

John Bachir
  • 22,495
  • 29
  • 154
  • 227
  • John, where in Rails app do we place the class above? – Rafael May 17 '12 at 21:48
  • great. thank you. one more thing, i my dev env - using webrick - when i set the secure flag to true in environment.rb i cannot get passed my login screen - i noticed there is set cookie from app in responses. any idea what could be going one? – Rafael May 18 '12 at 14:11
  • 1
    if you set the secure flag to true on your cookie, then the cookie won't be sent for non-https requests. are you using https in your dev environment? – John Bachir May 18 '12 at 21:57
  • i did set the secure flag to true on my cookie (through ActionController::Base.session). i set up apache httpd with ssl which forwards to WebRick in my dev env and still get same behavior – Rafael May 21 '12 at 16:20
  • 1
    For anyone trying this on Rails 3.2.x, I believe set_cookie has been refactored in to ActionDispatch::Response so monkeypatching that class instead should work. – Chris Hart Nov 04 '12 at 14:36
1
# session only available over HTTPS
ActionController::Base.session_options[:secure] = true
Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
alsotang
  • 1,520
  • 1
  • 13
  • 15
1

Quick and dirty solution: i think it is possible by modifying []= method in action pack cookies module (actionpack/lib/action_controller/cookies.rb)

from:

    def []=(name, options)
      if options.is_a?(Hash)
        options = options.inject({}) { |options, pair| options[pair.first.to_s] = pair.last; options }
        options["name"] = name.to_s
      else
        options = { "name" => name.to_s, "value" => options }
      end

      set_cookie(options)
    end

to:

    def []=(name, options)
      if options.is_a?(Hash)
        options.merge!({:secure => true})
        options = options.inject({}) { |options, pair| options[pair.first.to_s] = pair.last; options }
        options["name"] = name.to_s
      else
        options = { "name" => name.to_s, "value" => options }
      end

      set_cookie(options)
    end
knx
  • 340
  • 2
  • 13
0

You should look at the rack-ssl-enforcer gem. I was just looking for a clean answer to this and it solves the problem independent of which version of Rails you're on, plus it's extremely configurable.

brightball
  • 923
  • 13
  • 11
0

You can do this as mentioned in some of the above answers (use secure option in the config/initializers/session_store.rb file):

MyApp::Application.config.session_store :cookie_store, key: '_my_app_session',
                                                       secure: Rails.env.production?

which will only secure the session cookie, but other cookies will not be secure.

If you want to secure all the cookies in your Rails app by default, you can use the secure_headers gem. Just add the secure_headers gem to your Gemfile, bundle install the gem and create a config/initializers/secure_headers.rb file with this content:

SecureHeaders::Configuration.default do |config|
  config.cookies = {
    secure: true, # mark all cookies as "Secure"
  }
end

This will make all the cookies secure in your Rails app by default.

You can also add these recommended configurations and set the httponly and samesite options as well:

SecureHeaders::Configuration.default do |config|
  config.cookies = {
    secure: true, # mark all cookies as "Secure"
    httponly: true, # mark all cookies as "HttpOnly"
    samesite: {
      lax: true # mark all cookies as SameSite=lax
    }
  }
end
K M Rakibul Islam
  • 33,760
  • 12
  • 89
  • 110