Secure, HttpOnly and SameSite: fix cookie flags
Short answer: session and login cookies should have Secure (only sent over HTTPS), HttpOnly (not readable by JavaScript, which limits damage from XSS) and SameSite=Lax or Strict (not sent on most cross-site requests, which helps against CSRF). Set them where the cookie is created, or add them at the web server as a fallback.
See what your site sends
curl -sI https://yourdomain.com | grep -i '^set-cookie'PHP sessions
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = "Lax"setcookie('name', $value, [
'expires' => time() + 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);WordPress
WordPress login cookies are already HttpOnly. Make sure the admin is HTTPS-only so they are also Secure:
define( 'FORCE_SSL_ADMIN', true );Cookies set by plugins are up to each plugin. If one is missing flags, check for an update or use the server fallback below.
nginx (as a reverse proxy)
proxy_cookie_flags ~ secure httponly samesite=lax;Apache
Header edit Set-Cookie "^((?!.*;\s*Secure).*)$" "$1; Secure"
Header edit Set-Cookie "^((?!.*;\s*HttpOnly).*)$" "$1; HttpOnly"
Header edit Set-Cookie "^((?!.*;\s*SameSite).*)$" "$1; SameSite=Lax"If the cookies come from a proxied backend (for example PHP-FPM behind ProxyPass) and nothing changes, use Header always edit instead.
Don’t break things
- Cookies that your own JavaScript must read (some consent banners, A/B tests) can’t be HttpOnly.
SameSite=Noneis needed for cookies used inside third-party iframes, and it requiresSecure.SameSite=Strictcan log users out when they arrive from a link in an email.Laxis the safer default.
Frequently asked questions
Do analytics cookies need these flags?
Secure, yes, on an HTTPS site. HttpOnly only if no script needs to read them, which is rarely the case for analytics.
Is SameSite=Lax enough against CSRF?
It blocks most cross-site form posts, but forms that change data should still use CSRF tokens.
Why does a scanner still report a missing flag after my fix?
Often a page cache or CDN is serving the old response, or a plugin sets its own cookie. Purge the cache and check the Set-Cookie lines again.