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:



Friday 5 February 2010

Recursive delete files/directories on OSX

When working with Java projects in Eclipse, or some other IDE, checked out from a source code repository such as CVS or SVN, you sometimes need to remove the IDE's project files or files created by the repository system. On the command line there's an easy way to do this using one of the following 2 code snippets:

  • Files: rm -rf `find . -name '*.project'`
  • Directories: rm -rf `find . -type d -name '.svn'`