Rails: Flagging all cookies as secure-only to pass a security audit

Why secure-only cookies used to be necessary

Cookies Archive have an optional secure flag. It tells the browser to not send the cookie for a non-https request.

It used to be important to activate the secure flag even on sites that automatically redirect users from http:// to https://. The reason was that most users will only enter a scheme-less domain like makandra.de into their location bar, which will default to http://makandra.de in any browser. Even though the site will immediately redirect to https://makanda.de, cookies from a prior visit will already have appeared on the unencrypted wire during the first request to http://makandra.de.

Why secure-only cookies are no longer necessary

Today, all sites should use https://. They should also set a HSTS header Archive which makes browsers default to https:// instead of http:// when the user enters a scheme-less domain into their browser's URL bar.

For a page that exclusively uses https:// with HSTS, it is not necessary to set the secure flag on your cookies. There is simply no case when the browser would talk to the server via unencrypted http:// requests.

Why you might need secure-only cookies anyway

A security audit will still raise missing "secure" flags as an issue that needs to be fixed.

It's usually easier flag all your cookies as secure-only, than it is to explain why your application does not need secure cookies.

Rails: Automatically flag all cookies as secure-only

In a Ruby on Rails app you can add a middleware that automatically sets the Secure flag to all server-set cookies. The flag is only added for secure requests, so cookies will still work for local development where you might still use http://.

If you set any cookies from JavaScript, this isn't fixed by the middleware.

Add this to lib/middleware/secure_cookies.rb:

# On HTTPS requests, we flag all cookies sent by the application to be "Secure".
#
module Middleware
  class SecureCookies

    COOKIE_SEPARATOR = "\n".freeze

    def initialize(app)
      @app = app
    end

    def call(env)
      status, headers, body = @app.call(env)

      if headers['Set-Cookie'].present? && Rack::Request.new(env).ssl?
        cookies = headers['Set-Cookie'].split(COOKIE_SEPARATOR)

        cookies.each do |cookie|
          next if cookie.blank?
          next if cookie =~ /;\s*secure/i

          cookie << '; Secure'
        end

        headers['Set-Cookie'] = cookies.join(COOKIE_SEPARATOR)
      end

      [status, headers, body]
    end

  end
end

Add this to your Middleware stack in the middle of config/application.rb:

    require 'middleware/secure_cookies'
    config.middleware.insert_after ActionDispatch::Static, Middleware::SecureCookies

Add a test to spec/requests/secure_cookies_spec.rb:

describe Middleware::SecureCookies do

  it 'flags all cookies sent by the application as secure' do
    get 'https://www.example.com/test/set_cookie'
    response.headers['Set-Cookie'].should =~ %r(test=\S+; path=/; Secure$)
  end

  it 'will not flag cookies as secure when HTTPS is not being used (in development and tests)' do
    get 'http://www.example.com/test/set_cookie'
    response.headers['Set-Cookie'].should include('test=') # Cookie is still set
    response.headers['Set-Cookie'].should_not include('; Secure')
  end

end

Note that /test/set_cookie must be an existing route that sets a cookie.

Henning Koch almost 4 years ago
This website uses short-lived cookies to improve usability.
Accept or learn more