Monday, September 21, 2009

Tomcat 6.0.16 quoting cookies

Recently I've stumbled upon a feature in Tomcat 6.0.16:

  • browser cookies containing symbols like : or = get broken in Tomcat when reading using request.getCookies() - only part of the cookie value is returned
  • when writing such cookies using request.addCookie(), Tomcat automatically adds double quotes around the value

Arguable, such cookies are malformed and using them involves security risks.

In reality, such cookies are quite popular among web developers - they allow to put together some user preferences like that:

MyCookie=language=en:currency=EUR

Even Google uses cookies like that.

What options we have to solve the problem ?

  1. use the correct cookie format (with double quotes)
  2. stick to Tomcat 6.0.14 or older
  3. do some custom hack

Option 1 is the best if you can afford it (but you must still account for cookies already present in client browsers).

Option 2 is only a temporary solution - sooner or later you will need to upgrade (e.g. because of important security patches).

So the unfortunate souls like me are left with the Option 3. My cookie is used jointly by many web application and changing the format at once is not possible.

So here is the solution: write a servlet filter that does custom cookie reading and writing.

public class TomcatQuotedCookieFilter implements Filter {

    private static final String MY_COOKIE_NAME = "MyCookie";

    public static class CookieWrappedRequest extends HttpServletRequestWrapper {

        public CookieWrappedRequest(HttpServletRequest request) {
            super(request);
        }

        @Override
        public Cookie[] getCookies() {
            // getCookies() returns correct numbers of cookies, but myCookie value might be broken
            Cookie[] cookies = super.getCookies();

            int myCookieIndex = CookieUtils.findCookiePosition(MY_COOKIE_NAME, cookies);
            if (myCookieIndex == -1) {
                return cookies;
            }

            String cookieValue = CookieUtils.extractCookieValueFromHeader(MY_COOKIE_NAME, getHeader("Cookie"));
            if (cookieValue != null) {
                cookies[myCookieIndex] = new Cookie(MY_COOKIE_NAME, cookieValue);
            }

            return cookies;
        }

    }

    public static class CookieWrappedResponse extends HttpServletResponseWrapper {

        public CookieWrappedResponse(HttpServletResponse response) {
            super(response);
        }

        @Override
        public void addCookie(Cookie cookie) {
            // catch my cookie and apply custom formatting
            if (cookie.getName().equals(MY_COOKIE_NAME)) {
                addHeader("Set-Cookie", CookieUtils.formatSetCookieHeaderValue(cookie));
            } else {
                super.addCookie(cookie);
            }
        }

    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException,
            ServletException {

        filterChain.doFilter(new CookieWrappedRequest((HttpServletRequest) request), new CookieWrappedResponse(
                (HttpServletResponse) response));
    }

    // init, destroy...

}

CookieUtils methods are left as an exercise for the reader :) Those might be dependent on the specific cookie format that you are using.