PHP Cookies

Su Subhalakshmi Chandran Updated 17 Oct 2026
7 min read ·Lesson 62 of 109
In this post

The $_COOKIE lesson covered the basics: setting cookies with setcookie() , reading them from $_COOKIE , deleting them, and protecting them with security options and signatures. This lesson focuses on

The $_COOKIE lesson covered the basics: setting cookies with setcookie(), reading them from $_COOKIE, deleting them, and protecting them with security options and signatures. This lesson focuses on how cookies are used in real applications: scope (which requests receive a cookie), SameSite behavior, cookie prefixes, consent preferences, and a secure "Remember me" login implementation.


A browser sends a cookie only with requests that match its domain and path:

AttributeSettingCookie Is Sent To
domainNot set (recommended)Only the exact host that set it, e.g. gocourse.in
domaingocourse.ingocourse.in and all subdomains (app.gocourse.in, blog.gocourse.in)
path/Every path on the host
path/admin/admin and paths below it, such as /admin/users
<?php
// Only for this host, all paths
setcookie('lang', 'ta', ['path' => '/', 'secure' => true, 'samesite' => 'Lax']);

// Shared with all subdomains (use only when needed)
setcookie('region', 'south', ['domain' => 'gocourse.in', 'path' => '/', 'secure' => true, 'samesite' => 'Lax']);

Why Narrow Scope Is Safer

  • A cookie shared with all subdomains is also sent to less-trusted subdomains (for example, a marketing site hosted by a third party), which could read or overwrite it.
  • Leaving domain unset makes the cookie host-only, which is the safest default.
  • path is not a security boundary: pages on other paths of the same host can still read cookies through other means. Use it to reduce unnecessary traffic, not to protect secrets.

SameSite in Practice

The SameSite attribute decides whether the browser sends a cookie with requests that start on another website:

Request Starting on Another SiteStrictLax (default in modern browsers)None
Clicking a link to your siteNot sentSentSent
A form on another site POSTing to yoursNot sentNot sentSent
Your page embedded in an <iframe>, or images/AJAX from other sitesNot sentNot sentSent

Choosing a Value

  • Lax — the right choice for most cookies, including login sessions: users arriving from a link (for example, from an email or a search engine) stay logged in, while cross-site form submissions do not carry the cookie, which blocks most CSRF attacks.
  • Strict — for highly sensitive actions (such as a banking session); users following a link from another site appear logged out on the first page load.
  • None — only for genuinely cross-site use, such as an embedded widget or a payment provider's iframe. It requires secure: true.

SameSite reduces CSRF risk but does not replace CSRF tokens, because subdomains and older browsers can still be sources of cross-site requests.


Browsers enforce extra rules for cookies whose names start with special prefixes:

