Modify request parameter with servlet filter

JavaServlet Filters

Java Problem Overview


An existing web application is running on Tomcat 4.1. There is an XSS issue with a page, but I can't modify the source. I've decided to write a servlet filter to sanitize the parameter before it is seen by the page.

I would like to write a Filter class like this:

import java.io.*;
import javax.servlet.*;

public final class XssFilter implements Filter {

  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException
  {
    String badValue = request.getParameter("dangerousParamName");
    String goodValue = sanitize(badValue);
    request.setParameter("dangerousParamName", goodValue);
    chain.doFilter(request, response);
  }

  public void destroy() {
  }

  public void init(FilterConfig filterConfig) {
  }
}

But ServletRequest.setParameter doesn't exist.

How can I change the value of the request parameter before passing the request down the chain?

Java Solutions


Solution 1 - Java

As you've noted HttpServletRequest does not have a setParameter method. This is deliberate, since the class represents the request as it came from the client, and modifying the parameter would not represent that.

One solution is to use the HttpServletRequestWrapper class, which allows you to wrap one request with another. You can subclass that, and override the getParameter method to return your sanitized value. You can then pass that wrapped request to chain.doFilter instead of the original request.

It's a bit ugly, but that's what the servlet API says you should do. If you try to pass anything else to doFilter, some servlet containers will complain that you have violated the spec, and will refuse to handle it.

A more elegant solution is more work - modify the original servlet/JSP that processes the parameter, so that it expects a request attribute instead of a parameter. The filter examines the parameter, sanitizes it, and sets the attribute (using request.setAttribute) with the sanitized value. No subclassing, no spoofing, but does require you to modify other parts of your application.

Solution 2 - Java

For the record, here is the class I ended up writing:

import java.io.IOException;

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;

public final class XssFilter implements Filter {

	static class FilteredRequest extends HttpServletRequestWrapper {

		/* These are the characters allowed by the Javascript validation */
		static String allowedChars = "+-0123456789#*";

		public FilteredRequest(ServletRequest request) {
			super((HttpServletRequest)request);
		}

		public String sanitize(String input) {
			String result = "";
			for (int i = 0; i < input.length(); i++) {
				if (allowedChars.indexOf(input.charAt(i)) >= 0) {
					result += input.charAt(i);
				}
			}
			return result;
		}

		public String getParameter(String paramName) {
			String value = super.getParameter(paramName);
			if ("dangerousParamName".equals(paramName)) {
				value = sanitize(value);
			}
			return value;
		}
		
		public String[] getParameterValues(String paramName) {
			String values[] = super.getParameterValues(paramName);
			if ("dangerousParamName".equals(paramName)) {
				for (int index = 0; index < values.length; index++) {
					values[index] = sanitize(values[index]);
				}
			}
			return values;
		}
	}

	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		chain.doFilter(new FilteredRequest(request), response);
	}

	public void destroy() {
	}

	public void init(FilterConfig filterConfig) {
	}
}

Solution 3 - Java

Write a simple class that subcalsses HttpServletRequestWrapper with a getParameter() method that returns the sanitized version of the input. Then pass an instance of your HttpServletRequestWrapper to Filter.doChain() instead of the request object directly.

Solution 4 - Java

