![]() | Source code below from: Tomcat: The Definitive Guide By Jason Brittain and Ian F. Darwin Published June, 2003 Average rating
Powells
Alibris
|
/* * $Header: $ * $Revision: $ * $Date: $ * * ==================================================================== * * The Apache Software License, Version 1.1 * * Copyright (c) 1999 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, if * any, must include the following acknowlegement: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowlegement may appear in the software itself, * if and wherever such third-party acknowlegements normally appear. * * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software * Foundation" must not be used to endorse or promote products derived * from this software without prior written permission. For written * permission, please contact apache@apache.org. * * 5. Products derived from this software may not be called "Apache" * nor may "Apache" appear in their names without prior written * permission of the Apache Group. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * * [Additional notices, if required by prior licensing conditions] * */ package com.oreilly.tomcat.valves; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.Set; import javax.servlet.ServletException; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.regexp.RE; import org.apache.regexp.RESyntaxException; import org.apache.catalina.HttpRequest; import org.apache.catalina.HttpResponse; import org.apache.catalina.Logger; import org.apache.catalina.Request; import org.apache.catalina.Response; import org.apache.catalina.ValveContext; import org.apache.catalina.util.ParameterMap; import org.apache.catalina.valves.ValveBase; import org.apache.catalina.valves.RequestFilterValve; /** * Filters out bad user input from HTTP requests to avoid malicious * attacks including Cross Site Scripting (XSS), SQL Injection, and * HTML Injection vulnerabilities, among others. * * @author <a href="mailto:jason@brittainweb.org">Jason Brittain</a> * @version 1.0 */ public class BadInputFilterValve extends RequestFilterValve { // ----------------------------------------------------------- Constructors /** * Construct a new instance of this class with default property values. */ public BadInputFilterValve() { super(); // Populate the (regex, substitution) maps. quotesHashMap.put("\"", """); quotesHashMap.put("\'", "'"); quotesHashMap.put("`", "`"); angleBracketsHashMap.put("<", "<"); angleBracketsHashMap.put(">", ">"); javaScriptHashMap.put( "document(.*)\\.(.*)cookie", "document.cookie"); javaScriptHashMap.put("eval(\\s*)\\(", "eval("); javaScriptHashMap.put("setTimeout(\\s*)\\(", "setTimeout$1("); javaScriptHashMap.put("setInterval(\\s*)\\(", "setInterval$1("); javaScriptHashMap.put("execScript(\\s*)\\(", "exexScript$1("); javaScriptHashMap.put("javascript:", "javascript:"); } // ------------------------------------------------------- Static Variables /** * Descriptive information about this implementation. */ protected static String info = "com.oreilly.tomcat.valves.BadInputFilterValve/1.0"; // ----------------------------------------------------- Instance Variables /** * The flag that determines whether or not to escape quotes that are * part of the request. */ protected boolean escapeQuotes = true; /** * The flag that determines whether or not to escape angle brackets * that are part of the request. */ protected boolean escapeAngleBrackets = true; /** * The flag that determines whether or not to escape JavaScript * function and object names that are part of the request. */ protected boolean escapeJavaScript = true; /** * A substitution mapping (regular expression to match, replacement) * that is used to replace single quotes (') and double quotes (") * with escaped equivalents that can't be used for malicious purposes. */ protected HashMap quotesHashMap = new HashMap(); /** * A substitution mapping (regular expression to match, replacement) * that is used to replace angle brackets (<>) with escaped * equivalents that can't be used for malicious purposes. */ protected HashMap angleBracketsHashMap = new HashMap(); /** * A substitution mapping (regular expression to match, replacement) * that is used to replace potentially dangerous JavaScript function * calls with escaped equivalents that can't be used for malicious * purposes. */ protected HashMap javaScriptHashMap = new HashMap(); /** * The debug level. */ protected int debug = 0; // ------------------------------------------------------------- Properties /** * Gets the flag which determines whether this Valve will escape * any quotes (both double and single quotes) that are part of the * request, before the request is performed. */ public boolean getEscapeQuotes() { return escapeQuotes; } /** * Sets the flag which determines whether this Valve will escape * any quotes (both double and single quotes) that are part of the * request, before the request is performed. * * @param escapeQuotes */ public void setEscapeQuotes(boolean escapeQuotes) { this.escapeQuotes = escapeQuotes; } /** * Gets the flag which determines whether this Valve will escape * any angle brackets that are part of the request, before the * request is performed. */ public boolean getEscapeAngleBrackets() { return escapeAngleBrackets; } /** * Sets the flag which determines whether this Valve will escape * any angle brackets that are part of the request, before the * request is performed. * * @param angleBrackets */ public void setEscapeAngleBrackets(boolean escapeAngleBrackets) { this.escapeAngleBrackets = escapeAngleBrackets; } /** * Gets the flag which determines whether this Valve will escape * any potentially dangerous references to JavaScript functions * and objects that are part of the request, before the request is * performed. */ public boolean getEscapeJavaScript() { return escapeJavaScript; } /** * Sets the flag which determines whether this Valve will escape * any potentially dangerous references to JavaScript functions * and objects that are part of the request, before the request is * performed. * * @param escapeJavaScript */ public void setEscapeJavaScript(boolean escapeJavaScript) { this.escapeJavaScript = escapeJavaScript; } /** * Return descriptive information about this Valve implementation. */ public String getInfo() { return info; } /** * Set the debugging detail level for this Valve. * * @param debug The new debugging detail level. */ public void setDebug(int debug) { this.debug = debug; } // --------------------------------------------------------- Public Methods /** * Sanitizes request parameters before bad user input gets into the * web application. * * @param request The servlet request to be processed * @param response The servlet response to be created * @param valveContext The valve context used to invoke the next valve * in the current processing pipeline * * @exception IOException if an input/output error occurs * @exception ServletException if a servlet error occurs */ public void invoke(Request request, Response response, ValveContext valveContext) throws IOException, ServletException { // Skip logging for non-HTTP requests and responses. if (!(request instanceof HttpRequest) || !(response instanceof HttpResponse)) { valveContext.invokeNext(request, response); return; } // Only let requests through based on the allows and denies. if (denies.length > 0 || allows.length > 0) { if (processAllowsAndDenies(request, response, valveContext)) { // Filter the input for potentially dangerous JavaScript // code so that bad user input is cleaned out of the request // by the time Tomcat begins to perform the request. HashMap parameterEscapes = new HashMap(); if (escapeQuotes) { // Escape all quotes. parameterEscapes.putAll(quotesHashMap); } if (escapeAngleBrackets) { // Escape all angle brackets. parameterEscapes.putAll(angleBracketsHashMap); } if (escapeJavaScript) { // Escape potentially dangerous JavaScript method calls. parameterEscapes.putAll(javaScriptHashMap); } filterParameters(parameterEscapes, request); // Perform the request. valveContext.invokeNext(request, response); } } } /** * Uses the functionality of the (abstract) RequestFilterValve to * stop requests that contain forbidden string patterns in parameter * names and parameter values. * * @param request The servlet request to be processed * @param response The servlet response to be created * @param ValveContext The valve context used to invoke the next valve * in the current processing pipeline * * @exception IOException if an input/output error occurs * @exception ServletException if a servlet error occurs * * @return false if the request is forbidden, true otherwise. */ public boolean processAllowsAndDenies(Request request, Response response, ValveContext valveContext) throws IOException, ServletException { ParameterMap paramMap = (ParameterMap) ((HttpServletRequest) request).getParameterMap(); // Loop through the list of parameters. Iterator y = paramMap.keySet().iterator(); while (y.hasNext()) { String name = (String) y.next(); String[] values = ((HttpServletRequest) request).getParameterValues(name); // See if the name contains a forbidden pattern. if (!checkAllowsAndDenies(name, request, response, valveContext)) { return false; } // Check the parameter's values for the pattern. if (values != null) { for (int i = 0; i < values.length; i++) { String value = values[i]; if (!checkAllowsAndDenies(value, request, response, valveContext)) { return false; } } } } // The request should continue. return true; } /** * Perform the filtering that has been configured for this Valve, matching * against the specified request property. If the request is allowed to * proceed, this method returns true. Otherwise, this method sends * a Forbidden error response page, and returns false. * * <br><br> * * This method borrows heavily from RequestFilterValve.process(), * only this method has a boolean return type and doesn't call * valveContext.invokeNext(). * * @param property The request property on which to filter * @param request The servlet request to be processed * @param response The servlet response to be processed * @param context The valve context used to invoke the next valve * in the current processing pipeline * * @exception IOException if an input/output error occurs * @exception ServletException if a servlet error occurs * * @return true if the request is still allowed to proceed. */ public boolean checkAllowsAndDenies(String property, Request request, Response response, ValveContext valveContext) throws IOException, ServletException { // Check the deny patterns, if any for (int i = 0; i < denies.length; i++) { if (denies[i].match(property)) { ServletResponse sres = response.getResponse(); if (sres instanceof HttpServletResponse) { HttpServletResponse hres = (HttpServletResponse) sres; hres.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } } } // Check the allow patterns, if any for (int i = 0; i < allows.length; i++) { if (allows[i].match(property)) { return true; } } // Allow if denies specified but not allows if ((denies.length > 0) && (allows.length == 0)) { return true; } // Deny this request ServletResponse sres = response.getResponse(); if (sres instanceof HttpServletResponse) { HttpServletResponse hres = (HttpServletResponse) sres; hres.sendError(HttpServletResponse.SC_FORBIDDEN); } return false; } /** * Filters all existing parameters for potentially dangerous content, * and escapes any if they are found. * * @param escapes A HashMap containing substitution regex data. * @param request The Request that contains the parameters. */ public void filterParameters(HashMap subs, Request request) { ParameterMap paramMap = (ParameterMap) ((HttpServletRequest) request).getParameterMap(); // Unlock the parameters map so we can modify the parameters. paramMap.setLocked(false); try { // Loop through each of the substitution patterns. Iterator x = subs.keySet().iterator(); while (x.hasNext()) { String pattern = (String) x.next(); RE r = new RE(pattern); // Loop through the list of parameters. Iterator y = paramMap.keySet().iterator(); while (y.hasNext()) { String name = (String) y.next(); String[] values = ((HttpServletRequest) request).getParameterValues(name); // See if the name contains the pattern. boolean nameMatch; synchronized (r) { nameMatch = r.match(name); } if (nameMatch) { // The parameter's name matched a pattern, so we // fix it by modifying the name, adding the parameter // back as the new name, and removing the old one. String newName; synchronized (r) { newName = r.subst(name, (String) subs.get(pattern)); } ((HttpRequest) request).addParameter(newName, values); paramMap.remove(name); log("Parameter name " + name + " matched pattern \"" + pattern + "\". Remote addr: " + ((HttpServletRequest) request).getRemoteAddr()); } // Check the parameter's values for the pattern. if (values != null) { for (int i = 0; i < values.length; i++) { String value = values[i]; boolean valueMatch; synchronized (r) { valueMatch = r.match(value); } if (valueMatch) { // The value matched, so we modify the value // and then set it back into the array. String newValue; synchronized (r) { newValue = r.subst(value, (String) subs.get(pattern)); } values[i] = newValue; ((HttpRequest) request).addParameter( name, values); log("Parameter \"" + name + "\"'s value \"" + value + "\" matched pattern \"" + pattern + "\". Remote addr: " + ((HttpServletRequest) request).getRemoteAddr()); } } } } } } catch (Exception e) { e.printStackTrace(); } finally { // Make sure the parameters map is locked again when we're done. paramMap.setLocked(true); } } /** * Return a text representation of this object. */ public String toString() { return ("BadInputFilterValve[container=" + container.getName() + ']'); } /** * Log the specified message to our current Logger (if any). * * @param message Message to be logged */ protected void log(String message) { Logger logger = container.getLogger(); if (logger != null) logger.log(this.toString() + ": " + message); else System.out.println(this.toString() + ": " + message); } }