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; }