Session fixation, a problem easily overlooked

Session fixation is a problem that web applications can have but not realize until it is too late. In general, session fixation derives directly from flaws present within the login mechanism, namely failure to generate a new session for a user upon successful authentication. This vulnerability, when exploited gives attackers the ability to directly influence a user's session id and therefore assume the identity of a targeted user within the system.

At the heart of session fixation generally lies code similar in function to the following:

login.jsp:

< %@ page session="true" % >
< html >
< head >
< title >Login< / title >
< meta http-equiv="Content-Type" content="text/html; charset=utf-8" / >
< meta name="robots" content="noindex, nofollow" / >
< meta http-equiv="pragma" content="no-cache"/ >
< meta http-equiv="cache-control" content="no-cache"/ >
< / head >

< body bgcolor="#66CCFF" onload="document.loginForm.username.focus()" >
< table width="500" border="0" cellspacing="0" cellpadding="0" >
< tr >
< td >
< form name="loginForm" method="post" action="Login" >
< table width="500" border="0" cellspacing="0" cellpadding="0" >
< tr >
< td width="401" >< div align="right" >User Name: < /div >< /td >
< td width="399" >< input type="text" name="username" / >< /td >
< /tr >
< tr >
< td width="401" >< div align="right" >Password: < /div >< /td >
< td width="399" >< input type="password" name="password" / >< /td >
< /tr >
< tr >
< td width="401" > < /td >
< td width="399" >
< br / >< input type="submit" name="Submit" / >
< /td >
< /tr >
< /table >
< /form >
< /td >
< /tr >
< /table >
< / body >
< / html >

Login Servlet:

import javax.servlet.http.*;
import java.io.*;
import java.util.*;

public class Login extends HttpServlet {

private final int MAX_LOGIN_ATTEMPTS = 5;

private boolean canFindUser(String username, String password) {
// Do LDAP, AD, Database lookup functionality here
// and return appropriately
}

private void lockAccount(String username) {
// Do account locking here
}

public void init(ServletConfig config)
throws ServletException {
super.init(config);
}

public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {

// Get context info
ServletContext context = getServletContext();
RequestDispatcher dispatcher;

// Get the user's session created on the login.jsp page
// Do not create a new one
HttpSession session = request.getSession();

// Get request parameters
String username = request.getParameter("username");
String password = request.getParameter("password");

// Check maximum logins attempts
int loginAttempts = (session.getValue("loginAttempts") == null) ? 1 : session.getValue("loginAttempts");
if (loginAttempts > MAX_LOGIN_ATTEMPTS) {
lockAccount(username);
dispatcher = context.getRequestDispatcher("/accountLocked.jsp");
dispatcher.forward(request, response);
}

// Find the user
if (canFindUser(username, password)) {
session.putValue("User", new User(username, password));
dispatcher = context.getRequestDispatcher("/index.jsp");
}
else {
session.putValue("loginAttempts", loginAttempts + 1);
dispatcher = context.getRequestDispatcher("/login.jsp");
}

dispatcher.forward(request, response);
}

public void destroy() {
// do nothing
}
}
As mentioned earlier, session fixation revolves around failure to provide a new session token to a user once they are authenticated (a trusted user state). Instead many applications use the session token utilized during the login process prior to the user being authenticated (an untrusted user state). The key message to pick up at this point is that prior to a user authenticating, your application must assume that the session token being passed to it was generated in that user's browser when they first accessed the login page. This is a very bad assumption to make as it is trivial for an attacker to create and distribute links including a session token that they know and get users to click on the them. This can be accomplished in many ways, the simplest of which only involve adding a JSESSIONID, PHPSESSIONID or ASPSESSID parameter to the URL. As you can see this attack, like many others, is implementation language agnostic. Once the targeted user is logged in, the only step the attacker needs to take is to refresh his/her browser. Given that the session token has not changed during the login process, the attacker has justed assumed the identity of that application user without having to know any credentials. As administrative accounts are usually the most prized in any application, this attack is used quite frequently in spear phishing attacks targeted towards site administrators.

So, how do you protect your application from these types of attacks? Look at the following updated version of the Login servlet.

Login Servlet:

import javax.servlet.http.*;
import java.io.*;
import java.util.*;

public class Login extends HttpServlet {

private final int MAX_LOGIN_ATTEMPTS = 5;

private boolean canFindUser(String username, String password) {
// Do LDAP, AD, Database lookup functionality here
// and return appropriately
}

private void lockAccount(String username) {
// Do account locking here
}

public void init(ServletConfig config)
throws ServletException {
super.init(config);
}

public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {

// Get context info
ServletContext context = getServletContext();
RequestDispatcher dispatcher;

// Get the user's session created on the login.jsp page
// Do not create a new one
HttpSession session = request.getSession();

// Get request parameters
String username = request.getParameter("username");
String password = request.getParameter("password");

// Check maximum logins attempts
int loginAttempts = (session.getValue("loginAttempts") == null) ? 1 : session.getValue("loginAttempts");
if (loginAttempts > MAX_LOGIN_ATTEMPTS) {
lockAccount(username);
dispatcher = context.getRequestDispatcher("/accountLocked.jsp");
dispatcher.forward(request, response);
}

// Find the user
if (canFindUser(username, password)) {
// Killl the current session created by the jsp so it can no longer be used
session.invalidate();

// Create an entirely new session for the logged in user
HttpSession newSession = request.getSession(true);

newSession .putValue("User", new User(username, password));
dispatcher = context.getRequestDispatcher("/index.jsp");
}
else {
session.putValue("loginAttempts", loginAttempts + 1);
dispatcher = context.getRequestDispatcher("/login.jsp");
}

dispatcher.forward(request, response);
}

public void destroy() {
// do nothing
}
}
The difference that you should notice in this updated code is that upon successfully finding the user, the current session is terminated and rendered useless by a call to session.invalidate(). From the Java documentation here, the function "[i]nvalidates this session and unbinds any objects bound to it."
What this means is that the id being used to reference the session used during login no longer points to a valid session object on the server. As such, that session token is no longer valid and cannot be used for anything effectively preventing an attacker from logging in as the targeted user. Now, as long as there are no XSS vulnerabilities that could allow the attacker to use document.cookie to steal the login session that way, your application has now successfully reduced its attack surface which is always a good thing.

Although the actual application code required to prevent session fixation is trivial in nature, it is very important that such be put in place. Otherwise, targeted attacks against your user base could result in large compromises of your application and therefore your data.

2 comments:

OWASP said...

Nice summary. For Java EE you might check out the OWASP ESAPI project which has utilities for handling session fixation on both login and logout. In addition, there's support for copying information from the original (unauthenticated) session to the authenticated one. So if your user fill their shopping cart you don't lose their purchases when they log in.

Matt Presson said...

I have looked at the ESAPI project several times, and I find that although its features do help to enhance the overall security posture of an application, it is not ready for the enterprise market in its current state.

One of the problems that I continually see is that the reference implementation does not take into consideration the common configurations of today's enterprise deployments, namely clustered environments. Many of the design decisions made in the reference implementation seem to fit better in a situation where an application is deployed across a single application server. I also see a problem in the way that the input validation is very US-ASCII centric and not positioned to validate international characters. Although it is entirely possible to create one's own reference implementation, it would be nice to see a provided enterprise considerate reference implementation for a security product toughted as an enterprise solution.

Do not take the above criticism the wrong way. I do appreciate and value the work that has been put into the project, but I also feel it is still lacking in a few key places that are critical to true enterprise acceptance.