Showing posts with label liferay. Show all posts
Showing posts with label liferay. Show all posts

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:



Thursday, 21 January 2010

Liferay + FancyBox + IE8 = problems

The last few days I've been trying to integrate PDF in a nice way into Liferay by showing them in a Lightbox alike way (more on the full integration in a future post). I first tried to use the Liferay.Popup, but that just gave me a headache. So finally I settled on using FancyBox 1.2.6.

Getting it to work on OSX/Safari wasn't a problem, Firefox also was a breeze, but IE8 was a different story. It was the first browser to cause a real problem, a Javascript error to be exact: Object doesn't support this property or method. The strange thing was that the examples on the FancyBox website would work correctly in IE8. So what to do now?

The error line and column pointed to some browser detection code (if you can call IE a browser) in the FancyBox Javascript file:
var ieQuirks = null, IE6 = $.browser.msie && $.browser.version.substr(0,1) == 6 && !window.XMLHttpRequest, oldIE = IE6 || ($.browser.msie && $.browser.version.substr(0,1) == 7);
This is default JQuery browser detection code, so I couldn't figure out why this would cause a problem? But some Googling turned up a comment of Nate Cavanaugh to a post about Liferay/IE/Browser detection problems. In his comment he mentions that Liferay has had its own Javascript browser detection code since ages. So I decided to change the FancyBox code to use the Liferay browser detection instead of the JQuery one:
var ieQuirks = null;
var IE6 = Liferay.Browser.isIe() && Liferay.Browser.getMajorVersion() == 6 && !window.XMLHttpRequest;
var oldIE = IE6 || (Liferay.Browser.isIe() && Liferay.Browser.getMajorVersion() == 7);

And what do you think: the error disappeared. I'm no Javascript expert and so I can't explain what exactly causes the Javascript error, maybe a strange Liferay/JQuery/browser interaction (it's not the first one we've encountered), but frankly I don't care since it works now and I don't have to time to investigate further. On to the next problem...

Monday, 7 December 2009

Liferay installation problem: Could not find the main class: . Program will exit.

A few weeks ago I was installing Liferay at a client and ran into a very weird installation problem. I was under the impression that I'd done everything correctly, but when I tried to start Liferay I got a baffling error message almost immediatly:

"Could not find the main class: . Program will exit."

As Eddie Izzard would say: Quod The Fuck! How can a simple Liferay install go this wrong? I followed some simple steps to install it:
  • download Liferay
  • transfer ZIP file to server
  • unzip the downloaded file
  • check installed Java version
  • check DB connection
  • configure DB connection in portal-ext.properties
  • start Liferay
By now I've done this simple procedure more than a few times and it never failed, ... until now. So I was kinda gobsmacked. I started digging around, but couldn't find an obvious cause. Also Googling didn't turn up any leads. So I was on my own and decided to start from the beginning and check every line in the Tomcat start script and quickly found that strange things were happening with the startup classpath, hence the empty main class name. A JAR file seemed to be missing: bootstrap.jar.

How could that be possible. I downloaded a fresh Liferay ZIP that unzipped without problems. Then I remembered a long pause during the download around the same time the datacenter people were fiddling with the firewall. A second fresh download later, this time without hickups, I was able to run a file size check against the old download and what do you think: the first download was 20Mb smaller.

How it was able to unzip with throwing obvious errors I still don't know, but after deleting the previous install, unzipping the new one, Liferay started without any problem.

Monday, 2 November 2009

Liferay Password Policies with a regular expression sauce

For a Liferay project that I'm working on there are a bunch of security requirements. One of those is that a user password has to conform to a certain set of rules. Because Liferay is pretty customizable, changing the password policy shouldn't be a problem.

Looking around the Control Board already reveals some possibilities hidden in the Portal > Password Policies menu. Here you can tweak the default password policy or even create a new one. There are several things to customize on this page:
  • Changeable/Change required: can the user change his/her password and is a change required after the first login
  • Password Syntax Checking: (dis)allow passwords that are common dictionary words and set a minimum password length
  • Password History: disallow passwords according to a password history of a configurable length
  • Password Expiration: how long is a password valid before it needs to be changed
  • Lockout: how many times can someone try to log in correctly before his/her account gets locked and how does it get unlocked
