HTTP Cookies in PHP
debt(d5/e3/b3/t7)
Closest to 'specialist tool catches it' (d5), because the detection_hints list semgrep and phpstan as the tools that can catch missing HttpOnly/Secure flags via the pattern `setcookie( without 'httponly'=>true or 'secure'=>true`. These are not default linters but specialist SAST tools, placing this squarely at d5.
Closest to 'simple parameterised fix' (e3), because the quick_fix is a single replacement of positional args with the options array `['secure'=>true,'httponly'=>true,'samesite'=>'Lax']`. Each setcookie() call is a localised one-call swap, but there may be multiple call sites to update across the codebase, making it slightly more than a one-liner but well within e3 territory.
Closest to 'localised tax' (b3), because cookie security flags are set at the call site of setcookie() or in session configuration. The applies_to scope is any PHP web context but the actual fix and ongoing maintenance burden is confined to the specific cookie-setting code paths. The rest of the codebase is largely unaffected.
Closest to 'serious trap' (t7), because there are multiple distinct gotchas here that contradict reasonable developer assumptions: the same-request $_COOKIE read failure (the canonical misconception), SameSite=None silently rejected by browsers without Secure=true, and headers-already-sent causing the cookie to silently not be sent. These contradictions are well-documented but counterintuitive, especially the round-trip requirement which contradicts how setting a variable normally works.
Also Known As
TL;DR
Explanation
Cookies are the primary mechanism for persisting state between HTTP requests. PHP sets them with setcookie(name, value, options) — the options array accepts expires (Unix timestamp), path, domain, secure (HTTPS-only), httponly (JS cannot read), and samesite (Strict | Lax | None). The Set-Cookie header is sent as part of the HTTP response; the browser echoes it on subsequent requests via the Cookie header, accessible in PHP as $_COOKIE. Because setcookie() must emit a header, it must be called before any output — even a single space or BOM before <?php will cause a 'headers already sent' error. Cookie values are URL-encoded by default; use setrawcookie() to skip encoding. Secure should always be true in production. HttpOnly blocks JavaScript access, mitigating XSS cookie theft. SameSite=Lax is the browser default and blocks cross-site POST submissions; SameSite=Strict blocks all cross-site sends; SameSite=None requires Secure=true and is needed for embeds or third-party contexts. To delete a cookie, call setcookie() with an expiry in the past.
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Calling setcookie() after any output — even whitespace before the opening <?php tag causes 'headers already sent' and the cookie is never sent.
- Omitting HttpOnly — JavaScript can read the cookie, making XSS attacks trivially able to steal session tokens.
- Using SameSite=None without Secure=true — modern browsers silently reject the cookie entirely.
- Reading $_COOKIE immediately after setcookie() on the same request — the value is not present until the next request.
Code Examples
// Missing security flags — vulnerable to XSS theft and network interception:
setcookie('session', $token, time() + 3600, '/');
// No Secure, no HttpOnly, no SameSite
// Secure cookie with all flags:
setcookie('session', $token, [
'expires' => time() + 3600,
'path' => '/',
'secure' => true, // HTTPS only
'httponly' => true, // JS cannot read
'samesite' => 'Lax', // CSRF protection
]);