Based on all your remarks here is my proposal that worked for me :

 private final class CustomHttpServletRequest extends HttpServletRequestWrapper {

    private final Map<String, String[]> queryParameterMap;
    private final Charset requestEncoding;

    public CustomHttpServletRequest(HttpServletRequest request) {
        super(request);
        queryParameterMap = getCommonQueryParamFromLegacy(request.getParameterMap());

        String encoding = request.getCharacterEncoding();
        requestEncoding = (encoding != null ? Charset.forName(encoding) : StandardCharsets.UTF_8);
    }

    private final Map<String, String[]> getCommonQueryParamFromLegacy(Map<String, String[]> paramMap) {
        Objects.requireNonNull(paramMap);

        Map<String, String[]> commonQueryParamMap = new LinkedHashMap<>(paramMap);

        commonQueryParamMap.put(CommonQueryParams.PATIENT_ID, new String[] { paramMap.get(LEGACY_PARAM_PATIENT_ID)[0] });
        commonQueryParamMap.put(CommonQueryParams.PATIENT_BIRTHDATE, new String[] { paramMap.get(LEGACY_PARAM_PATIENT_BIRTHDATE)[0] });
        commonQueryParamMap.put(CommonQueryParams.KEYWORDS, new String[] { paramMap.get(LEGACY_PARAM_STUDYTYPE)[0] });

        String lowerDateTime = null;
        String upperDateTime = null;

        try {
            String studyDateTime = new SimpleDateFormat("yyyy-MM-dd").format(new SimpleDateFormat("dd-MM-yyyy").parse(paramMap.get(LEGACY_PARAM_STUDY_DATE_TIME)[0]));

            lowerDateTime = studyDateTime + "T23:59:59";
            upperDateTime = studyDateTime + "T00:00:00";

        } catch (ParseException e) {
            LOGGER.error("Can't parse StudyDate from query parameters : {}", e.getLocalizedMessage());
        }

        commonQueryParamMap.put(CommonQueryParams.LOWER_DATETIME, new String[] { lowerDateTime });
        commonQueryParamMap.put(CommonQueryParams.UPPER_DATETIME, new String[] { upperDateTime });

        legacyQueryParams.forEach(commonQueryParamMap::remove);
        return Collections.unmodifiableMap(commonQueryParamMap);

    }

    @Override
    public String getParameter(String name) {
        String[] params = queryParameterMap.get(name);
        return params != null ? params[0] : null;
    }

    @Override
    public String[] getParameterValues(String name) {
        return queryParameterMap.get(name);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
            return queryParameterMap; // unmodifiable to uphold the interface contract.
        }

        @Override
        public Enumeration<String> getParameterNames() {
            return Collections.enumeration(queryParameterMap.keySet());
        }

        @Override
        public String getQueryString() {
            // @see : https://stackoverflow.com/a/35831692/9869013
            // return queryParameterMap.entrySet().stream().flatMap(entry -> Stream.of(entry.getValue()).map(value -> entry.getKey() + "=" + value)).collect(Collectors.joining("&")); // without encoding !!
            return queryParameterMap.entrySet().stream().flatMap(entry -> encodeMultiParameter(entry.getKey(), entry.getValue(), requestEncoding)).collect(Collectors.joining("&"));
        }

        private Stream<String> encodeMultiParameter(String key, String[] values, Charset encoding) {
            return Stream.of(values).map(value -> encodeSingleParameter(key, value, encoding));
        }

        private String encodeSingleParameter(String key, String value, Charset encoding) {
            return urlEncode(key, encoding) + "=" + urlEncode(value, encoding);
        }

        private String urlEncode(String value, Charset encoding) {
            try {
                return URLEncoder.encode(value, encoding.name());
            } catch (UnsupportedEncodingException e) {
                throw new IllegalArgumentException("Cannot url encode " + value, e);
            }
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            throw new UnsupportedOperationException("getInputStream() is not implemented in this " + CustomHttpServletRequest.class.getSimpleName() + " wrapper");
        }

    }

note : queryString() requires to process ALL the values for each KEY and don't forget to encodeUrl() when adding your own param values, if required

As a limitation, if you call request.getParameterMap() or any method that would call request.getReader() and begin reading, you will prevent any further calls to request.setCharacterEncoding(...)

Solution 5 - Java

I had the same problem (changing a parameter from the HTTP request in the Filter). I ended up by using a ThreadLocal<String>. In the Filter I have:

class MyFilter extends Filter {
    public static final ThreadLocal<String> THREAD_VARIABLE = new ThreadLocal<>();
    public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
        THREAD_VARIABLE.set("myVariableValue");
        chain.doFilter(request, response);
    }
}

In my request processor (HttpServlet, JSF controller or any other HTTP request processor), I get the current thread value back:

...
String myVariable = MyFilter.THREAD_VARIABLE.get();
...

