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.
Cookie Domain
s
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 Rails cookie_store
domain:
option
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.com
or 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?
Use tld_length:
with domain: :all
of the cookie store
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.
A note on secure cookie options
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.