
September 09, 2026
13 min read
By Kokil Thapa | Last reviewed: September 2026
Session hijacking still starts with a cookie the browser sends on every request. If that cookie lacks the right flags, a single XSS flaw or cross-site form can hand an attacker a live login. Secure session cookies with SameSite and HttpOnly are the baseline fix: they limit JavaScript access, block most cross-site sends, and (with Secure) refuse plain HTTP. This guide covers Laravel 13, raw PHP 8.5, nginx/Apache edge cases, and the production mistakes I see on Nepal-hosted Laravel and WordPress deployments.
HttpOnly, Secure (on HTTPS), and SameSite=Lax or Strict. HttpOnly blocks JavaScript reads; SameSite limits cross-site requests; Secure sends the cookie only over TLS. Configure these in Laravel's config/session.php or PHP's session.cookie_* ini settings.What are secure session cookies with SameSite and HttpOnly?
A session cookie stores the server-side session ID. The browser attaches it automatically to matching requests. Without protection, any script on your page can read it. A malicious third-party site can also trigger sends in some browser versions.
Three attributes harden that cookie:
- HttpOnly — JavaScript cannot read or write the cookie via
document.cookie. - Secure — The browser sends the cookie only over HTTPS.
- SameSite — Controls whether the cookie goes out on cross-site navigation and embedded requests.
HttpOnly does not stop XSS from performing actions as the user. It stops the attacker from exfiltrating the raw session ID. SameSite reduces CSRF risk by withholding the cookie on many cross-origin POST and iframe loads. Together they form the minimum bar for production authentication systems.
How do you configure HttpOnly and SameSite in Laravel 13?
Laravel 13 centralises cookie flags in config/session.php. The framework reads these values when it issues the session cookie through Symfony's HttpFoundation component. Set them once and every login, logout, and page view inherits the same policy.
Recommended Laravel session.php settings
<?php
// config/session.php (Laravel 13.x)
return [
'driver' => env('SESSION_DRIVER', 'database'),
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
'encrypt' => env('SESSION_ENCRYPT', true),
'cookie' => env('SESSION_COOKIE', Str::slug(env('APP_NAME', 'laravel')).'-session'),
'path' => env('SESSION_PATH', '/'),
'domain' => env('SESSION_DOMAIN'),
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => env('SESSION_HTTP_ONLY', true),
'same_site' => env('SESSION_SAME_SITE', 'lax'),
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
];
Match your .env to the environment:
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=true
SESSION_SECURE_COOKIE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=lax
APP_URL=https://example.com
On local HTTP, set SESSION_SECURE_COOKIE=false temporarily. Never ship that to production. I've debugged "login works locally, fails on server" dozens of times. The cause was almost always Secure=true behind HTTP or a mis-set SESSION_DOMAIN.
For multi-server Laravel session setups, use Redis or database drivers. Cookie flags still apply at the edge. Each app node must share the same APP_KEY when SESSION_ENCRYPT=true.
Sanctum and SPA cookies
API-first apps using Sanctum for SPA auth need the same flags on the laravel_session and XSRF-TOKEN cookies. Sanctum reads config/session.php for the session cookie. The CSRF cookie is separate. Keep HttpOnly on the session cookie only. The XSRF token cookie must stay readable by JavaScript for Axios/fetch interceptors.
Never set HttpOnly on the XSRF-TOKEN cookie. That breaks SPA CSRF handling. Do set Secure and SameSite on both.
What does each SameSite value do for session security?
SameSite tells the browser when to include a cookie on cross-site requests. Modern Chrome, Firefox, and Safari default unset cookies to Lax. Explicit configuration beats relying on browser defaults across older clients.
| SameSite value | Top-level GET navigation | Cross-site POST / iframe | Typical use |
|---|---|---|---|
Strict | Cookie withheld until same-site visit | Cookie withheld | Admin panels, banking, high-risk portals |
Lax | Cookie sent (link from email, bookmark) | Cookie withheld on POST | Default for most Laravel apps and eCommerce |
None | Cookie sent cross-site | Cookie sent | Embedded widgets; requires Secure |
Lax is the sweet spot for sites like client portals with document uploads and payments. Users arriving from Google or an email link stay logged in. Cross-site POST forgeries do not carry the session cookie.
Strict logs users out when they open your site from an external link. That surprises non-technical clients. Reserve it for admin-only zones behind a separate subdomain and cookie name.
None is rare in Laravel monoliths. You need it only when a third-party iframe or cross-origin API must send your session cookie. It always pairs with Secure. Browsers reject SameSite=None without Secure.
How do you set secure session cookies in plain PHP 8.5?
Legacy apps and custom microservices outside Laravel still rely on PHP's native session handler. PHP 8.5 exposes the same ini keys. Set them in php.ini, a pool-specific FPM file, or at runtime before session_start().
php.ini or FPM pool snippet
; /etc/php/8.5/fpm/pool.d/www.conf or php.ini
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax
session.use_strict_mode = 1
session.use_only_cookies = 1
session.cookie_lifetime = 0
session.gc_maxlifetime = 7200
Runtime configuration before session_start()
<?php
declare(strict_types=1);
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1');
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.use_strict_mode', '1');
session_name('APPSESSID');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '.example.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
session.use_strict_mode rejects session IDs the server never created. That closes fixation attacks where an attacker seeds a known ID. Pair it with regeneration on login:
session_regenerate_id(true);
On shared hosting common in Nepal, you may lack full php.ini access. A .user.ini or .htaccess php_value directive can set the same keys. Confirm with phpinfo() in a staging slot first. Wrong FPM pool edits affect every vhost on that pool.
When should the Secure flag be used on session cookies?
Always enable Secure in production when your site is served over HTTPS. Let's Encrypt makes TLS free. There is no good reason to accept session cookies on port 80 in 2026.
The Secure flag is independent of HttpOnly and SameSite. All three appear on the same Set-Cookie header. A typical production header looks like this:
Set-Cookie: laravel_session=eyJpdiI6...; expires=...; Max-Age=7200;
path=/; domain=.example.com; secure; httponly; samesite=lax
Watch these edge cases behind reverse proxies:
- TLS termination at nginx — Laravel must trust
X-Forwarded-Proto: httpsor it generates HTTP URLs and may mis-detect secure context. SetTrustProxiesmiddleware correctly. - Mixed content during migration — During HTTP→HTTPS cutover, run a hard redirect before enabling Secure cookies site-wide.
- Staging on HTTP — Use a separate
.env.stagingwithSESSION_SECURE_COOKIE=false. Document the difference so nobody copies staging env to production. - Subdomain cookies —
SESSION_DOMAIN=.example.comshares sessions acrossappandwww. A typo likeexample.com.silently breaks logins.
For infrastructure work beyond app config, see Linux system administration for TLS and proxy headers. Cookie flags mean nothing if traffic still hits PHP over plain HTTP on the origin.
How do you test and verify session cookie attributes?
Do not guess from config files alone. Inspect the live Set-Cookie response header after deploy. Browser DevTools → Network → first document request → Response Headers shows the truth.
curl one-liner
curl -sI https://example.com/login \
| grep -i set-cookie
Expect HttpOnly, Secure, and SameSite=Lax (or your chosen value) on the session cookie. If flags are missing, clear config cache:
php artisan config:clear
php artisan config:cache
Stale cached config is a common post-deploy surprise on production servers using config:cache in CI.
Checklist for QA
- Log in on HTTPS. Confirm session cookie flags in DevTools Application tab.
- Attempt
document.cookiein the console. Session name must not appear when HttpOnly is on. - Submit a cross-origin POST form from a local HTML file. With SameSite=Lax, the action should fail auth unless CSRF token is present.
- Log out. Confirm the cookie expires or is invalidated server-side.
- Run strong password generation for test accounts. Never reuse production credentials in staging.
Automate header checks in CI where possible. A smoke test that asserts cookie attributes catches regressions before they reach clients. Pair this with guidance from OWASP Top 10 practices for Laravel and pre-release security testing.
What are common mistakes that break secure session cookies?
Misconfiguration often looks like a bug in the framework. It is not. These patterns show up repeatedly on client projects and sister-site deploy pipelines I maintain.
HttpOnly on the wrong cookie
Setting HttpOnly globally via a middleware that wraps every outgoing cookie will break SPA CSRF flows. Scope HttpOnly to the session cookie only.
SameSite=None without Secure
Browsers silently drop the cookie. OAuth return flows and embedded dashboards fail with random logouts. Fix both flags together.
Overbroad domain attribute
domain=.co.np or a parent domain you do not control leaks cookies to sibling hosts. Keep the domain exact or use a single intentional wildcard like .yourbrand.com.
Long session lifetime without rotation
HttpOnly limits theft via JS. It does not help if the cookie itself lives 30 days on a shared computer. Keep SESSION_LIFETIME at 120 minutes for admin areas. Regenerate ID on privilege change.
WordPress and WooCommerce 11.1
WordPress sets its own auth cookies via wp-config.php constants and plugin filters. WooCommerce adds cart session cookies. Audit with the same DevTools method. Plugins that set JavaScript-readable "session" cookies defeat HttpOnly benefits. Review third-party chat and analytics scripts on document-heavy legal portals especially carefully.
For API-only backends that abandon cookies entirely, compare trade-offs in JWT vs session vs API key authentication. Cookies remain the right default for server-rendered Laravel Blade apps and enterprise web portals.
External references worth bookmarking: the MDN Set-Cookie reference, the Laravel 13 session documentation, and OWASP Session Management Cheat Sheet. They stay aligned with browser behaviour better than random Stack Overflow answers from 2019.
Store production secrets outside git. Encrypted env patterns from Ansible Vault for secrets and API rate limiting complement cookie hardening. They do not replace it. Defense in depth means fixing XSS, enforcing CSRF tokens, and locking session storage. Cookie flags are layer one, not the whole wall.
If you run payment callbacks from eSewa, Khalti, or Stripe, confirm return URLs stay same-site or use token exchange instead of cross-site cookie reliance. I've seen payment success pages lose sessions when Strict was enabled without planning the redirect chain. Map the full redirect path before you flip SameSite to Strict on a live store like a grocery delivery platform.
Redis 8.10 as a session driver adds server-side expiry independent of cookie Max-Age. Stolen cookie bytes still work until the server entry dies. Short lifetimes plus session_regenerate_id() on login close that gap. For high-traffic apps, read about speed optimisation alongside security. A slow login page encourages password reuse and shared sessions on office kiosks — a human factor no flag fixes.
On projects I ship, cookie policy is part of the deployment checklist next to SSL and backup verification. Treat it that way on your stack too.
Key Takeaways
- Enable
HttpOnly,Secure(HTTPS), andSameSite=Laxon every production session cookie. - Laravel 13: set
SESSION_HTTP_ONLY,SESSION_SECURE_COOKIE, andSESSION_SAME_SITEin.envand verify via curl or DevTools. - Keep the XSRF-TOKEN cookie JavaScript-readable; never apply HttpOnly to it in SPA setups.
- Use
SameSite=Strictonly when you accept logout-on-external-link behaviour; default to Lax. - Trust reverse-proxy HTTPS headers correctly or Secure cookies will not issue on TLS-terminated hosts.
- Test live response headers after every deploy — cached config silently drops flags.
People Also Ask
Does HttpOnly prevent XSS attacks?
No. HttpOnly stops JavaScript from reading the session cookie. An XSS attacker can still send authenticated requests from the victim's browser while the session is active. You must still escape output, use a Content Security Policy, and fix XSS sources. HttpOnly limits cookie exfiltration, not session abuse.
Is SameSite=Lax enough for CSRF protection?
It helps significantly but is not a full substitute for CSRF tokens. SameSite blocks many cross-site POST cookie sends. Same-site subdomains, GET-changing routes, and older browsers still need Laravel's @csrf middleware and form tokens. Use both.
Can I use secure session cookies on localhost?
Browsers treat http://localhost as a secure context in many cases, but Secure cookies still expect HTTPS on real domains. For local HTTP dev, disable SESSION_SECURE_COOKIE. Use mkcert or Laravel Valet with HTTPS locally if you want parity with production flags.
What is the difference between session cookies and permanent cookies?
Session cookies expire when the browser closes (Max-Age=0 or no expiry). Persistent cookies carry an explicit expiry date. Laravel's session cookie respects SESSION_LIFETIME in minutes. Auth "remember me" tokens are separate cookies and need the same HttpOnly, Secure, and SameSite treatment.
Ship secure sessions on your next deploy
Secure session cookies with SameSite and HttpOnly take thirty minutes to configure and years of headache to skip. Set the flags in Laravel or PHP, verify headers on staging, and add a CI smoke check before production. If you want a second pair of eyes on auth, proxy headers, or a legal-tech portal that handles sensitive documents, reach out for a security-focused review or browse web development services for full-stack delivery.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

