Session Fixation

Description

Session Fixation occurs when authenticating a user, or otherwise establishing a new user session, without invalidating any existing session identifier. This gives an attacker the opportunity to steal authenticated sessions. The attacker first obtains a valid session identifier (through various means), then tricks a victim into authenticating using that session identifier. Since the application doesn't generate a new session ID upon authentication, the attacker now possesses a valid, authenticated session token and can impersonate the victim.

Risk

Session fixation enables attackers to hijack user sessions without needing to capture credentials or session tokens through network interception. CVE-2025-4644 in Payload CMS demonstrated a novel variant where JWT tokens remained valid after account deletion, allowing attackers to hijack sessions of newly created accounts that reused the same identifier. ABB ASPECT-Enterprise, NEXUS, and MATRIX Series products had high-severity session fixation vulnerabilities in 2025. Jenkins OpenId Connect Authentication plugin was also affected. Unlike session hijacking which requires intercepting an active session, session fixation allows pre-authentication attacks where the attacker sets up the session before the victim even logs in.

Solution

Always generate a new session identifier upon successful authentication. Invalidate the old session completely. Bind sessions to additional user attributes (IP address, user agent) as secondary verification. Implement session timeout policies. Use secure, HttpOnly, and SameSite cookie flags. Reject session identifiers from URL parameters—only accept them from cookies. Implement session rotation at privilege level changes. Monitor for session anomalies such as geographic impossibilities or concurrent sessions from different locations.

Common Consequences

ImpactDetails
Access ControlScope: Session Hijacking

Attackers gain full access to victim's authenticated session and can perform any action as the victim.
AuthenticationScope: Identity Theft

The attacker effectively assumes the victim's identity for the duration of the session.
Non-RepudiationScope: Action Attribution

Malicious actions are logged under the victim's account, potentially framing them for the attacker's activities.

Example Code + Solution Code

Vulnerable Code

<?php
// VULNERABLE: Session ID not regenerated after login
session_start();

if ($_POST['username'] && $_POST['password']) {
    if (authenticate($_POST['username'], $_POST['password'])) {
        // Session ID remains the same - attacker's pre-set ID is now authenticated!
        $_SESSION['authenticated'] = true;
        $_SESSION['username'] = $_POST['username'];
        header('Location: /dashboard');
    }
}
?>

<!-- VULNERABLE: Session ID in URL -->
<a href="login.php?PHPSESSID=attacker_controlled_id">Login</a>
# VULNERABLE: Flask without session regeneration
from flask import Flask, session, redirect

app = Flask(__name__)

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']

    if authenticate(username, password):
        # Session ID stays the same after authentication!
        session['authenticated'] = True
        session['user'] = username
        return redirect('/dashboard')

    return 'Login failed'
// VULNERABLE: Java Servlet without session invalidation
@WebServlet("/login")
public class LoginServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        if (authenticate(username, password)) {
            // Old session retained - vulnerable to fixation!
            HttpSession session = request.getSession();
            session.setAttribute("authenticated", true);
            session.setAttribute("username", username);
            response.sendRedirect("/dashboard");
        }
    }
}

Fixed Code

<?php
// SAFE: Regenerate session ID after authentication
session_start();

// Prevent session ID in URLs
ini_set('session.use_only_cookies', 1);
ini_set('session.use_trans_sid', 0);

if ($_POST['username'] && $_POST['password']) {
    if (authenticate($_POST['username'], $_POST['password'])) {
        // Destroy old session and create new one
        session_regenerate_id(true);  // true = delete old session

        $_SESSION['authenticated'] = true;
        $_SESSION['username'] = $_POST['username'];
        $_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
        $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
        $_SESSION['created_at'] = time();

        header('Location: /dashboard');
        exit;
    }
}

// Validate session integrity on each request
function validate_session() {
    if (!isset($_SESSION['authenticated'])) {
        return false;
    }

    // Check session binding
    if ($_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
        session_destroy();
        return false;
    }

    // Check session age
    if (time() - $_SESSION['created_at'] > 3600) {  // 1 hour
        session_destroy();
        return false;
    }

    return true;
}
?>
# SAFE: Flask with session regeneration
from flask import Flask, session, redirect, request
from flask_session import Session
import secrets

app = Flask(__name__)
app.config['SECRET_KEY'] = secrets.token_hex(32)
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SECURE'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'

@app.route('/login', methods=['POST'])
def login_safe():
    username = request.form['username']
    password = request.form['password']

    if authenticate(username, password):
        # Clear existing session data
        session.clear()

        # Generate new session (Flask-Session handles ID regeneration)
        session['authenticated'] = True
        session['user'] = username
        session['ip_address'] = request.remote_addr
        session['user_agent'] = request.user_agent.string
        session['created_at'] = time.time()

        # For extra security, regenerate session ID explicitly
        session.modified = True

        return redirect('/dashboard')

    return 'Login failed', 401

@app.before_request
def validate_session():
    if 'authenticated' in session:
        # Validate session binding
        if session.get('ip_address') != request.remote_addr:
            session.clear()
            return redirect('/login')

        # Rotate session ID periodically
        if time.time() - session.get('created_at', 0) > 900:  # 15 minutes
            session['created_at'] = time.time()
            session.modified = True
// SAFE: Java Servlet with session invalidation and regeneration
@WebServlet("/login")
public class SecureLoginServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        if (authenticate(username, password)) {
            // Invalidate existing session completely
            HttpSession oldSession = request.getSession(false);
            if (oldSession != null) {
                oldSession.invalidate();
            }

            // Create new session with fresh ID
            HttpSession newSession = request.getSession(true);
            newSession.setAttribute("authenticated", true);
            newSession.setAttribute("username", username);
            newSession.setAttribute("ip_address", request.getRemoteAddr());
            newSession.setAttribute("user_agent", request.getHeader("User-Agent"));
            newSession.setAttribute("created_at", System.currentTimeMillis());

            // Set session timeout
            newSession.setMaxInactiveInterval(3600);  // 1 hour

            response.sendRedirect("/dashboard");
        } else {
            response.sendRedirect("/login?error=invalid");
        }
    }
}

// Session validation filter
@WebFilter("/*")
public class SessionValidationFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpSession session = request.getSession(false);

        if (session != null && session.getAttribute("authenticated") != null) {
            String storedIP = (String) session.getAttribute("ip_address");

            if (!request.getRemoteAddr().equals(storedIP)) {
                session.invalidate();
                ((HttpServletResponse) res).sendRedirect("/login?error=session");
                return;
            }
        }

        chain.doFilter(req, res);
    }
}

Exploited in the Wild

Payload CMS JWT Reuse (Payload CMS, 2025)

CVE-2025-4644 in Payload CMS before 3.44.0 allowed attackers to capture JWTs from accounts they created, delete those accounts, and then reuse the still-valid JWTs to hijack sessions of new users who were assigned the same identifier.

ABB Industrial Control Systems (ABB, 2025)

Session fixation vulnerabilities in ABB ASPECT-Enterprise, NEXUS, and MATRIX Series industrial control products allowed attackers to hijack operator sessions in critical infrastructure environments.

Jenkins OpenId Connect Plugin (Jenkins, 2024)

High-severity session fixation vulnerability in Jenkins OpenId Connect Authentication plugin enabled attackers to hijack authenticated sessions in CI/CD environments.


Tools to test/exploit


CVE Examples


References

  1. MITRE. "CWE-384: Session Fixation." https://cwe.mitre.org/data/definitions/384.html

  2. OWASP. "Session Fixation." https://owasp.org/www-community/attacks/Session_fixation