johnny tisdale
moving the needle forward

Securing cookies in Laravel.

August 29, 2021

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.

Most websites use cookies as the only identifiers for user sessions, because other methods of identifying web users have limitations and vulnerabilities. If a website uses cookies as session identifiers, attackers can impersonate users' requests by stealing a full set of victims' cookies. From the web server's point of view, a request from an attacker then has the same authentication as the victim's requests; thus the request is performed on behalf of the victim's session.

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 or HEAD)
    • 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.
  • 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:

  1. It checks your .env file for a SESSION_COOKIE value. If it finds one, it uses that, and does not proceed with the following steps.
  2. It determines the name of your application by checking your .env file for an APP_NAME value. If no value is found, it defaults to laravel.
  3. 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.