While this is already a nice feature set, the password syntax checking isn't quite enough to satisfy our password requirements. Checking the source code and the default portal.properties file reveals that it is possible configure a custom passwords.toolkit. This can be one you create yourself and that extends from com.liferay.portal.security.pwd.BasicToolkit:
public abstract class BasicToolkit {

public abstract String generate();

public void validate(String password1, String password2, PasswordPolicy passwordPolicy)
throws PortalException, SystemException {

validate(0, password1, password2, passwordPolicy);
}

public abstract void validate(long userId, String password1, String password2, PasswordPolicy passwordPolicy)
throws PortalException, SystemException;

}

Another possibility is to use the regular expression toolkit that is available in Liferay. This toolkit can be configured as follows:

#
# Input a class name that extends
# com.liferay.portal.security.pwd.BasicToolkit. This class will be called to
# generate and validate passwords.
#
passwords.toolkit=com.liferay.portal.security.pwd.RegExpToolkit

#
# If you choose to use com.liferay.portal.security.pwd.RegExpToolkit as
# your password toolkit, set the regular expression pattern that will be
# used to generate and validate passwords.
#
# Note that \ is replaced with \\ to work in Java.
#
# The pattern ensures that passwords must have at least 4 valid
# characters consisting of digits or letters.
#
passwords.regexptoolkit.pattern=(?=.{4})(?:[a-zA-Z0-9]*)
One useful regular expression (customized a bit more) that checks if a password is at least 6 characters long, contains at least 1 digit, 1 lowercase/uppercase character, 1 symbol from a given list and no whitespace is:

