If you want to develop a secure web application, you need to make sure your cookies are locked down tight.
Because web applications use the client-server model, they are stateless. So we use sessions to persist data from one request to the next. The most common method for identifying a session is to create a unique session ID. Obviously this ID is stored on the server, either directly on the filesystem or in a database. But how will it be stored on the client side? That's where cookies come in.
Clearly, it is of supreme importance that we make our cookies secure. So what defines a "secure" cookie? Like everything else in the world of security, this depends on context. There are no quick fixes or easy answers. To really be confident that your cookies are secure, you need to do your own research and base your approach on the specifics of your situation. That being said, the purpose of this post is to provide a decent starting point. I will focus on the following and describe how each can be implemented in Laravel:
- prefixes
- the
HttpOnly
attribute - the
SameSite
attribute - the
Secure
attribute
Why should we use prefixes?
Every cookie has a name. We can add prefixes to the names of our cookies to make them more secure. These prefixes tell the browser to reject the cookies if they do not meet certain requirements. The obvious limitation here is that this assumes that the user is using a browser that supports these prefixes.
There are two supported prefixes. Each begins with two underscores and ends with a hyphen:
__Secure-
and __Host-
.
The __Secure-
prefix, perhaps counterintuitively, is the less restrictive of the two.
Compliant browsers will reject a cookie with this prefix if it does not meet the following
requirements:
- It must be marked with the
Secure
attribute. - It must be sent from a secure origin.
The __Host-
prefix is more restrictive and is thus preferable. In addition to the
requirements of the __Secure-
prefix, it must also meet these:
- It must not include the
Domain
attribute. - It must have its
Path
attribute set to/
.
Always opt for the __Host-
prefix unless your application for some reason necessitates
the use of the Domain
attribute or setting the Path
attribute to something
other than the root. Proper use of prefixes helps prevent a man-in-the-middle (MITM) attacker from
overwriting cookie values.
Why should we use the HttpOnly attribute?
In case you're wondering, the name HttpOnly
does not mean "HTTP as opposed to HTTPS." Rather,
it means "HTTP as opposed to JavaScript." The purpose of this attribute is to prevent the cookie from
being accessed by JavaScript. If this attribute is set, the cookie can only be accessed by the server.
This helps prevent cross-site scripting (XSS) attacks.
Why should we use the SameSite attribute?
The SameSite
attribute controls whether a cookie is sent on cross-origin requests. There are
three possible values:
None
(least secure): the cookie is sent on cross-origin requests.-
Lax
(intermediate security): the cookie is sent on cross-origin requests only if:- the method is safe (
GET
orHEAD
) -
it is a top-level navigation (meaning the URL in the address bar changes),
as opposed to an AJAX request, a request from a
frame
element, etc.
- the method is safe (
Strict
(most secure): the cookie is not sent on any cross-origin requests.
Clearly, you should always use Strict
for your session cookies! Otherwise, you are leaving
yourself open to cross-site request forgery (CSRF).
Why should we use the Secure attribute?
The Secure
attribute ensures that the cookie is not sent unencrypted. It is only sent
over the HTTPS protocol. This helps to mitigate man-in-the-middle attacks.
Prefixing the session cookie in Laravel
To secure the session cookie, open up config/session.php
. The sections of interest to us
begin on line 118 (as of Laravel 8.57.0).
Let's make sure we understand what this code does:
-
It checks your
.env
file for aSESSION_COOKIE
value. If it finds one, it uses that, and does not proceed with the following steps. -
It determines the name of your application by checking your
.env
file for anAPP_NAME
value. If no value is found, it defaults tolaravel
. -
Using the application name determined in step 2, it constructs the cookie name using the
template
[app_name]_session
.
It may be tempting to modify this file directly. For example, you might want to just hard code the cookie name like this:
But I would recommend against this. If you develop locally using php artisan serve
,
it will work, because Firefox and Chrome make developers' lives easier by ignoring cookie
restrictions on http://localhost
. However, it is not future-proof. If one day you
switch to developing on a server on your network without HTTPS, then suddenly your cookies will
no longer work in development.
A more future-proof solution is to use environment variables as the framework developers intended.
In your .env
files, add a new line under the other session-related variables. That way
you can exclude the prefix in development and include it in local/production.
Adding the Secure attribute to the session cookie in Laravel
Scroll down until you find the following:
This checks your .env
file for an environment variable called
SESSION_SECURE_COOKIE
value. By default, it doesn't exist. So add it:
If developing on a server without HTTPS, you can set it to false in your development .env
file.
Adding the HttpOnly attribute to the session cookie in Laravel
Keep scrolling until you get to this section:
Here we don't need to worry about .env
files, since there is no reason we would need JavaScript
to access the session cookie, regardless of environment. So simply hard code it to true
.
Adding the SameSite attribute to the session cookie in Laravel
Finally, scroll down to the bottom of the file.
Again, no need to worry about .env
files here. Hard code it to 'strict'
.
And just like that, your session cookie is now secure. Don't forget to run
php artisan config:clear
so that your changes take effect. Also double check that for
every change made to your local/development environment file, you also made the corresponding change
to the production file!
Understanding the CSRF cookie in Laravel
The CSRF token cookie is a little different. In my applications, I tend to remove this cookie
altogether. I do this because I like it when vulnerability scans find nothing wrong, but with
this cookie you cannot prefix it or add the HttpOnly
attribute without totally defeating
its purpose.
This cookie exists only because some JavaScript libraries/frameworks use it to make AJAX requests. If you
prefix it, you have changed the name, so the frameworks will probably miss it. If you add the
HttpOnly
attribute, then it becomes inaccessible via JavaScript. So you have to either accept
the potential vulnerabilities or remove it altogether. I opt to remove it.
According to the official Laravel documentation:
Laravel stores the current CSRF token in an encrypted XSRF-TOKEN
cookie that is
included with each response generated by the framework. You can use the cookie value to set the
X-XSRF-TOKEN
request header.
This cookie is primarily sent as a developer convenience since some JavaScript frameworks and
libraries, like Angular and Axios, automatically place its value in the X-XSRF-TOKEN
header on same-origin requests.
You should decide for yourself whether to remove this cookie. Are you securing an existing application that relies heavily on a JavaScript framework/library that uses this cookie? If so, then it might be preferable for you to keep this cookie, at least until you have time to manually add the CSRF token to all same-origin requests .
Removing the CSRF cookie in Laravel
Removing this cookie is incredibly easy. Open up app/Http/Middleware/VerifyCsrfToken.php
.
By default, it looks something like this:
The parent class contains a protected
property, $addHttpCookie
, which we need
to override and set to false
.
That's all there is to it.