PrefixBrowser RequirementBenefit
__Secure-Must be set with secure over HTTPSCannot be set or overwritten by plain HTTP pages
__Host-secure, path=/, and no domainLocked to the exact host; subdomains cannot set or overwrite it
<?php
setcookie('__Host-session_hint', 'abc123', [
    'path' => '/',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

If the requirements are not met, the browser rejects the cookie. __Host- is a strong default for important cookies, such as session and remember-me cookies.


Non-essential cookies (analytics, advertising) may require the user's consent, depending on the laws that apply to your site and visitors. A common pattern stores the user's choices in a small, readable cookie:

<?php
function readConsent(array $cookies): array
{
    $defaults = ['necessary' => true, 'analytics' => false, 'marketing' => false];

    $decoded = json_decode($cookies['consent'] ?? '', true);
    if (!is_array($decoded)) {
        return $defaults;
    }

    foreach (['analytics', 'marketing'] as $category) {
        $defaults[$category] = ($decoded[$category] ?? false) === true;
    }

    return $defaults;
}

var_export(readConsent([]));
echo "\n";
var_export(readConsent(['consent' => '{"analytics":true,"marketing":"yes"}']));
echo "\n";

Output

array (
  'necessary' => true,
  'analytics' => false,
  'marketing' => false,
)
array (
  'necessary' => true,
  'analytics' => true,
  'marketing' => false,
)

How It Works

  • Without a consent cookie, only necessary cookies are allowed.
  • Only the exact value true counts as consent; anything else (here, the string "yes") is treated as "no", because cookie values can be modified.
  • The function receives the cookie array as a parameter (readConsent($_COOKIE) in real use), which makes it testable.

The template then includes analytics scripts only when $consent['analytics'] is true. Setting the consent cookie is done when the user clicks a button in the consent banner, usually with an expiry of several months.


Practical Example: A Secure "Remember Me" Login

A session normally ends when the browser is closed or the session expires. "Remember me" keeps users logged in for weeks using a long-lived cookie. A naive implementation — storing the user ID in a cookie — lets anyone log in as anyone by editing the cookie. A secure design uses a random token that is checked against the database.

The Design

PartStored InPurpose
Selector (random, not secret)Cookie and databaseFinds the token record quickly
Validator (random, secret)Cookie onlyProves possession of the token
Hash of the validatorDatabase onlyLets the server verify without storing the secret
User ID and expiryDatabaseWhich user, and until when

The cookie value is selector:validator. If the database leaks, attackers get only hashes, which cannot be turned back into valid cookies.

Creating a Token

<?php
function createRememberToken(int $userId, DateTimeImmutable $now): array
{
    $selector = bin2hex(random_bytes(12));
    $validator = bin2hex(random_bytes(32));

    $record = [
        'selector' => $selector,
        'validator_hash' => hash('sha256', $validator),
        'user_id' => $userId,
        'expires_at' => $now->modify('+30 days')->getTimestamp(),
    ];

    return ["$selector:$validator", $record];
}

In a real application, $record is inserted into a remember_tokens table, and the cookie is set:

setcookie('__Host-remember', $cookieValue, [
    'expires' => time() + 30 * 86400,
    'path' => '/',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

Verifying a Token

<?php
function verifyRememberToken(string $cookieValue, array $tokensBySelector, DateTimeImmutable $now): ?int
{
    $parts = explode(':', $cookieValue, 2);
    if (count($parts) !== 2) {
        return null;
    }

    [$selector, $validator] = $parts;
    $record = $tokensBySelector[$selector] ?? null;

    if ($record === null || $record['expires_at'] < $now->getTimestamp()) {
        return null;
    }

    return hash_equals($record['validator_hash'], hash('sha256', $validator)) ? $record['user_id'] : null;
}

$now = new DateTimeImmutable('2026-10-15 10:00');

[$cookieValue, $record] = createRememberToken(7, $now);
$table = [$record['selector'] => $record];   // simulates the database table

[$selector] = explode(':', $cookieValue);

var_dump(verifyRememberToken($cookieValue, $table, $now));
var_dump(verifyRememberToken($selector . ':' . str_repeat('0', 64), $table, $now));
var_dump(verifyRememberToken($cookieValue, $table, $now->modify('+31 days')));
var_dump(verifyRememberToken('garbage', $table, $now));

Output

int(7)
NULL
NULL
NULL

Step-by-Step Explanation

  1. A valid cookie returns the user's ID (7).
  2. A cookie with the right selector but a forged validator fails: its hash does not match the stored hash.
  3. After 30 days, the token has expired.
  4. Malformed cookie values are rejected before any lookup.

Using It Safely

  • When a user is logged in through a remember-me token, issue a new token (and delete the old one) — this "rotation" limits the damage if a cookie is stolen.
  • Delete all of a user's tokens when they change their password or click "log out of all devices".
  • Require the password again for sensitive actions (changing the email address, payments), even for remembered users.
  • Call session_regenerate_id(true) when the remember-me login creates a new session (see PHP Sessions).

Common Mistakes

Sharing Cookies With All Subdomains Unnecessarily

Effect: Other subdomains can read or overwrite them.

Fix: Leave domain unset, or use the __Host- prefix.

Using SameSite=None Without Need

Effect: Cookies are sent with cross-site requests, increasing CSRF risk.

Fix: Use Lax unless cross-site use is required.

Storing User IDs or Roles in Cookies

Effect: Users can change them to impersonate others.

Fix: Use random tokens verified on the server.

Storing Remember-Me Tokens in Plain Text

Effect: A database leak gives attackers working login cookies.

Fix: Store only a hash of the secret validator.

Effect: Legal and trust problems.

Fix: Load non-essential scripts only after consent, and store the user's choice.


Best Practices

  • Keep cookies host-only and small; use __Host- for important cookies.
  • Use SameSite=Lax, secure, and httponly by default.
  • Store only random identifiers or harmless preferences in cookies, and validate them when reading.
  • Implement "Remember me" with a selector, a hashed validator, expiry, and rotation.
  • Respect consent for non-essential cookies.

Official reference: https://www.php.net/manual/en/features.cookies.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 PHP Cookies and get answers drawn from it.

Signed-in readers only.