Let’s look at the implications of setting up a Rails session_store cookie with domain: :all.

Sure, it’s a convenient way to allow users to be authenticated across subdomains, however, don’t forget any CNAME subdomains mapped to third-party services will also receive those session cookies too.

First, a quick refresher on the Rails session store and cookie domains!

The Rails session store

The Rails session store is a mechanism for storing user data between requests. It’s useful for keeping track of user info, most often used for authentication status.

In Rails we can use a cookie to effectively store this information on the client-side. This is called the :cookie_store.

Rails encrypts the session cookie meaning its contents are not visible to the user and any tampering with the cookie will render it invalid.

Cookies can have certain attributes associated to them, one of which is a Domain.

Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Expires=<date and time>; SameSite=<Strict/Lax/None>; Secure; HttpOnly

If the Domain attribute is not specified, the cookie is only sent to the exact domain of the host from which it was created. This restricts the cookie to use with one domain.

Otherwise, if Domain is set, it specifies which domains the cookie should be sent to. Ie it restricts the cookie’s visibility to those domains.

The important point is that when the Domain is specified, the cookie is sent to the specified domain and any subdomains of that domain.

As the MDN docs state: “specifying Domain is less restrictive than omitting it.”

This is useful as you can thus share a cookie across multiple subdomains (for example, a session cookie that identifies an authenticated user).

But the fact that our cookie is now less restricted may hide a subtle security risk. More on that soon.

You can inspect cookies in your browser’s dev tools. For example, in Chrome, open the dev tools, go to the Application tab, and select Cookies in the left-hand menu. Cookies with a Domain attribute which starts with a . are also sent to all subdomains of that domain, ie they were created with the Domain attribute. If there is no leading . then the cookie is only sent to the exact domain specified ie it was created without a Domain attribute.

The default

By default, the Rails session_store: :cookie_store cookie is created without a specific Domain.

Thus as mentioned above, it is tightly scoped to the exact domain of the request from which it was created.

Ie if your app host is app.example.com the cookie will only be sent on subsequent requests to exactly app.example.com.

It will not be sent on requests to example.com or foo.example.comor bar.app.example.com.

Manually setting the domain

You can also set the cookie’s Domain manually (though you are unlikely to do so).

You use the domain: option,

Rails.application.config.session_store :cookie_store, key: "_my_app_session", domain: "app.example.com"

This means that the Domain on the Rails session cookie will always be set to app.example.com.

But does that scope it tightly to app.example.com?

Nope! Because it actually tells the browsers to also send the cookie to all subdomains of app.example.com, ie foo.app.example.com and bar.app.example.com will also receive the cookie.

The domain: :all option

The domain: option can also be set to :all, which tells Rails to set the session cookie Domain to the ‘top level domain’ of the app host.

So you are basically saying “I want all subdomains of my apps host to receive this cookie.”

Rails determines what this ‘top level domain’ is automatically.

For example, let’s say your app is running on www.example.com. Rails will set Domain of the session cookie to example.com.

Therefore, that cookie will be sent on requests to app.example.com, foo.example.com, foo.bar.example.com and any other subdomains that exist.

So using the domain: :all option can be a convenient way to ensure users stay authenticated across subdomains.

Your domain CNAMEs may hide a security risk

But there is a potential catch.

Sometimes we use so called “CNAME Cloaking” to map third party services to our own domain.

CNAME is an abbreviated form for Canonical Name record. It’s a type of DNS record that maps one domain name to another.

This was often recommended by analytics companies as a way of avoiding ad-blockers (though its used less now as its effectiveness has waned).

But let’s say you have set domain: :all, now you see that any ‘cloaked’ third-party services which exist on a subdomain will also receive the cookie.

I recommend you have a look at Boston University’s Security Lab paper “Oversharing Is Not Caring: How CNAME Cloaking Can Expose Your Session Cookies” which discusses the concept of “CNAME cloaking”.

So what can you do to allow users to be authenticated across your apps various domains while minimising the risk?

To minimise the risk, you can limit the scope of the Rails session cookie and domains to which it will be sent.

Rails.application.config.session_store :cookie_store, key: '_my_app_session', domain: :all, tld_length: 3

or more simply in config/application.rb, and let Rails set the other defaults:

config.session_config = {domain: :all, tld_length: 3}

By setting the tld_length: option you specify the number of segments of the host domain name (as determined by where the dots . are) that Rails should be considered as the “top-level domain”.

For example, if you set tld_length: 3, from an app running at app.example.com, then Rails will determine the session cookie domain to be app.example.com instead of example.com.

Thus, only subdomains of app.example.com will receive the Rails session cookie.

Your Rails app subdomains who share the session might then be signin.app.example.com, api.app.example.com, admin.app.example.com, etc.

Any other subdomains of example.com, such as thirdparty.example.com, will not receive the cookie.

So increasing the value of tld_length can help minimize the risk of leaking session tokens to third-party services.

It does this essentially by making it more unlikely that a “cloaked” third party would exist under the subdomains of your main Rails application. However, you still have the flexibility to define URLs for your services that share a login.

The secure: option

You may have noticed that the Rails cookie_store also has a secure: option.

This option is used to set the Secure attribute on the cookie, which tells the browser to only send the cookie over HTTPS.

This is important as it helps prevent the session cookie from being sent over an insecure connection, where it could be intercepted by an attacker.

However, you can let Rails deal with this one, assuming you are setting config.force_ssl in your environments. This generally means in development and test environments, the session cookie will not be set to Secure, but in production it will be.

If in doubt… check your production application’s session cookie in your browser’s dev tools, and see if it has the Secure attribute set.

The same_site: option

The same_site: option is used to set the SameSite attribute on the cookie, which controls the browser’s behaviour in relation to when a cookie is sent based on the origin of the request.

There is a great article on web.dev about it.

It defaults to :lax but you can set it to :strict or :none.

Ideally, you should leave this to the default.

You should only change it to :none if you have a specific reason to do so. For example, if your application must work embedded in an iframe inside another unrelated domain, then you must use :none.

The http_only: option

The http_only: option is used to set the HttpOnly attribute on the cookie, which tells the browser to only use the cookie on HTTP(S) requests it makes, and to not expose it to client-side JavaScript via document.cookie.

It defaults to true, and that is highly recommended… in fact if you set this to false, you should probably reconsider!

Conclusion

So, if you’re using :all, be sure to keep the potential security risk of “CNAME cloaking” in mind. Consider using tld_length: to limit the scope of the session cookie and domains to which it will be sent.

Ensure your session store cookie is set to Secure in production, and HttpOnly in all environments. Ideally, leave SameSite to the default. And if Domain is set, check that the domain, and all its possible subdomains, should be allowed to access that cookie.