XSS filter
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)
<init-param> <param-name>Interceptor.Classes</param-name> <param-value> com.mypath.web.security.XssInterceptor net.sourceforge.stripes.controller.BeforeAfterMethodInterceptor </param-value> </init-param>
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);
}
}
}
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 ??
}
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("script");
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("<");
else if (ch=='>')
sb.append(">");
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;
}