Tuesday, 9 February 2010

Fixing session fixation in Liferay on Tomcat

The last months I've been working on a Liferay 5.2.5 (embedded Tomcat 6.0.18) portal implementation for a customer and about a week ago they had a security expert do a penetration test on it. One of the biggest remarks was that the portal had a problem with session fixation, a problem that can allow a malicious third party to hijack a portal session.

A short description of the problem is that it is possible for an attacker to provide someone with a URL that contains a session ID that he has retrieved. When the unsuspecting user clicks the URL and logs in, the portal will use the provided session ID, thus enabling the attacker to request URLs with the same session ID and get results as if he had logged in.

The cause of the problem is usually that the web application doesn't change the session ID for a successful login, but continues to use the provided one. The solution is simple, change the session ID just before or after a login, the implementation of this proved to be a bit more difficult.

I first looked at using the login.events.pre or login.events.post events (see portal.properties) that Liferay provides, but invalidating the session, creating a new one and copying over the old session information to the new session in one of these custom actions didn't solve the problem. When using a custom pre login action invalidating the session caused a problem in Liferay's MainServlet because that keeps a reference to the old session and tries to set an attribute on it after the pre login events have been executed causing an IllegalStateException. So I tried a custom post login action since the MainServlet doesn't do any session manipulation after the post login events, but eventhough the action ran without exceptions, the session ID didn't change.

So I switched to plan B: a servlet filter. I used my post login action code in a pretty straightforward servlet filter and configured Liferay to use it, but the result was the same: the code was being executed, but the session ID stubbornly stayed the same even after invalidating the old session contrary to what this post claims (no fingerpointing, just an observation, could be a difference in Tomcat versions).

So what to do now, plan C? Some Googling brought one more possibility: a custom Tomcat Valve. By this time I feld I was getting into 'obscure hack' territory. But I still decided to give it a twirl since we're using Tomcat, no change of web container is foreseen for the far future and we're in control of the enviroment. I quickly threw together a Maven project in Eclipse, put the code attached to the blog post I found in it, packaged it, put the resulting JAR file in tomcat/lib/ext, added the valve definition to tomcat/conf/context.xml and restarted Liferay. Again the code was being executed just fine, but ... the session ID remained the same. Unbe-f*cking-lievable. 3 strikes and I'm out.

Or maybe not. I complained to a collegue about my trials and tribulations trying to solve the session fixation problem and with the provided information he was able to find a blog post with a variation of the Tomcat Valve solution. The solution is largely similar to the previous one, except that it manipulates the request/session a bit more. Applying these changes to the code of the previous Valve, packaging it again and redeploying it brought me to my fourth test and this time 'the bar was green' so to speak. This time the custom Valve seemed to work as expected and produce a different session ID between opening the first page of the portal and going to the login page.

No only one task was left to do: take the best parts off the 2 custom Valves and create an uberValve. One Valve to rule them all:

package your.company.valves;

import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.ServletException;

import org.apache.catalina.Session;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.apache.juli.logging.Log;

/**
* Valve to regenerate HTTP Session ID's. Based on information
* available in the following 2 links:
*
* http://mikusa.blogspot.com/2008/06/tomcat-authentication-session-fixation.html
* http://www.koelnerwasser.de/?p=11
*/
public class FixSessionFixationValve extends ValveBase {

private static final String INFO = "your.company.valves.FixSessionFixationValve/1.0";

private String url = null;

public String getInfo() {
return INFO;
}

public void setUrl(String url) {
this.url = url;
}

public String getUrl() {
return url;
}

public void invoke(Request request, Response response) throws IOException, ServletException {
Log logger = container.getLogger();

if (url != null && !"".equals(url) && request.getRequestURI().contains(getUrl())) {
// step 1: save old session
Session oldSession = request.getSessionInternal(true);
Map<String, Object> oldAttribs = new HashMap<String, Object>();
Map<String, Object> oldNotes = new HashMap<String, Object>();

if (logger.isDebugEnabled()) logger.debug("Old session ID: " + oldSession.getId());

// Save HTTP session data
Enumeration names = oldSession.getSession().getAttributeNames();
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
oldAttribs.put(name, oldSession.getSession().getAttribute(name));
}

// Save Tomcat internal session data
Iterator it = oldSession.getNoteNames();
while (it.hasNext()) {
String name = (String) it.next();
oldNotes.put(name, oldSession.getNote(name));
}

// step 2: invalidate old session
request.getSession(true).invalidate();
request.setRequestedSessionId(null);
request.clearCookies();

// step 3: create a new session and set it to the request
Session newSession = request.getSessionInternal(true);
request.setRequestedSessionId(newSession.getId());

if (logger.isDebugEnabled()) logger.debug("New session ID: " + newSession.getId());

// step 4: copy data pointer from the old session
// to the new one. Restore HTTP session data
for (String name : oldAttribs.keySet()) {
newSession.getSession().setAttribute(name, oldAttribs.get(name));
}

// Restore Tomcat internal session data
for (String name : oldNotes.keySet()) {
newSession.setNote(name, oldNotes.get(name));
}
}

getNext().invoke(request, response);
}
}

