Session Fixation is an attack where an attacker forces a victim’s browser to use a specific session identifier known to the attacker before the victim logs in. If the application fails to generate a new session identifier upon successful authentication, the attacker can then use the same, fixated session identifier to impersonate the victim after they have logged in. The attacker essentially “rides along” on the session they forced the user to adopt. 🍪📌Common Attack Flow:
Attacker obtains a valid session ID from the application (e.g., by visiting the login page).
Attacker tricks the victim into using this specific session ID (e.g., via a phishing link containing the session ID in the URL: http://vulnerable.com/?SESSIONID=attacker_knows_this, or via XSS setting the cookie).
Victim logs into the application using the session ID provided by the attacker.
The application authenticates the user but fails to generate a new session ID.
Attacker uses the known session ID to access the victim’s authenticated session.
This vulnerability occurs when the application’s session management logic fails to invalidate the existing session identifier and issue a new one immediately after successful authentication. Modern web frameworks often handle this automatically as part of their standard login procedures.Key Remediation Principles:
Regenerate Session ID on Login:Crucially, always generate a completely new session identifier and invalidate the old (pre-login) one immediately upon successful user authentication.
Do Not Accept Session IDs from URL Parameters: Configure the framework or server to ignore session identifiers passed as query parameters. Rely only on session cookies.
Use Secure Cookie Attributes: Set HttpOnly, Secure, and SameSite=Strict (or Lax) attributes on session cookies to mitigate other risks like XSS stealing the cookie or CSRF leveraging it (See CWE-1004, CWE-614, CWE-1275).
Django’s login() function and Flask-Login’s login_user() function typically handle session ID regeneration correctly by default. Vulnerabilities arise in custom login logic or if defaults are overridden.
Vulnerable Scenario 1: Custom Django Login without login()
Manually setting session variables without calling django.contrib.auth.login.
# views/auth.py (Django Custom Login)from django.shortcuts import redirectfrom django.contrib.auth import authenticatedef custom_manual_login(request): if request.method == 'POST': user = authenticate(username=request.POST['username'], password=request.POST['password']) if user is not None: # DANGEROUS: Manually setting user ID in session without # invalidating the old session ID (fixation). request.session['_auth_user_id'] = user.pk request.session['_auth_user_backend'] = user.backend # Missing: session rotation/invalidation return redirect('/dashboard') # ... handle failure ... # ... render form ...
Django: Always use django.contrib.auth.login(request, user) after authenticating a user. This function automatically rotates the session key.
Flask: Use flask_login.login_user(user). Ensure your Flask session configuration is secure (e.g., using Flask-Session with server-side storage and configuring session protection options if available). Modern Flask versions often have basic session rotation.
Visit the login page of the application in your browser. Use developer tools to find the current sessionid cookie value.
Copy this session ID.
In a different browser (or incognito window), manually set the session cookie to the copied value (e.g., using browser extensions like Cookie Editor). Alternatively, try passing the session ID as a URL parameter if the application accepts it (?sessionid=...).
Using this second browser (with the fixated session ID), log in successfully.
Go back to the first browser (which still has the original session ID) and refresh the page or navigate to a protected area.
If you are now logged in as the user who authenticated in the second browser, the application is vulnerable to session fixation. A secure application would have invalidated the original session ID upon login in the second browser, leaving the first browser logged out or with a new, unauthenticated session ID.
Using HttpServletRequest.getSession() without invalidating the old session and creating a new one after login. Spring Security handles this correctly by default with its session management filters.
Spring Security: Use the default session fixation protection strategy (migrateSession or newSession). migrateSession (default) creates a new session ID but attempts to copy attributes from the old session. newSession creates a completely clean session. Avoid none.
Manual Servlet: After successful authentication, invalidate the old session and create a new one:
HttpSession oldSession = request.getSession(false); // Get old session without creatingif (oldSession != null) { oldSession.invalidate(); // Invalidate old session}// Create a new session for the authenticated userHttpSession newSession = request.getSession(true);newSession.setAttribute("userPrincipal", user);
Follow the same steps as the Python testing strategy:
Get a pre-login session ID (JSESSIONID).
Force a second browser to use that ID.
Log in using the second browser.
Refresh the first browser.
If the first browser is now logged in, the application is vulnerable. Check Spring Security’s sessionFixation() configuration or manual session handling logic.
ASP.NET Core Identity and session management generally handle session regeneration automatically upon login (SignInManager.PasswordSignInAsync, SignInAsync). Vulnerabilities are more likely in custom authentication or manual session manipulation.
Vulnerable Scenario 1: Custom Authentication Not Regenerating Session
If using custom authentication logic that manually sets session variables without leveraging SignInManager.
// Controllers/AccountController.cs (Custom Auth Example)public IActionResult ManualLogin(string username, string password){ var user = ValidateCredentialsManually(username, password); // Assume returns user object if (user != null) { // DANGEROUS: Directly setting session variables without regenerating session ID. // Attacker could fixate the session ID before this point. HttpContext.Session.SetString("UserId", user.Id.ToString()); HttpContext.Session.SetInt32("IsLoggedIn", 1); // Missing: HttpContext.Session.CommitAsync(); HttpContext.Session.Clear(); then set again? Complex. // Better: Use SignInManager or manually force new session ID if possible. return RedirectToAction("Index", "Home"); } // Handle failure... return View("Login");}
Use SignInManager: Rely on SignInManager.PasswordSignInAsync() or SignInManager.SignInAsync() for logging users in. These methods handle the creation and management of the authentication cookie (which acts as the session identifier for Identity) securely, including regeneration upon login.
Avoid Manual Session Auth: Do not implement authentication by manually setting flags in HttpContext.Session. Use the built-in Identity framework.
Get the pre-login authentication cookie value (e.g., .AspNetCore.Identity.Application or session cookie if using HttpContext.Session).
Force a second browser to use that cookie value.
Log in using the second browser.
Refresh the first browser.
If the first browser is now logged in, investigate how authentication is handled. Ensure SignInManager is used or custom logic explicitly regenerates identifiers. Check if session IDs are accepted from query parameters (they shouldn’t be).
Using session_start() without calling session_regenerate_id(true) after successful login. Laravel’s Auth::attempt() and Session::regenerate() handle this correctly.
Vulnerable Scenario 1: Plain PHP without session_regenerate_id()
<?php// login.php (Plain PHP)session_start(); // Starts session, potentially using attacker-provided ID$username = $_POST['username'];$password = $_POST['password'];$stored_hash = get_password_hash($username); // Assume function existsif ($stored_hash && password_verify($password, $stored_hash)) { // DANGEROUS: User is logged in, but the session ID is not regenerated. // Attacker who fixated the ID before login now has access. $_SESSION['user_id'] = get_user_id($username); $_SESSION['logged_in'] = true; header('Location: /dashboard.php'); exit;} else { header('Location: /login.php?error=1'); exit;}?>
Plain PHP: Call session_regenerate_id(true) immediately after successful authentication and before setting any authenticated session variables. true deletes the old session file.
Laravel: Use Auth::attempt() or Auth::login(), and ensure session regeneration occurs (it’s default in AuthenticatesUsers trait via Request::session()->regenerate()).
// app/Http/Controllers/Auth/LoginController.php (Laravel Default - Secure)use Illuminate\Foundation\Auth\AuthenticatesUsers;// ...class LoginController extends Controller { use AuthenticatesUsers; // Includes secure login attempt handling // ... // AuthenticatesUsers trait calls $request->session()->regenerate() // in the authenticated() method or sendLoginResponse().}// OR if using manual login:public function secureManualLogin(Request $request) { // ... validate, check credentials ... if ($validCredentials) { Auth::login($user); // Log the user in // SECURE: Manually regenerate session ID $request->session()->regenerate(); return redirect()->intended('dashboard'); } // ...}
Get pre-login session ID (PHPSESSID, laravel_session).
Force second browser to use it.
Log in with second browser.
Refresh first browser.
If first browser is logged in, check login code for session_regenerate_id(true) (plain PHP) or Request::session()->regenerate() (Laravel). Ensure session IDs are not accepted from URL parameters (php.ini: session.use_trans_sid = 0).
Vulnerable Scenario 2: Passport.js without Session Regeneration (Less Common)
While Passport’s standard req.login() often triggers session regeneration depending on the express-session setup, custom callbacks or misconfigurations might bypass it.
Use the req.session.regenerate(callback) method provided by express-session immediately after successful authentication and before setting the authenticated user’s data in the new session.
Get pre-login session ID (connect.sid or custom name).
Force second browser to use it (e.g., via Cookie Editor extension).
Log in with second browser.
Refresh first browser.
If first browser is logged in, check the login route handler for a call to req.session.regenerate(). Ensure session IDs are not accepted from URL parameters.
# app/controllers/sessions_controller.rb (Secure Manual Login)class SessionsController < ApplicationController def create user = User.find_by(email: params[:email]) if user&.authenticate(params[:password]) # SECURE: Reset session to invalidate the old ID and create a new one. reset_session # Now store user ID in the new session. session[:user_id] = user.id redirect_to root_path, notice: 'Logged in successfully.' else flash.now[:alert] = "Invalid email or password." render :new, status: :unprocessable_entity end endend# Using Devise (Secure by Default)# Ensure standard Devise controllers or Warden hooks are used.# Devise's sign_in method typically handles session rotation.
Get pre-login session cookie value (_my_app_session).
Force second browser to use it.
Log in with second browser.
Refresh first browser.
If first browser is logged in, check the login action for a call to reset_session. Ensure session identifiers are not accepted via URL parameters (config/initializers/session_store.rb should not enable this).