PHP Cookies

Su Subhalakshmi Chandran Updated 17 Oct 2026
7 min read ·Lesson 61 of 248
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 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 User-defined Functions Slicing Strings Function Arguments 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 AJAX PHP AJAX Database AJAX XML AJAX Live Search AJAX Poll AJAX with JSON Security Introduction Password Hashing SQL Injection Prevention XSS Prevention Garbage Collection Project: Simple Blog PHP What is OOP PHP Classes/Objects The $this Keyword Access Array Items Update Array Items Add Array Items Remove Array Items Sorting Arrays Multidimensional Arrays Array Destructuring Array Spread Operator Search in Arrays Merge Arrays PHP Abstract Classes PHP Interfaces Polymorphism PHP Traits PHP Static Methods PHP Static Properties Late Static Binding Magic Methods Object Cloning MySQL Database MySQL Connect MySQL Create DB MySQL Create Table MySQL Insert Data MySQL Get Last ID MySQL Insert Multiple MySQL Prepared MySQL Select Data MySQL Where Debugging PHP Project: Image Upload Gallery $_SESSION $_ENV PHP RegEx RegEx Patterns Heredoc and Nowdoc RegEx Modifiers preg_match preg_replace preg_split PHP RegEx Functions Nullable Types Mixed and Never Types Nullsafe Operator PHP Attributes PHP Fibers Readonly Classes Property Hooks (PHP 8.4) Asymmetric Visibility (PHP 8.4) Reflection API SPL Data Structures Composer PHP Form Handling PDO Transactions PDO Error Handling PHP XML Parsers PHP SimpleXML Parser PHP SimpleXML - Get PHP XML Expat Parser PHP DOM Parser PHP XMLReader PHP XMLWriter AJAX Intro PHP Class Constants Encryption with Sodium Recursive Functions Variable Functions Anonymous Functions Closures Arrow Functions First-class Callable Syntax PHP Arrays Indexed Arrays Associative Arrays Create Arrays Variables Scope Static Variables Variable Variables PHP Echo / Print PHP printf / sprintf PHP Data Types PHP Booleans PHP NULL PHP Strings String Functions array_map / array_filter / array_reduce Array Functions PHP Superglobals $GLOBALS What Is `$_SERVER` ? $_REQUEST $_POST $_GET $_FILES $_COOKIE PHP Type Juggling MySQL Order By MySQL Delete Data MySQL Update Data MySQL Limit Data MySQL Joins with PHP Pagination with PHP PDO Introduction PDO Connect PDO Prepared Statements PDO Fetch Modes PSR-4 Autoloading Installing Extensions (PECL / PIE) OPcache & Performance PHP-FPM Project: Contact Form with Email Project: Login & Registration System Project: CRUD App with MySQL Project: Shopping Cart with Sessions Project: REST API with PHP DateTime Class Timezones PHP Include Include vs Require PHP File Handling PHP File Open/Read PHP File Create/Write File Delete / Rename / Copy PHP Directories PHP CSV Files
Ask about this post
AI Ask about this post

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

Signed-in readers only.