In a Laravel app, why does $request->cookie() return null for a cookie set by front-end JavaScript, and how do you fix it?
answer
- the web group's first middleware
- decrypt fails, value becomes null
- encryptCookies(except: [...])
- exact cookie names, no wildcards
- outgoing cookies are encrypted too
basics
~20 sEncryptCookies in the web group decrypts every incoming cookie and sets any value that fails decryption to null, so a plain cookie written by JavaScript reads as null. List it in $middleware->encryptCookies(except: ['voter_theme']) in bootstrap/app.php.
solid answer
~40 s`EncryptCookies` is the first entry of the `web` group. On the way in it decrypts every cookie with the application key and checks a prefix that binds the value to the cookie's name; when decryption throws a `DecryptException`, it sets that cookie to `null`. A cookie such as `voter_theme=dark` written by `document.cookie` was never encrypted, so it fails and `$request->cookie('voter_theme')` returns `null`. The fix is `$middleware->encryptCookies(except: ['voter_theme'])` in `bootstrap/app.php`; names are matched exactly. On the way out the same middleware encrypts cookies the app sets, so an excluded cookie is also sent in plain text and JavaScript can read it. Never exclude a cookie the server trusts for security decisions, since the client can then change it freely. Routes in the `api` group do not run `EncryptCookies` at all.
code
php · 8 lines<?php
use Illuminate\Foundation\Configuration\Middleware;
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
$middleware->encryptCookies(except: ['voter_theme']);
})go deeper
Recall that Laravel encrypts its cookies and that a cookie written by JavaScript must be listed in encryptCookies(except:).
Explain the decrypt-or-null behaviour, the name-bound prefix check, and that exclusion also stops outgoing encryption.
Decide which cookies may be excluded based on trust, and plan for APP_KEY rotation so users are not silently logged out.
Set a rule that security-relevant state lives in encrypted cookies or the session, never in client-editable ones.
## What EncryptCookies does `Illuminate\Cookie\Middleware\EncryptCookies` is the first middleware in Laravel's `web` group. Its `handle()` method is one line: ```php return $this->encrypt($next($this->decrypt($request))); ``` So it works in both directions: - **Incoming**: every cookie on the request is decrypted with the application's encrypter, which uses `APP_KEY`. The decrypted value must also carry a prefix derived from the cookie's name; this stops an attacker copying a valid encrypted value from one cookie into another. - **Outgoing**: every cookie the response sets is encrypted before it leaves. Because values are encrypted and authenticated, users cannot read or tamper with cookies the application sets, such as the session ID cookie. ## Why a JavaScript cookie reads as null On a voting app, the front end remembers the chosen theme with `document.cookie = 'voter_theme=dark'`. The browser sends it back on the next request, but it is plain text. When `EncryptCookies` tries to decrypt it, the encrypter throws a `DecryptException`. The middleware catches that and **sets the cookie's value to `null`** rather than failing the request. By the time the controller calls `$request->cookie('voter_theme')`, the value is gone. Common symptoms: - A cookie is visible in the browser's developer tools but `null` in Laravel. - A cookie set by a third-party script on the same domain is always missing server-side. - A cookie written by another application on a shared parent domain cannot be read. ## The fix: exclude the cookie by name ```php ->withMiddleware(function (Middleware $middleware): void { $middleware->encryptCookies(except: [ 'voter_theme', ]); }) ``` - Names are compared **exactly**; there is no wildcard matching. - Excluded cookies are neither decrypted on the way in nor encrypted on the way out. That is what you want for a cookie JavaScript must read as well as write. - The call adds to a static list on the middleware, so several calls accumulate. ## What not to exclude Excluding a cookie turns it into ordinary client-controlled input: | Cookie | Safe to exclude? | Why | |---|---|---| | UI preference such as `voter_theme` | Yes | Nothing breaks if a user edits it | | A cookie read by an analytics script | Yes | The server does not trust it | | The session cookie | No | Its encryption protects the session ID | | A cookie holding a user ID or role | No | The user could change it to impersonate someone | If the server must trust a value, keep it encrypted, or keep it in the session instead of a cookie. ## Where the middleware does not run `EncryptCookies` is only in the `web` group. On routes in `routes/api.php`, cookies are not decrypted, so an encrypted cookie set by a web page arrives there as ciphertext. The reverse also holds: cookies set by an API route are not encrypted by this middleware. ## Debugging checklist 1. Is the route in the `web` group? If it is in `routes/api.php`, `EncryptCookies` is not involved at all. 2. Does the cookie appear in `$request->cookies->all()` as `null`? That is the decrypt-failure signature. 3. Was the cookie written by the server with `cookie()` or `Cookie::queue()`, or by the browser? Only server-written cookies are encrypted. 4. Is the cookie name listed exactly, with the same case, in `encryptCookies(except:)`? ## Related behaviour worth knowing - `Cookie::queue()` stores cookies that `AddQueuedCookiesToResponse` attaches to the response. That middleware runs **inside** `EncryptCookies`, so queued cookies are encrypted on the way out like any other. - Rotating `APP_KEY` makes existing encrypted cookies undecryptable. Laravel can decrypt with previous keys listed in `APP_PREVIOUS_KEYS`; without them, affected cookies simply read as `null`, and users are logged out because the session cookie is lost.
- Why doesn't Laravel throw an error when an incoming cookie fails to decrypt?Cookies are user-controlled, so a broken or foreign cookie is expected input, not a server fault. `EncryptCookies` catches the `DecryptException` and sets that cookie to `null`, which keeps one bad cookie from failing the whole request.
- Can an attacker copy a valid encrypted cookie value from one Laravel cookie into another cookie's slot?Not usefully. Laravel prefixes the value with a hash derived from the cookie's name before encrypting it, and on decryption it validates that prefix against the name. A value moved to a different cookie name fails the check and is discarded.
saying these in an interview costs you the question
- Blames the browser for dropping a cookie that Laravel set to null
- Excludes the session cookie from encryption to make it readable in JavaScript
- Stores a user's role in an excluded cookie and trusts it
- Expects wildcard patterns to work in encryptCookies(except:)
- Assumes API routes decrypt cookies like web routes