Advantages:

  • more versatile than passing HTTP parameters (you can pass POJO objects)
  • slightly faster (no need to parse the URL to extract the variable value)
  • more elegant thant the HttpServletRequestWrapper boilerplate
  • the variable scope is wider than just the HTTP request (the scope you have when doing request.setAttribute(String,Object), i.e. you can access the variable in other filtrers.

Disadvantages:

  • You can use this method only when the thread which process the filter is the same as the one which process the HTTP request (this is the case in all Java-based servers I know). Consequently, this will not work when
    • doing a HTTP redirect (because the browser does a new HTTP request and there is no way to guarantee that it will be processed by the same thread)
    • processing data in separate threads, e.g. when using java.util.stream.Stream.parallel, java.util.concurrent.Future, java.lang.Thread.
  • You must be able to modify the request processor/application

Some side notes:

  • The server has a Thread pool to process the HTTP requests. Since this is pool:
  1. a Thread from this thread pool will process many HTTP requests, but only one at a time (so you need either to cleanup you variable after usage or to define it for each HTTP request = pay attention to code such as if (value!=null) { THREAD_VARIABLE.set(value);} because you will reuse the value from the previous HTTP request when value is null : side effects are guaranteed).
  2. There is no guarantee that two requests will be processed by the same thread (it may be the case but you have no guarantee). If you need to keep user data from one request to another request, it would be better to use HttpSession.setAttribute()
  • The JEE @RequestScoped internally uses a ThreadLocal, but using the ThreadLocal is more versatile: you can use it in non JEE/CDI containers (e.g. in multithreaded JRE applications)

Solution 6 - Java

This is what i ended up doing

//import ../../Constants;

public class RequestFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(RequestFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {
        try {
            CustomHttpServletRequest customHttpServletRequest = new CustomHttpServletRequest((HttpServletRequest) servletRequest);
            filterChain.doFilter(customHttpServletRequest, servletResponse);
        } finally {
            //do something here
        }
    }

    

    @Override
    public void destroy() {

    }

     public static Map<String, String[]> ADMIN_QUERY_PARAMS = new HashMap<String, String[]>() {
        {
            put("diagnostics", new String[]{"false"});
            put("skipCache", new String[]{"false"});
        }
    };

    /*
        This is a custom wrapper over the `HttpServletRequestWrapper` which 
        overrides the various header getter methods and query param getter methods.
        Changes to the request pojo are
        => A custom header is added whose value is a unique id
        => Admin query params are set to default values in the url
    */
    private class CustomHttpServletRequest extends HttpServletRequestWrapper {
        public CustomHttpServletRequest(HttpServletRequest request) {
            super(request);
            //create custom id (to be returned) when the value for a
            //particular header is asked for
            internalRequestId = RandomStringUtils.random(10, true, true) + "-local";
        }

        public String getHeader(String name) {
            String value = super.getHeader(name);
            if(Strings.isNullOrEmpty(value) && isRequestIdHeaderName(name)) {
                value = internalRequestId;
            }
            return value;
        }

        private boolean isRequestIdHeaderName(String name) {
            return Constants.RID_HEADER.equalsIgnoreCase(name) || Constants.X_REQUEST_ID_HEADER.equalsIgnoreCase(name);
        }

        public Enumeration<String> getHeaders(String name) {
            List<String> values = Collections.list(super.getHeaders(name));
            if(values.size()==0 && isRequestIdHeaderName(name)) {
                values.add(internalRequestId);
            }
            return Collections.enumeration(values);
        }

        public Enumeration<String> getHeaderNames() {
            List<String> names = Collections.list(super.getHeaderNames());
            names.add(Constants.RID_HEADER);
            names.add(Constants.X_REQUEST_ID_HEADER);
            return Collections.enumeration(names);
        }

        public String getParameter(String name) {
            if (ADMIN_QUERY_PARAMS.get(name) != null) {
                return ADMIN_QUERY_PARAMS.get(name)[0];
            }
            return super.getParameter(name);
        }

        public Map<String, String[]> getParameterMap() {
            Map<String, String[]> paramsMap = new HashMap<>(super.getParameterMap());
            for (String paramName : ADMIN_QUERY_PARAMS.keySet()) {
                if (paramsMap.get(paramName) != null) {
                    paramsMap.put(paramName, ADMIN_QUERY_PARAMS.get(paramName));
                }
            }
            return paramsMap;
        }

        public String[] getParameterValues(String name) {
            if (ADMIN_QUERY_PARAMS.get(name) != null) {
                return ADMIN_QUERY_PARAMS.get(name);
            }
            return super.getParameterValues(name);
        }

        public String getQueryString() {
            Map<String, String[]> map = getParameterMap();
            StringBuilder builder = new StringBuilder();
            for (String param: map.keySet()) {
                for (String value: map.get(param)) {
                    builder.append(param).append("=").append(value).append("&");
                }
            }
            builder.deleteCharAt(builder.length() - 1);
            return builder.toString();
        }
    }
}

Solution 7 - Java

You can use Regular Expression for Sanitization. Inside filter before calling chain.doFilter(request, response) method, call this code. Here is Sample Code:

for (Enumeration en = request.getParameterNames(); en.hasMoreElements(); ) {
String name = (String)en.nextElement();
String values[] = request.getParameterValues(name);
int n = values.length;
    for(int i=0; i < n; i++) {
     values[i] = values[i].replaceAll("[^\\dA-Za-z ]","").replaceAll("\\s+","+").trim();   
    }
}

Solution 8 - Java

Try request.setAttribute("param",value);. It worked fine for me.

Please find this code sample:

private void sanitizePrice(ServletRequest request){
		if(request.getParameterValues ("price") !=  null){
			String price[] = request.getParameterValues ("price");
			
			for(int i=0;i<price.length;i++){
				price[i] = price[i].replaceAll("[^\\dA-Za-z0-9- ]", "").trim();
				System.out.println(price[i]);
			}
			request.setAttribute("price", price);
			//request.getParameter("numOfBooks").re
		}
	}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJeremy SteinView Question on Stackoverflow
Solution 1 - JavaskaffmanView Answer on Stackoverflow
Solution 2 - JavaJeremy SteinView Answer on Stackoverflow
Solution 3 - JavaAsaphView Answer on Stackoverflow
Solution 4 - JavabenGiacomoView Answer on Stackoverflow
Solution 5 - JavaJulien KroneggView Answer on Stackoverflow
Solution 6 - Javaagent47View Answer on Stackoverflow
Solution 7 - JavaAjinkya PeshaveView Answer on Stackoverflow
Solution 8 - JavaAnkur BagView Answer on Stackoverflow