Matt
Matt

Reputation: 81

Use servlet filter to remove a form parameter from posted data

A vendor has been posting XML data over HTTPS within a form variable named XMLContent to my Coldfusion application server. I recently moved to a more recent version of the application server and those requests are throwing 500 server errors. It is throwing the error because a second form parameter's content is not properly urlencoded, but I don't need that parameter anyway. (I contacted the vendor to fix this but they are forcing me to pay to fix their mistake so I'm looking to fix it myself if possible.)

How would I utilize a servlet filter to remove all but the form parameter named: XMLContent I have tried various attempts to explicitly remove the offending parameter "TContent" but it never gets removed.

A snippet of the data being received:

XMLContent=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%0A%3CCheck+xmlns%3D%22http .........&TContent=<!--?xml version="1.0" encoding="UTF-8"?--><check xmlns="http...........

The code I've tried:

import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;


import java.util.*;



public class MultipartFilter implements Filter {

// Init ----------------------------------------------------------------

  public FilterConfig filterConfig;

// Actions -------------------------------------------------------------


public void init(FilterConfig filterConfig) throws ServletException {
    this.filterConfig = filterConfig;
}

/**
 * Check the type request and if it is a HttpServletRequest, then parse the request.
 * @throws ServletException If parsing of the given HttpServletRequest fails.
 * @see javax.servlet.Filter#doFilter(
 *      javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
 */
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws ServletException, IOException
{
    // Check type request.
    if (request instanceof HttpServletRequest) {
        // Cast back to HttpServletRequest.
        HttpServletRequest httpRequest = (HttpServletRequest) request;

        // Parse HttpServletRequest.
        HttpServletRequest parsedRequest = parseRequest(httpRequest);

        // Continue with filter chain.
        chain.doFilter(parsedRequest, response);
    } else {
        // Not a HttpServletRequest.
        chain.doFilter(request, response);
    }
}

/**
 * @see javax.servlet.Filter#destroy()
 */
public void destroy() {
    this.filterConfig = null;
}



private HttpServletRequest parseRequest(HttpServletRequest request) throws ServletException {

    // Prepare the request parameter map.
    Map<String, String[]> parameterMap = new HashMap<String, String[]>();

    // Loop through form parameters.
Enumeration<String> parameterNames = request.getParameterNames();

    while (parameterNames.hasMoreElements()) {
    String paramName = parameterNames.nextElement();
    String[] paramValues = request.getParameterValues(paramName);

            // Add just the XMLContent form parameter
    if (paramName.equalsIgnoreCase("xmlcontent")) {   

        parameterMap.put(paramName, new String[] { paramValues[0] });

    }
}

    // Wrap the request with the parameter map which we just created and return it.
    return wrapRequest(request, parameterMap);
}



// Utility (may be refactored to public utility class) ---------------

/**
 * Wrap the given HttpServletRequest with the given parameterMap.
 * @param request The HttpServletRequest of which the given parameterMap have to be wrapped in.
 * @param parameterMap The parameterMap to be wrapped in the given HttpServletRequest.
 * @return The HttpServletRequest with the parameterMap wrapped in.
 */
private static HttpServletRequest wrapRequest(
    HttpServletRequest request, final Map<String, String[]> parameterMap)
{
    return new HttpServletRequestWrapper(request) {
        public Map<String, String[]> getParameterMap() {
            return parameterMap;
        }
        public String[] getParameterValues(String name) {
            return parameterMap.get(name);
        }
        public String getParameter(String name) {
            String[] params = getParameterValues(name);
            return params != null && params.length > 0 ? params[0] : null;
        }
        public Enumeration<String> getParameterNames() {
            return Collections.enumeration(parameterMap.keySet());
        }
    };
  }
}

Upvotes: 8

Views: 12817

Answers (2)

Glen Best
Glen Best

Reputation: 23115

Approach

The code follows the correct approach:

  • in wrapRequest(), it instantiates HttpServletRequestWrapper and overrides the 4 methods that trigger request parsing:

    • public String getParameter(String name)
    • public Map<String, String[]> getParameterMap()
    • public Enumeration<String> getParameterNames()
    • public String[] getParameterValues(String name)
  • the doFilter() method invokes the filter chain using the wrapped request, meaning subsequent filters, plus the target servlet (URL-mapped) will be supplied the wrapped request.

Problem

  • Calling any of the 4 methods getParameterXXX() methods on the underlying 'original request' triggers implicit parsing of all request parameters (via a server internal method such as Request.parseRequestParameters or parsePameters). These 4 methods are the only way to trigger such parsing.
  • Before the request is wrapped, in parseRequest(), your code calls such methods on the underlying request:

    request.getParameterNames();
    
    request.getParameterValues(paramName);
    

Solution

You need to control the parsing logic. Reading raw bytes from the input stream and doing your own URL decoding is too complex - it would mean replacing a large volume of tightly-coupled server code. Instead, the simplest approach is to just replace the method that does the actual URL decoding. That means you can leave in place the request.getParameterXXX calls mentioned in prev section.

Not sure what server you're hosting ColdFusion on, but following description is based on Glassfish/Tomcat, but can be adapted. At the bottom of this post is the internal request parsing method from Glassfish (a modified version of Tomcat). JD-decompiler is useful for this tinkering for converting .class files to .java.

  1. Find the class that does URL decoding (for Glassfish this is com.sun.grizzly.util.http.Parameters from grizzly-utils.jar as shown below, for Tomcat this is org.apache.tomcat.util.http.Parameters from tomcat-coyote.jar)
  2. Copy the entire source code to a new class somepackage.StrippedParameters
  3. Find the line of code that decodes the parameter value (below: value = urlDecode(this.tmpValue))
  4. Change this code so that it only decodes when the parameter name matches your desired parameter:

    if (decodeName)
      name = urlDecode(this.tmpName);
    else
      name = this.tmpName.toString();
    // 
    // !! THIS IF STATEMENT ADDED TO ONLY DECODE DESIRED PARAMETERS, 
    // !! OTHERS ARE STRIPPED:
    //
    if ("XMLContent".equals(name)) {
        String value;
        String value;
        if (decodeValue)
          value = urlDecode(this.tmpValue);
        else {
          value = this.tmpValue.toString();
        }
        try
        {
          addParameter(name, value);
        }
        catch (IllegalStateException ise)
        {
          logger.warning(ise.getMessage());
          break;
        }
    }
    
  5. Now the tricky part: replace the default class Parameters with your class StrippedParmeters just before URL decoding occurs. Once the parameters are obtained, copy them back to the container class. For Glassfish copy the method parseRequestParameters into your implementation of HttpServletRequestWrapper (for Tomcat, the corresponding method is parseParameters in class org.apache.catalina.connector.Request in catalina.jar)

    • replace this line:

      Parameters parameters = this.coyoteRequest.getParameters();
      

      with:

      Parameters parameters = new somepackage.StrippedParameters();
      
    • at the bottom of the method, add this:

      Parameters coyoteParameters = this.coyoteRequest.getParameters();
      for (String paramName : parameters.getParameterNames()) {
          String paramValue = parameters.getParameterValue(paramName);
          coyoteParameters.addParameter(paramName, paramValue);
      }
      

Glassfish Container Code - Modified Version of Tomcat, with Grizzly replacing Coyote (Catalina Servlet Engine still refers to Coyote objects, but they are Grizzly instances mocked like Coyote)

package org.apache.catalina.connector;
....
public class Request  implements HttpRequest, HttpServletRequest {
    ....
    protected com.sun.grizzly.tcp.Request coyoteRequest;
    ....    
    // This is called from the 4 methods named 'getParameterXXX'
    protected void parseRequestParameters()  {
        Parameters parameters = this.coyoteRequest.getParameters();
        parameters.setLimit(getConnector().getMaxParameterCount());
        String enc = getCharacterEncoding();
        this.requestParametersParsed = true;
        if (enc != null) {
            parameters.setEncoding(enc);
            parameters.setQueryStringEncoding(enc);
        } else {
            parameters.setEncoding("ISO-8859-1");
            parameters.setQueryStringEncoding("ISO-8859-1");
        }
        parameters.handleQueryParameters();
        if ((this.usingInputStream) || (this.usingReader)) {
            return;
        }
        if (!getMethod().equalsIgnoreCase("POST")) {
            return;
        }
        String contentType = getContentType();
        if (contentType == null) {
            contentType = "";
        }
        int semicolon = contentType.indexOf(';');
        if (semicolon >= 0)
            contentType = contentType.substring(0, semicolon).trim();
        else {
            contentType = contentType.trim();
        }
        if ((isMultipartConfigured()) && ("multipart/form-data".equals(contentType)))  {
            getMultipart().init();
        }
        if (!"application/x-www-form-urlencoded".equals(contentType)) {
            return;
        }
        int len = getContentLength();
        if (len > 0) {
        int maxPostSize = ((Connector)this.connector).getMaxPostSize();
        if ((maxPostSize > 0) && (len > maxPostSize)) {
            log(sm.getString("coyoteRequest.postTooLarge"));
            throw new IllegalStateException("Post too large");
        }
        try {
            byte[] formData = getPostBody();
            if (formData != null)
                parameters.processParameters(formData, 0, len);
        } catch (Throwable t) {
        }
    }
  }
}

package com.sun.grizzly.tcp;

import com.sun.grizzly.util.http.Parameters;
public class Request {
    ....
    private Parameters parameters = new Parameters();
    ....
    public Parameters getParameters() {
        return this.parameters;
    }
}

package com.sun.grizzly.util.http;

public class Parameters {
    ....
    public void processParameters(byte[] bytes, int start, int len) {
        processParameters(bytes, start, len, getCharset(this.encoding));
    }

    public void processParameters(byte[] bytes, int start, int len, Charset charset) {
        if (debug > 0) {
        try {
            log(sm.getString("parameters.bytes", new String(bytes, start, len, "ISO-8859-1")));
        } catch (UnsupportedEncodingException e) {
            logger.log(Level.SEVERE, sm.getString("parameters.convertBytesFail"), e);
        }
    }
    int decodeFailCount = 0;
    int end = start + len;
    int pos = start;
    while (pos < end) {
        int nameStart = pos;
        int nameEnd = -1;
        int valueStart = -1;
        int valueEnd = -1;
        boolean parsingName = true;
        boolean decodeName = false;
        boolean decodeValue = false;
        boolean parameterComplete = false;
        do {
            switch (bytes[pos]) {
              case 61:
                  if (parsingName) {
                      nameEnd = pos;
                      parsingName = false;
                      pos++; valueStart = pos;
                  } else {
                      pos++;
                  }
                  break;
              case 38:
                  if (parsingName) {
                      nameEnd = pos;
                  } else {
                      valueEnd = pos;
                  }
                  parameterComplete = true;
                  pos++;
                  break;
              case 37:
              case 43:
                  if (parsingName)
                      decodeName = true;
                  else {
                      decodeValue = true;
                  }
                  pos++;
                  break;
              default:
                  pos++;
            }
        } while ((!parameterComplete) && (pos < end));
        if (pos == end) {
            if (nameEnd == -1)
                nameEnd = pos;
            else if ((valueStart > -1) && (valueEnd == -1)) {
                valueEnd = pos;
            }
        }
        if ((debug > 0) && (valueStart == -1)) {
            try {
                log(sm.getString("parameters.noequal", Integer.valueOf(nameStart),
                Integer.valueOf(nameEnd), 
                new String(bytes, nameStart, nameEnd - nameStart, "ISO-8859-1")));
            } catch (UnsupportedEncodingException e) {
                logger.log(Level.SEVERE, sm.getString("parameters.convertBytesFail"), e);
            }
        }

        if (nameEnd <= nameStart) {
            if (logger.isLoggable(Level.INFO)) {
                if (valueEnd >= nameStart)
                    try {
                        new String(bytes, nameStart, valueEnd - nameStart, "ISO-8859-1");
                    } catch (UnsupportedEncodingException e) {
                        logger.log(Level.SEVERE,
                                   sm.getString("parameters.convertBytesFail"), e);
                    } else {
                        logger.fine(sm.getString("parameters.invalidChunk", 
                                Integer.valueOf(nameStart), Integer.valueOf(nameEnd), null));
                    }
                }
            } else {
                this.tmpName.setCharset(charset);
                this.tmpValue.setCharset(charset);
                this.tmpName.setBytes(bytes, nameStart, nameEnd - nameStart);
                this.tmpValue.setBytes(bytes, valueStart, valueEnd - valueStart);
    if (debug > 0)
      try {
        this.origName.append(bytes, nameStart, nameEnd - nameStart);
        this.origValue.append(bytes, valueStart, valueEnd - valueStart);
      }
      catch (IOException ioe) {
        logger.log(Level.SEVERE, sm.getString("parameters.copyFail"), ioe);
      }
    try
    {
      String name;
      String name;
      if (decodeName)
        name = urlDecode(this.tmpName);
      else
        name = this.tmpName.toString();
      String value;
      String value;
      if (decodeValue)
        value = urlDecode(this.tmpValue);
      else {
        value = this.tmpValue.toString();
      }
      try
      {
        addParameter(name, value);
      }
      catch (IllegalStateException ise)
      {
        logger.warning(ise.getMessage());
        break;
      }
    } catch (IOException e) {
      decodeFailCount++;
      if ((decodeFailCount == 1) || (debug > 0)) {
        if (debug > 0) {
          log(sm.getString("parameters.decodeFail.debug", this.origName.toString(), this.origValue.toString()), e);
        }
        else if (logger.isLoggable(Level.INFO)) {
          logger.log(Level.INFO, sm.getString("parameters.decodeFail.info", this.tmpName.toString(), this.tmpValue.toString()), e);
        }
      }

    }

    this.tmpName.recycle();
    this.tmpValue.recycle();

    if (debug > 0) {
      this.origName.recycle();
      this.origValue.recycle();
    }
  }
}
if ((decodeFailCount > 1) && (debug <= 0))
  logger.info(sm.getString("parameters.multipleDecodingFail", Integer.valueOf(decodeFailCount)));

}

Upvotes: 5

ajgautam
ajgautam

Reputation: 86

We face these situations every day while handling locale. If user locale is different than browser locale, we have to update the request. But its not mutable.

Solution: create a request wrapper class. Copy existing request parameters (the ones you want) into it and pass this wrapper class in filterchain.doFilter()

sample wrapper class:

public class WrappedRequest extends HttpServletRequestWrapper
{
  Map<String, String[]> parameterMap;
  public WrappedRequest(final HttpServletRequest request)
  {
   //build your param Map here with required values
  }

  @Override
  public Map<String, String[]> getParameterMap()
  {
    //return local param map
  }

  //override other methods if needed.

}

Now in your filter code, do following.

wrapRequest = new WrappedRequest(hreq);
filterChain.doFilter(wrapRequest, servletResponse);

Hopefully it will solve your problem.

Upvotes: 6

Related Questions