Secure Cookies

Yo Yogesh.M Updated 17 Oct 2026
6 min read ·Lesson 74 of 109
In this post

Why Cookie Security Matters In simple terms: Cookies are small pieces of data the browser stores and sends back with every request to your site. They carry session IDs, "remember me" tokens, and prefe

In simple terms: Cookies are small pieces of data the browser stores and sends back with every request to your site. They carry session IDs, "remember me" tokens, and preferences. Because the browser stores them and the user can see and edit them, cookies must be protected in two ways: against theft (someone else obtaining them) and against tampering (the user changing their content).

Technically: Cookie protection relies on attributes set by the server (Secure, HttpOnly, SameSite, Domain, Path, expiry, name prefixes) that tell the browser when to send a cookie and who may read it, plus cryptographic integrity (HMAC signatures) or confidentiality (encryption) for cookie values that the server must be able to trust. The basics of setcookie() are covered in $_COOKIE, and practical cookie patterns in PHP Cookies; this lesson focuses on hardening.


AttributeSettingProtects Against
Secure'secure' => trueSending the cookie over unencrypted HTTP, where it can be intercepted
HttpOnly'httponly' => trueJavaScript (and therefore XSS) reading the cookie
SameSite'samesite' => 'Lax' or 'Strict'Sending the cookie with cross-site requests (CSRF)
DomainLeave unsetSharing the cookie with all subdomains
Path'/' (or narrower)Sending the cookie to unrelated paths (not a security boundary on its own)
Expires / Max-AgeAs short as practicalLong-lived tokens being stolen and reused
__Host- name prefix__Host-sessionOverwriting by subdomains; requires Secure, Path=/, no Domain
__Secure- name prefix__Secure-prefBeing set over plain HTTP

A Hardened setcookie() Call

<?php
setcookie('__Host-theme', 'dark', [
    'expires' => time() + 30 * 86400,
    'path' => '/',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

The Resulting Response Header

Set-Cookie: __Host-theme=dark; expires=Sun, 15 Nov 2026 10:00:00 GMT; Max-Age=2592000; path=/; secure; HttpOnly; SameSite=Lax

Check the actual headers in your browser's developer tools (Network tab → response headers, and Application/Storage → Cookies) — a missing attribute is easy to spot there.

When HttpOnly Must Be Off

Only cookies that JavaScript genuinely needs to read (for example, a UI preference read by a script) should omit HttpOnly. Session IDs, login tokens, and anything security-relevant must always be HttpOnly.


Users can change cookies freely with browser tools or by crafting requests. A cookie like role=admin or user_id=7 is an invitation to impersonation. Cookies may contain only:

Safe in a CookieNever in a Cookie
Random identifiers that are looked up on the server (session ID, remember-me selector)User IDs, roles, prices, discounts, or permissions used without verification
Harmless preferences (theme, language), validated when readPasswords, password hashes, API keys
Values protected with a signature (integrity) or encryption (confidentiality)Personal data such as addresses or phone numbers in plain text

Signed Cookies: Detecting Tampering

When the server must trust a value it stored in a cookie, it adds an HMAC signature — a keyed hash that only the server can compute:

<?php
final class SignedCookie
{
    public function __construct(#[\SensitiveParameter] private string $key) {}

    public function sign(string $value): string
    {
        return $value . '.' . $this->mac($value);
    }

    public function verify(?string $cookie): ?string
    {
        if ($cookie === null) {
            return null;
        }

        $separator = strrpos($cookie, '.');
        if ($separator === false) {
            return null;
        }

        $value = substr($cookie, 0, $separator);
        $mac = substr($cookie, $separator + 1);

        return hash_equals($this->mac($value), $mac) ? $value : null;
    }

    private function mac(string $value): string
    {
        $raw = hash_hmac('sha256', $value, $this->key, true);
        return rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
    }
}

$cookies = new SignedCookie('demo-only-key: use 32+ random bytes from an environment variable');

$signed = $cookies->sign('dark');

var_dump($cookies->verify($signed));
var_dump($cookies->verify('light' . substr($signed, strlen('dark'))));   // value changed, old signature kept
var_dump($cookies->verify('dark.forged-signature'));
var_dump($cookies->verify('no-signature'));
var_dump($cookies->verify(null));

Output

string(4) "dark"
NULL
NULL
NULL
NULL

How It Works

  1. sign() appends an HMAC-SHA256 signature (base64url-encoded, so it is safe in cookies) to the value.
  2. verify() splits the cookie at the last dot, recomputes the signature, and compares with hash_equals() (constant time).
  3. Any change to the value or the signature makes verification fail, and verify() returns null — the application then ignores the cookie.
  4. The key is marked #[\SensitiveParameter], so it is hidden in stack traces.

Generate the key once (for example, base64_encode(random_bytes(32))), store it in an environment variable, and never commit it. Rotating the key invalidates all existing signed cookies.

Note: A signature prevents tampering, not reading: the value dark remains visible. For data that must stay secret, encrypt it (for example, with sodium_crypto_secretbox() — see Encryption with Sodium) — or better, store the data on the server and keep only an identifier in the cookie.


Deleting Cookies Reliably

A cookie is deleted by sending it again with an expiry date in the past and the same name, path, and domain it was set with:

<?php
function deleteCookie(string $name): void
{
    setcookie($name, '', [
        'expires' => time() - 3600,
        'path' => '/',
        'secure' => true,
        'httponly' => true,
        'samesite' => 'Lax',
    ]);
    unset($_COOKIE[$name]);
}

If the path or domain differs, the browser treats it as a different cookie and the original remains. unset($_COOKIE[...]) also removes it from the current request's data.


Further Considerations

TopicAdvice
SizeBrowsers limit cookies to about 4 KB each and a limited number per site; every cookie is sent with every request, so keep them small
LifetimeSession cookies (no expiry) for logins; long expiry only for low-risk preferences and rotated remember-me tokens
Third-party embedsCookies needed inside cross-site iframes require SameSite=None; Secure; browsers increasingly restrict such cookies (partitioned "CHIPS" cookies are the modern approach)
ConsentNon-essential cookies (analytics, advertising) may require user consent depending on applicable law
HSTSStrict-Transport-Security prevents browsers from ever using HTTP for your site, protecting Secure cookies from downgrade attacks

Common Mistakes

Missing Secure or HttpOnly on Session Cookies

Effect: Cookies can be intercepted or read by injected scripts.

Fix: Set both on every security-relevant cookie (and in php.ini for sessions).

Effect: Users escalate privileges or change prices.

Fix: Store identifiers only, look up data server-side, and sign values that must be trusted.

Comparing Signatures With ===

Effect: Timing differences can leak information.

Fix: Use hash_equals().

Setting Domain Unnecessarily

Effect: All subdomains receive (and can overwrite) the cookie.

Fix: Leave Domain unset; consider the __Host- prefix.

Deleting With Different Parameters

Effect: The cookie survives.

Fix: Use the same name, path, and domain as when it was set.


Best Practices

  • Set Secure, HttpOnly, and SameSite on every cookie that matters; prefer __Host- names.
  • Store only random identifiers or harmless preferences in cookies.
  • Sign cookies whose values must be trusted, and encrypt those whose contents must stay private.
  • Keep cookies small and short-lived, and delete them with matching parameters.
  • Serve the whole site over HTTPS with HSTS.

Official reference: https://www.php.net/manual/en/function.setcookie.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Home PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else Slicing Strings User-defined Functions Slicing Strings PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators
Ask about this post
AI Ask about this post

Ask questions about Secure Cookies and get answers drawn from it.

Signed-in readers only.