12

In a Rails app, the session cookie can be easily set to include the secure cookie attribute, when sending over HTTPS to ensure that the cookie is not leaked over a non-HTTP connection.

However, if the Rails app is NOT using HTTPS, but HTTP only, it seems that it doesnt even set the cookie at all.
While this does make some sense, in this scenario there is a seperate front end load balancer, which is responsible for terminating the SSL connection. From the LB to the Rails app, the connection is HTTP only.

How can I force the Rails app to set a secure cookie, even when not using HTTPS?

AviD
  • 12,944
  • 7
  • 61
  • 91
  • 1
    Just so I understand correctly, if I set a cookie via `cookies[:somekey] = {value: 'somevalue', secure: true}`, then the cookie is not set in the response from Rails over HTTP? – accounted4 Jan 26 '13 at 21:16
  • @SebastianGoodman good question - but haven't actually tested that, I'm talking about the automatic session cookie, so it's not actually being set explicitly in code. Perhaps that might be a workaround, if it works... – AviD Jan 27 '13 at 00:48
  • 1
    So when you say "the session cookie can be easily set to include the secure cookie attribute", how are you doing in code exactly? – accounted4 Jan 27 '13 at 00:56
  • Same problem here. Something in Rails blocking secure cookie when we want to send them over http (because we have https converter later on) – Nathan B Apr 02 '20 at 09:35

3 Answers3

11

Secure cookies are not sent over non-secure connections by definition.

Terminating SSL upstream is quite common, but you need to pass certain header fields through so that Rails knows and can do the right thing.

Here's a document that explains the configuration in pretty good detail for nginx. Search for "Set headers" to jump to the section describing the specific headers you need to pass through.

There are security considerations using this configuration, e.g., if the device terminating SSL is not on the same secure LAN as the Rails host, then you have a vulnerability.

pduey
  • 3,706
  • 2
  • 23
  • 31
  • Excellent, thats what I was hoping to find. Interesting that this is a solution at the nginx, not at the Rails... Thanks! – AviD Feb 21 '13 at 18:13
  • 1
    Thanks for the link - adding `proxy_set_header X-Forwarded-Proto https;` to nginx config worked great! – house9 Jan 23 '14 at 01:01
  • We still need a solution how to force Rails not to block secure-http cookies – Nathan B Apr 02 '20 at 09:35
  • not blocking secure-http cookies is not possible because by definition of 'secure' these cookies must not be sent by http (only https), Thats basicly what "secure" means. – Martin M Nov 11 '20 at 10:51
0

The same problem happened to me, and I solved it this way: In session_store.rb I configured:

MyApp::Application.config.session_store :cache_store, key: COOKIE_NAME, :expire_after => 1.days, :expires_in => 1.days, :domain => 'mydomain.com', same_site: :none

(note that I didn't put the , same_site: :none here, because it would kill the set cookie completely when serving HTTP)

And then I monkey patched the Rack->Utils->set_cookie_header, by placing this file in the initializers folder

require 'rack/utils'
module Rack
  module Utils
    def self.set_cookie_header!(header, key, value)
      case value
      when Hash
        domain  = "; domain="  + value[:domain] if value[:domain]
        path    = "; path="    + value[:path]   if value[:path]
        max_age = "; max-age=" + value[:max_age] if value[:max_age]
        expires = "; expires=" +
            rfc2822(value[:expires].clone.gmtime) if value[:expires]

        # Make secure always, even in HTTP
        # secure = "; secure"  if value[:secure]
        secure = "; secure"

        httponly = "; HttpOnly" if value[:httponly]
        same_site =
            case value[:same_site]
            when false, nil
              nil
            when :none, 'None', :None
              '; SameSite=None'
            when :lax, 'Lax', :Lax
              '; SameSite=Lax'
            when true, :strict, 'Strict', :Strict
              '; SameSite=Strict'
            else
              raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
            end
        value = value[:value]
      end
      value = [value] unless Array === value
      cookie = escape(key) + "=" +
          value.map { |v| escape v }.join("&") +
          "#{domain}#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"

      case header["Set-Cookie"]
      when nil, ''
        header["Set-Cookie"] = cookie
      when String
        header["Set-Cookie"] = [header["Set-Cookie"], cookie].join("\n")
      when Array
        header["Set-Cookie"] = (header["Set-Cookie"] + [cookie]).join("\n")
      end

      nil
    end
  end
end
Nathan B
  • 1,625
  • 1
  • 17
  • 15
0

The base issue is, that by definition of Set-Cookie, cookies with secure set may only be sent via secure connetions.
So not sending cookies with secure set over HTTP is the expected behavior.

You might want to set different cookie options in different environments. In config/environments/developent.rb you clould set

Rails.application.configure do
  config.session_store :cache_store, key: COOKIE_NAME, same_site: :none
end

and in production (config/environments/production.rb), where you deploy your site with HTTPS:

Rails.application.configure do
  config.session_store :cache_store, key: COOKIE_NAME, same_site: :lax, secure: true
end
Martin M
  • 8,430
  • 2
  • 35
  • 53