Skip to main content

Rails, Secure Cookies, HSTS and friends

·8 mins

Ruby on Rails as a framework does a lot of things for us developers. We get a very customizable middleware stack, great routing system, very expressive ORM, helpful modules with great utility methods in them and so on. But in Rails there’s more than meets the eye. It does some great things that we just take for granted or on occasions we don’t even know they exist.

Some of these features are TLS redirection, secure cookies and HTTP Strict Transport Security (HSTS). Let’s dive in into the Rails middleware stack and see what these things mean and what benefits they provide.

HTTP Strict Transport Security #

According to Wikipedia:

HTTP Strict Transport Security (HSTS) is a web security policy mechanism which helps to protect secure HTTPS websites against downgrade attacks and cookie hijacking. It allows web servers to declare that web browsers (or other complying user agents) should only interact with it using secure HTTPS connections and never via the insecure HTTP protocol. HSTS is an IETF standards track protocol and is specified in RFC 6797.

The HSTS Policy is communicated by the server to the user agent via an HTTP response header field named “Strict-Transport-Security”. HSTS Policy specifies a period of time during which the user agent shall access the server in a secure-only fashion.

This is a short and nice summary of HSTS. The Internet Engineering Task Force (IETF) have solved the problem by making web servers send a HTTP response header which will make the browsers use HTTPS over HTTP when requesting any of the resources on that particular web server.

If you take a deeper look, into the Request for Comments (RFC) No. 6797 where HSTS was proposed, you will see this:

2.3. Threat Model

HSTS is concerned with three threat classes: passive network attackers, active network attackers, and imperfect web developers. However, it is explicitly not a remedy for two other classes of threats: phishing and malware. Threats that are addressed, as well as threats that are not addressed, are briefly discussed below.

It’s a good thing that the IETF and the Rails core developers know that we are imperfect (read: lazy), so they have our backs. *wink emoji*

Since we got the basics right, let’s look at the structure of the HSTS header:

Strict-Transport-Security: max-age=31536000; includeSubdomains; preload

It is composed of three directives: max-age, includeSubdomains and preload. The max-age directive tells the browser that this is the duration of time for which HSTS will be active for the domain. The includeSubdomains directive is quite self-explanatory: it tells the browser that HSTS will be active for all subdomains. The last one, preload, is created by the Chrome security team. It’s purpose is to create a list of domains that will be preloaded to Chrome, so Chrome knows that HSTS will be preloaded for the given domain. Later, this preloading mechanism got incorporated to Firefox, Safari and Internet Explorer. This allows HSTS to kick in even for the first visit of the website.

Secure cookies #

Cookies, the ones that browsers consume, have multiple values and flags on them. Here’s a screenshot of someone’s cookies as shown in the Chrome Developer Tools:


As you can see in the screenshot, a cookie has a name, a value, the domain (or owner), the path, expiry date/time, it’s size, the HttpOnly flag, the Secure flag and the First-Party field. For the purpose of this article, we are only interested in the Secure flag.

The secure flag tells the browser that it can send this cookie to the owner only via HTTPS. This protects the user when under a Man in the middle (MITM) attack. Basically, if someone steals the session cookie from the user, it will always be encrypted with SSL/TLS so it will be unusable for the attacker.

SSL/TLS Redirection #

In comparison to HSTS and Secure Cookies this is a really simple security mechanism. Rails has the ability to redirect clients accessing it from HTTP to HTTPS. Think of it in this way - if the proper configuration is set, it will check if the request comes via HTTPS. If not, it redirects the client to the same URL, just via HTTPS. Simple as that.

Back to Rails #

Now, how does Rails implement these mechanisms? Think about this: when we are fetching/building the data for the response, whether it’s XML, JSON or HTML, we rarely do anything with the response headers. We usually render some document/data and we let Rails take care of the rest. So, there has to be some configuration where we can turn on TLS redirection, secure cookies and HSTS.

Rails configuration can be found in multiple places. If it’s configuration per environment, it’s usually config/<environment>.rb. If it’s general configuration - config/application.rb. Other times, it can be in config/initializers. It very much depends on what part of the application you want to configure.

By default Rails has some configurations set up for us. For example, when a brand new Rails application is generated, in the config/production.rb file you can see the following lines:

