/*
* $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);
}
}
|