XSS filter

Skip to end of metadata
Go to start of metadata

I tested the code below against all of the known xss attacks that are listed on http://ha.ckers.org/xss.html as of July 2007, except for one. I haven't yet tested it against the 'US-ASCII encoding' attack (which tomcat is vulnerable)

The wrapper escapes all params that Stripes binds during its Validation & Binding phase.  Parameters that you get manually through request.getParameter() are NOT sanitized. The code below basically follows the xss security guidance posted at http://www.owasp.org.

Notes:
*This init-param goes in the stripesfilter section
*If you have Interceptor.Classes listed already, I believe this should be listed as the last param value in the existing list.
*This example requires jregex jar to run successfully (jregex.sourceforge.net)

add to web.xml. for example:
<init-param>
	<param-name>Interceptor.Classes</param-name>
	<param-value>
		com.mypath.web.security.XssInterceptor
		net.sourceforge.stripes.controller.BeforeAfterMethodInterceptor
	</param-value>
</init-param>

XssInterceptor.java
package com.mypath.web.security;

import javax.servlet.http.HttpServletRequest;

import net.sourceforge.stripes.action.Resolution;
import net.sourceforge.stripes.controller.ExecutionContext;
import net.sourceforge.stripes.controller.Interceptor;
import net.sourceforge.stripes.controller.Intercepts;
import net.sourceforge.stripes.controller.LifecycleStage;
import net.sourceforge.stripes.controller.StripesRequestWrapper;

@Intercepts(LifecycleStage.BindingAndValidation)
public class XssInterceptor implements Interceptor
{
	private static ThreadLocal<ExecutionContext> currentContext = new ThreadLocal<ExecutionContext>();
	
	public Resolution intercept(ExecutionContext context) throws Exception 
	{
        StripesRequestWrapper stripesWrapper = null;
        HttpServletRequest originalRequest = null;
        try {
            currentContext.set(context);
            stripesWrapper = StripesRequestWrapper.findStripesWrapper(context
                    .getActionBeanContext().getRequest());
            originalRequest = (HttpServletRequest) stripesWrapper.getRequest();
            stripesWrapper.setRequest(new XssRequestWrapper(originalRequest));
            return context.proceed();
        } finally {
            currentContext.remove();
            if (stripesWrapper != null && originalRequest != null)
                stripesWrapper.setRequest(originalRequest);
        }
    }
}

XssRequestWrapper.java
package com.mypath.web.security;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.apache.log4j.Logger;

public class XssRequestWrapper extends HttpServletRequestWrapper
{
	private static final Logger logger = Logger.getLogger(XssRequestWrapper.class);


	private Map<String, String[]> sanitized;
	private Map<String, String[]> orig;
	
	@SuppressWarnings("unchecked")
	public XssRequestWrapper(HttpServletRequest req) 
	{
		super(req);
		orig = req.getParameterMap();	
		sanitized = getParameterMap();
		if (logger.isDebugEnabled())
			snzLogger();
	}		

	@Override
	public String getParameter(String name) 
	{		
		String[] vals = getParameterMap().get(name); 
		if (vals != null && vals.length > 0) 
			return vals[0];
		else        
			return null;        
	}


	@SuppressWarnings("unchecked")
	@Override
	public Map<String, String[]> getParameterMap() 
	{	
		if (sanitized==null)
			sanitized = sanitizeParamMap(orig);
		return sanitized;			

	}

	@Override
	public String[] getParameterValues(String name)
	{	
		return getParameterMap().get(name);
	}


	private  Map<String, String[]> sanitizeParamMap(Map<String, String[]> raw) 
	{		
		Map<String, String[]> res = new HashMap<String, String[]>();
		if (raw==null)
			return res;
	
		for (String key : (Set<String>) raw.keySet())
		{			
			String[] rawVals = raw.get(key);
			String[] snzVals = new String[rawVals.length];
			for (int i=0; i < rawVals.length; i++) 
			{
				snzVals[i] = SafeHtmlUtil.sanitize(rawVals[i]);
			}
			res.put(key, snzVals);
		}			
		return res;
	}


	@SuppressWarnings("unchecked")
	private void snzLogger()
	{
		for (String key : (Set<String>) orig.keySet())
		{
			String[] rawVals = orig.get(key);
			String[] snzVals = sanitized.get(key);
			if (rawVals !=null && rawVals.length>0)
			{
				for (int i=0; i < rawVals.length; i++) 
				{
					if (rawVals[i].equals(snzVals[i]))															
						logger.debug("Sanitization. Param seems safe: " + key + "[" + i + "]=" + snzVals[i]);				
					else
						logger.debug("Sanitization. Param modified: " + key + "[" + i + "]=" + snzVals[i]);
				}		
			}
		}
	}

	// TODO  need to sanitize getHeader(), getCookie(), etc ??

}
SafeHtmlUtil.java (barebones version)
public class SafeHtmlUtil
{
	public static String sanitize(String raw)
	{
		if (raw==null || raw.length()==0)
			return raw;

		return HTMLEntityEncode(canonicalize(raw));
	}


	private static Pattern scriptPattern = new Pattern("script", REFlags.IGNORE_CASE);
	private static Replacer scriptReplacer = scriptPattern.replacer("&#x73;cript");

	public static String HTMLEntityEncode(String input)
	{
		String next = scriptReplacer.replace(input);

		StringBuffer sb = new StringBuffer();
		for ( int i = 0; i < next.length(); ++i )
		{
			char ch = next.charAt( i );

			if (ch=='<')
				sb.append("&lt;");
			else if (ch=='>')
				sb.append("&gt;");
			else
				sb.append(ch);
		}

		return sb.toString();
	}


	// "Simplifies input to its simplest form to make encoding tricks more difficult"
	// though it didn't do seem to do anything to hex or html encoded characters... *shrug* maybe for unicode?
	public static String canonicalize( String input )
	{
		String canonical = sun.text.Normalizer.normalize( input, Normalizer.DECOMP, 0 );
		return canonical;
	}
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.