# Force all access to the app over SSL, use Strict-Transport-Security,
# and use secure cookies.
config.force_ssl = true

As you can notice in the comment, this is the line that enables all these security mechanisms. Quite self-descriptive, the attribute is called force_ssl. For production environment, this is set to true by default. Let’s see how this actually works under the hood.

Where does it do it? #

Searching through the Rails source code, starting from the Rails::Application::Configuration where I noticed that the force_ssl configuration is set to false by default. But, this was a dead end - it was only the Rails configuration object, nothing else.

Searching on for the force_ssl property, I ran into Rails::Application::DefaultMiddlewareStack (source). In the #build_stack method I noticed that when Rails is booting up, it decides if the Rails::Application::ActionDispatch::SSL middleware should be pushed onto the middleware stack. This was the obvious place to look, so let’s open that class.

Enforcing TLS/SSL and HSTS #

In the Rails::Application::ActionDispatch::SSL there’s the build_hsts_header method:

def build_hsts_header(hsts)
  value = "max-age=#{hsts[:expires].to_i}"
  value << "; includeSubDomains" if hsts[:subdomains]
  value << "; preload" if hsts[:preload]

It is being called from the #initialize method, which is where an object from the middleware class is created:

def initialize(app, redirect: {}, hsts: {}, **options)
  @app = app

  if options[:host] || options[:port]
    ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc
    The `:host` and `:port` options are moving within `:redirect`:
    `config.ssl_options = { redirect: { host: …, port: … }}`.
    @redirect = options.slice(:host, :port)
    @redirect = redirect

  @hsts_header = build_hsts_header(normalize_hsts_options(hsts))

The #build_hsts_header method takes a normalized hash of HSTS options and builds the header based on the values in the hash. This all takes effect in the #call method:

def call(env)
  request = env

  if request.ssl? do |status, headers, body|
      set_hsts_header! headers
      flag_cookies_as_secure! headers
    redirect_to_https request

After the HSTS header is added to the request, it is passed down onto the rest of the middleware stack.

Securing the cookies #

Securing the cookies is done in the same module. The flag_cookies_as_secure! method looks for the Set-Cookie header in the request and appends the secure property to the Set-Cookie header if needed.

def flag_cookies_as_secure!(headers)
  if cookies = headers['Set-Cookie'.freeze]
    cookies = cookies.split("\n".freeze)

    headers['Set-Cookie'.freeze] = { |cookie|
      if cookie !~ /;\s*secure\s*(;|$)/i
        "#{cookie}; secure"

For reference, this is what the Set-Cookie header looks like:

Set-Cookie: cookie-name=cookie-value-here; path=/; expires=Fri, 01 Jan 2016 00:00:00 -0000; secure; HttpOnly

This will tell the browser that the cookie should be secured and sent only via HTTPS. Just like the HSTS header, the secure cookies header is attached to the request in the #call method (which you can see above).

Note: do not confuse the secure directive with the HttpOnly directive.

TLS redirection #

Another feature of Rails is the so-called “TLS redirection”. It’s workings are quite simple - whenever you have force_ssl set to true, it will redirect all of the HTTP traffic to HTTPS. Again, the Rails::Application::ActionDispatch::SSL middleware class is responsible for this behaviour of Rails, more specifically the redirect_to_https method, which invokes the https_location_for method:

def redirect_to_https(request)
  [ @redirect.fetch(:status, 301),
    { 'Content-Type' => 'text/html',
      'Location' => https_location_for(request) },
    @redirect.fetch(:body, []) ]

def https_location_for(request)
  host = @redirect[:host] ||
  port = @redirect[:port] || request.port

  location = "https://#{host}"
  location << ":#{port}" if port != 80 && port != 443
  location << request.fullpath

As you can notice, redirect_to_https rebuilds a response which will have the same Content-Type and body, but the Location and HTTP Status will be different.

In it’s core, the middleware just wraps the incoming request and checks if it’s SSL/TLS based. If not, it redirects the web client to the same resource on the server, just via HTTPS.

Outro #

As we saw in this article, Rails does some magic under the hood for us. It’s really great that we all get these great benefits without even bothering to set them up. Although we get them by default, it’s good to know what happens with our application under the hood.

I hope this article was fun to read and informative for you. Do you have a favourite “hidden” functionality of Rails that not many of us know? Please share your thoughts with me in the comments section below.