^.*(?=.{6,})(?=.*\d)(?=.*[a-zA-Z])(?=.*[@#$%^&+=])(?!.*\s).*$

I would be using this regular expression if I could only find out how to extend it so it checks if a passwords contains for example at least 3 lowercase/uppercase characters. I tried several variants, but didn't succeed. So for the moment I'm using a custom written BasicToolkit that is using plain old Java code to do the necessary checks.

If a tree falls in the woods does it make a sound? Can you merge a SVN branch if your mother hasn't told you about it?

Change Liferay Chat Portlet Buddy List Strategy

Some time ago I had to check out the possibilities of the Liferay Chat portlet together with a colleague to see if it could be used in our current project or not. Installing it was easily done via the recently introduced Liferay Control Panel. After some quick tests it seemed that for the most part the portlet would be useable if we could find some way to configure who gets to chat to who.

Some clicking around in the control panel and checking out the portlet preferences didn't provide any clues as to how this could be done. So we started looking at the Chat portlet source code and discovered something promising: ChatUtil.getBuddies(long userId).

public class ChatUtil {

public static final int MAX_ENTRIES = 50;

public static final long MAX_POLL_LATENCY = Time.SECOND * 15;

public static final long ONLINE_DELTA = Time.MINUTE;

public static List<Object[]> getBuddies(long userId)
throws SystemException {

long modifiedDate = System.currentTimeMillis() - ONLINE_DELTA;

List<Object[]> buddies = null;

if (PortletPropsValues.BUDDY_LIST_STRATEGY.equals("all")) {
buddies = StatusLocalServiceUtil.getAllStatuses(
userId, modifiedDate, 0, SearchContainer.DEFAULT_DELTA);
}
else if (PortletPropsValues.BUDDY_LIST_STRATEGY.equals("communities")) {
buddies = StatusLocalServiceUtil.getGroupStatuses(
userId, modifiedDate, 0, SearchContainer.DEFAULT_DELTA);
}
else if (PortletPropsValues.BUDDY_LIST_STRATEGY.equals("friends")) {
buddies = StatusLocalServiceUtil.getSocialStatuses(
userId, SocialRelationConstants.TYPE_BI_FRIEND,
modifiedDate, 0, SearchContainer.DEFAULT_DELTA);
}
else if (PortletPropsValues.BUDDY_LIST_STRATEGY.equals(
"communities,friends")) {

List<Object[]> groupBuddies =
StatusLocalServiceUtil.getGroupStatuses(
userId, modifiedDate, 0, SearchContainer.DEFAULT_DELTA);
List<Object[]> socialBuddies =
StatusLocalServiceUtil.getSocialStatuses(
userId, SocialRelationConstants.TYPE_BI_FRIEND,
modifiedDate, 0, SearchContainer.DEFAULT_DELTA);

buddies = new ArrayList<Object[]>(
groupBuddies.size() + socialBuddies.size());

buddies.addAll(groupBuddies);

BuddyComparator buddyComparator = new BuddyComparator(true);

for (Object[] socialBuddy : socialBuddies) {
if (Collections.binarySearch(
groupBuddies, socialBuddy, buddyComparator) < 0) {

buddies.add(socialBuddy);
}
}

Collections.sort(buddies, buddyComparator);
}
else {
buddies = new ArrayList<Object[]>();
}

return buddies;
}

}
This code seems to suggest that there are 4 different ways in which the list of people you can chat to is constructed:
  • all
  • communities
  • friends
  • communities,friends (this looks like something that will have to be refactored someday...)
It seems from the code that all is the default strategy. As almost everything in Liferay can be configured by extending the default portal.properties file, by making and deploying a portal-ext.properties file containing a set of diffs against the original properties file. So we tried to change the strategy to communities by adding 'buddy.list.strategy=communities' to our portal-ext.properties file, using the key as suggested by the code in the PortletPropsValues class:
public class PortletPropsValues {

public static final String BUDDY_LIST_STRATEGY = GetterUtil.getString(
PortletProps.get("buddy.list.strategy"));

}

After restarting the portal and testing, it seemed as if nothing had changed. We still got the see the same people in our respective buddy list. We tried changing the organisations and communities our different users belonged to, but nothing seemed to have the desired effect. Where did we go wrong...? Some Googling pointed us to a forum post that reinforced our feeling that we were indeed looking in the right direction. But why didn't it work then?

While I had to start working on something else, my colleague continued to look into the problem and ultimately found the solution. While we did indeed find the correct configuration key/value pair, we had put it in the wrong file, portal-ext.properties, as the Chat portlet, by means of the portlet.properties file, is configured to override even this file:

include-and-override=portlet-ext.properties

buddy.list.strategy=all
#buddy.list.strategy=communities
#buddy.list.strategy=friends
#buddy.list.strategy=communities,friends
So setting the buddy list strategy in the portal-ext.properties file doesn't have any effect since it will be overridden by the value in portlet.properties because of the include-and-override setting. So when you change the value directly in the portlet everything starts to make a whole lot more sense.

And now I'm gonna have me some object-oriented toast.

Tuesday, 28 July 2009

Liferay theme problem

A while ago we were getting some issues reported from users that wew seeing some strange screens when accessing our Liferay portal. Although they saw most of the data we expected them to see, it wasn't presented properly, as if no theme was being applied. Looking in the logs didn't turn up anything useful and since I didn't see it with my own eyes I instructed the user that reported the error to email me a screenshot when it happened again.


So fast forward to today and I get an email with a screenshot. The screen is pretty white generally, but contains all the data, text and images, that I would expect on that given page, but it looks like no theme at all is applied. It check it for myself using Firefox, the user that reported the problem is using IE and you never know, and low and behold, I get the same fucked up page. So immediately I dive into the logs again, but still nothing.


So what could it be, maybe the portal thinks it is being accessed by a mobile device and presents a simpler theme? But that doesn't really make sense since both the user and I are using desktop browsers. Still I check the theme settings on the portal using the 'Manage Pages' part of the dock, but everything seems to be in order. So next step, check Google. And there we find something interesting, a caching problem: LPS-2834. This issue describes exactly what we are experiencing since after a few refreshes in my browser I get the correctly rendered portal page again. So it seems that something gets cached when a mobile device accesses the portal, but that this cached result is also returned when a non mobile browser requests the same content.


So we could apply the patch that is attached to the issue, but we'll probably use this issue as an excuse to do an upgrade to Liferay Portal 5.2.2 as this will solve this bug and some other problems we're having with our current Liferay Portal 5.1.1 on which I may or may not elaborate in the future.