This valve is configurable with one parameter, url, that is used to signal when the session ID needs to be invalidated and recreated. In the case of Liferay, I'm using /c/portal/login.

<?xml version='1.0' encoding='utf-8'?>
<Context useHttpOnly="true">

<!-- Default set of monitored resources -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>

<Valve className="your.company.valves.FixSessionFixationValve" url="/c/portal/login" />

</Context>

And that's it for today folks. It was late enough yesterday due to one hell of a deploy that continued to well after midnight, so I'm calling it a night.

Update 30/03/2010: after testing the valve a bit it seemed that it didn't work exactly as wanted, because the URL I was using to detect a login isn't called in all cases. The fix for this is not to detect a URL, but a POST parameter. For this we need to do 2 things: change the code of the valve a bit and move the valve configuration from context.xml to server.xml.


package your.company.valves;

import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.ServletException;

import org.apache.catalina.Session;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.apache.juli.logging.Log;

/**
* Valve to regenerate HTTP Session ID's. Based on information
* available in the following 2 links:
*
* http://mikusa.blogspot.com/2008/06/tomcat-authentication-session-fixation.html
* http://www.koelnerwasser.de/?p=11
*/
public class FixSessionFixationValve extends ValveBase {

private static final String INFO = "be.belgacom.enable.security.FixSessionFixationValve/1.0";

private String parameterName = null;
private String value = null;

public String getInfo() {
return INFO;
}

public String getParameterName() {
return parameterName;
}

public void setParameterName(String parameterName) {
this.parameterName = parameterName;
}

public String getValue() {
return value;
}

public void setValue(String value) {
this.value = value;
}

@SuppressWarnings("unchecked")
public void invoke(Request request, Response response) throws IOException, ServletException {
String param = request.getParameter(getParameterName());
if (param != null && getValue().equals(param)) {
Log logger = container.getLogger();

// Save old session
Session oldSession = request.getSessionInternal(true);
Map<String, Object> oldAttribs = new HashMap<String, Object>();
Map<String, Object> oldNotes = new HashMap<String, Object>();

if (logger.isDebugEnabled()) logger.debug("Old session ID: " + oldSession.getId());

// Save HTTP session data
Enumeration names = oldSession.getSession().getAttributeNames();
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
oldAttribs.put(name, oldSession.getSession().getAttribute(name));
}

// Save Tomcat internal session data
Iterator it = oldSession.getNoteNames();
while (it.hasNext()) {
String name = (String) it.next();
oldNotes.put(name, oldSession.getNote(name));
}

// Invalidate old session
request.getSession(true).invalidate();
request.setRequestedSessionId(null);
request.clearCookies();

// Create a new session and set it to the request
Session newSession = request.getSessionInternal(true);
request.setRequestedSessionId(newSession.getId());

if (logger.isDebugEnabled()) logger.debug("New session ID: " + newSession.getId());

// Copy data pointer from the old session to the new one. Restore HTTP session data
for (String name : oldAttribs.keySet()) {
newSession.getSession().setAttribute(name, oldAttribs.get(name));
}

// Restore Tomcat internal session data
for (String name : oldNotes.keySet()) {
newSession.setNote(name, oldNotes.get(name));
}
}

getNext().invoke(request, response);
}
}

Use the following configuration in server.xml:



