Previous Article
Geolocation and Google Maps Directions: A Working 2026 Guide
15 July 2018
A cookie is a small piece of data your server asks the browser to store and send back on subsequent requests. In PHP that is setcookie() — a function whose signature and security defaults have both changed significantly, and most tutorials still show the old form.
The positional signature is the one you will see everywhere:
// Legacy — cannot set SameSite setcookie($name, $value, $expires, $path, $domain, $secure, $httponly);
Since PHP 7.3 there is an array form, and it is the one to use — because the positional version has no way to set SameSite, which is now the single most important cookie attribute:
setcookie('preferred_theme', 'dark', [
'expires' => time() + 60 * 60 * 24 * 30, // 30 days
'path' => '/',
'domain' => '', // current host only — safer than a wildcard
'secure' => true, // HTTPS only
'httponly' => true, // unreachable from JavaScript
'samesite' => 'Lax',
]);
People historically worked around the gap by smuggling SameSite into the path parameter ('/; SameSite=Lax'). Do not — the array form exists precisely so you do not have to.
setcookie('user', 'Ada', ['path' => '/']);
echo $_COOKIE['user']; // Undefined array key "user"
setcookie() sends a Set-Cookie header. The browser stores it and returns it on the next request. $_COOKIE is populated from the incoming request, so it will not contain a cookie you set moments ago on this one.
If you need the value in the same request, set both:
setcookie('user', 'Ada', ['path' => '/']);
$_COOKIE['user'] = 'Ada'; // so the rest of this request can read it
Because it is a header, setcookie() must be called before any output — including whitespace before <?php. That is the cause of nearly every "headers already sent" error.
$theme = $_COOKIE['preferred_theme'] ?? 'light';
if (isset($_COOKIE['user'])) {
// Cookie values are user-controlled. Escape on output, validate before use.
echo 'Welcome back, ' . htmlspecialchars($_COOKIE['user'], ENT_QUOTES, 'UTF-8');
}
Treat every cookie value as untrusted input. A user can edit cookies freely in DevTools, so a cookie saying role=admin means nothing unless you signed it.
setcookie('user', '', [
'expires' => time() - 3600,
'path' => '/', // must match how it was set
'domain' => '', // must match how it was set
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
unset($_COOKIE['user']); // also clear it for the current request
The critical detail: path and domain must match the original exactly. A cookie set on /app is not deleted by an expiry on / — you end up with a "ghost" cookie you cannot remove and cannot see why. If deletion is not working, check those two values first.
| Value | Sent on cross-site requests? | Use for |
|---|---|---|
Strict | Never, not even following a link | Banking, admin panels |
Lax | Only on top-level GET navigation | Sessions — the sensible default |
None | Always — requires Secure | Genuine cross-site embedding |
SameSite is what makes CSRF hard. With Lax, another site cannot make an authenticated POST on your users' behalf, because the browser withholds the cookie.
Strict has a real usability cost worth knowing: a user following a link from an email to your site arrives logged out, because the cookie is not sent on that navigation. It appears on refresh, which is baffling. Use Lax for session cookies unless you have a specific reason.
SameSite=None without Secure is rejected outright by browsers — the cookie is silently dropped.
Makes the cookie invisible to document.cookie. If an attacker lands an XSS payload on your page, an HttpOnly session cookie cannot be read and exfiltrated. Set it on anything your JavaScript does not genuinely need.
Send only over HTTPS. Without it, a cookie leaks in plaintext on any accidental http:// request.
Underused and worth knowing. Browsers enforce rules based on the cookie's name:
// __Host- requires Secure, path=/, and NO domain attribute.
// A subdomain cannot overwrite it — this is the strongest binding available.
setcookie('__Host-session', $token, [
'expires' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
Without the prefix, a compromised or attacker-controlled subdomain can set a cookie that shadows yours on the parent domain — a "cookie tossing" attack. __Host- makes that impossible. __Secure- is a weaker variant that only requires the Secure flag.
If a cookie holds anything a user should not be able to change, sign it:
function signCookie(string $value, string $secret): string
{
$mac = hash_hmac('sha256', $value, $secret);
return $value . '.' . $mac;
}
function readSignedCookie(?string $raw, string $secret): ?string
{
if ($raw === null || !str_contains($raw, '.')) {
return null;
}
[$value, $mac] = explode('.', $raw, 2);
// hash_equals compares in constant time — a plain === leaks timing info.
if (!hash_equals(hash_hmac('sha256', $value, $secret), $mac)) {
return null;
}
return $value;
}
Signing proves the value came from you. It does not hide it — the value is still readable. For anything genuinely sensitive, keep the data server-side and store only an opaque session ID in the cookie.
setcookie() does this for you; setrawcookie() does not.For larger client-side data, use localStorage or IndexedDB — neither is sent to the server, and neither is a good place for anything sensitive, since both are readable by JavaScript.
Safari and Firefox have blocked third-party cookies by default for years, and Chrome has spent a long time restricting them. The practical consequence: a cookie set by a domain other than the one in the address bar is unreliable.
This affects cross-site tracking, some embedded widgets, and any SSO flow relying on an iframe reading a cookie from another origin. First-party cookies — the kind covered here — are unaffected. Sessions, preferences and CSRF tokens continue to work exactly as before.
Under GDPR and the ePrivacy Directive, cookies that are not strictly necessary require consent before being set. "Strictly necessary" is narrow: session cookies for a logged-in area, a shopping cart, a CSRF token. Analytics and advertising cookies are not, and setting them before the user agrees is the most common compliance failure.
// Only set analytics cookies once consent is recorded.
if (($_COOKIE['cookie_consent'] ?? '') === 'accepted') {
setcookie('_analytics_id', $id, [ /* … */ ]);
}
SameSite.$_COOKIE reflects the incoming request; a cookie you just set is not there yet.setcookie() before any output.Secure, HttpOnly, SameSite=Lax.__Host- for session cookies.Advertisement
Written by
Amit VermaFounder / Senior Software Engineer
Amit leads engineering at Being Idea. With 15+ years building scalable software products across global markets, he drives architecture decisions and engineering culture across every engagement.
More articles by AmitYou might also like
We ship software that scales. Let's work together.