14 comments:

  1. Can you provide some further details as to how the session fixation was performed. I've been trying to reproduce this without any luck. I'm testing on the later Enterprise Edition service pack, so maybe the issue is fixed.

    ReplyDelete
  2. To reproduce this you can use Firefox together with the HttpFox extension to inspect the HTTP traffic and cookies. Clear all caches and cookies and then go to an unauthenticated part of the portal while providing a session ID of your own via a request parameter 'jsessionid'. In HttpFox you'll see that the effect of this request is that Liefray creates a cookie with the session ID you provided. When logging in this cookie is kept while we in fact want it to change and use a new session ID so that even if an attacker was able to trick someone into using a special URL the result will be harmless .

    ReplyDelete
  3. Hello Marvin,
    I checked my logs and found a referer from your site.
    The reason why your first trys where not successful is because of the way how the catalina request class creates a new session. If the old session id was stored in a cookie it creates a new session using the old session id:
    if (connector.getEmptySessionPath() && isRequestedSessionIdFromCookie()) {
    session = manager.createSession(getRequestedSessionId());
    }

    The only way to prevent this is to call
    request.RequestedSessionId(null) befor calling
    req.getSessionInternal(true)

    This causes a manager.createSession(null) call in the request.doGetSession(), which generates a new session ID.
    The JBoss Web 2.1.3 (which is a pimped Tomcat 6.0) has fixed this problem.


    Greetings,
    Daniel
    www.koelnerwasser.de

    ReplyDelete
  4. Great fix for something that's been annoying me for days.

    I'd like to use this code: what license is it released under?

    Thanks!

    ReplyDelete
  5. @Chris B: there's no real license for the code, so you can use/change/distribute it as-is, no guarantees given. I've only been able to hack this code together based on the good work of other people that also have shared their work in similar ways and I'm more than happy to do the same. I hope it solves your problem and if there's any problem with it you're free to contact me and I'll try to help.

    ReplyDelete
  6. Thanks. I understand what you mean. I was hoping you'd say BSD or Apache or even GPL :). We get worried here in the US-not giving a license is essentially the same as saying that all rights are reserved. I'll note what you've said in our source code repo and link back to this page. If you decide to put a license on it, please let me know and I'll update our notes.

    ReplyDelete
  7. Hello Marvin,
    I am new to Liferay and had similar kind of problems. I tried using Filters but it was going to infinite redirection loop which is a problem in Liferay 4.4.0.

    I am trying to use the Fixation what you have provided but could you help what would be the parameterName and value in the server.xml???
    I have set value="c/portal/login".
    Also I have tried to set the value to default landing page path property url.

    So could you tell me what would be the correct parameterName??

    ReplyDelete
  8. @Murthy

    'c/portal/login' was the first value I tried, but that doesn't work in all cases. As you can see at the end of the article I found a second value, '/login/login', that works in all the cases I tried. I did however use Liferay 5.2.5 and I don't know if it'll work in 4.4.0

    ReplyDelete
  9. I believe that setting session.enable.phishing.protection to true in portal-ext.properties would do the trick.

    ReplyDelete
  10. @Andrej

    I checked the default portal.properties and it could indeed be as you describe, but I haven't been able to check it on a running Liferay. But as is the case again with a lot of things in Liferay, this functionality is again hidden away under some obscure unknown property for which the name suggest some different functionality.

    ReplyDelete
  11. Hey Marvin,

    can you tell me how you get the liferay portal running with an embedded tomcat? :-) Maybe this code was bundled at this time, but now its not so comfortable :-(


    Tanks and Cheers

    ReplyDelete
  12. @stone: embedded Tomcat == the Liferay + Tomcat bundles that you can download from the Liferay site and that you just have to unzip and start

    ReplyDelete
  13. Hi Marvin,

    We are facing exactly this session fixation problem on another java portal framework.
    We are experimenting with your valve that checks for a login URL and it seems to solve the problem perfectly for us.

    One question : have you noticed any side-effects to using this valve?

    Thanks for posting!

    Sean

    ReplyDelete
  14. @sean: we haven't noticed any side effects in the project where we used this solution. There might be, who knows, but none that we or the customer discovered that affected the normal use of the portal.

    ReplyDelete