I'm trying to filter/modify Post and Put calls to make sure all parameters provided by the user are filtered from HTML and JS code to prevent XSS attacks. I would like to make sure this is implemented at the API level so no matter what client is being used, it will be protected.
With Jersey 1.x, this was possible by implementing ContainerRequestFilter and modifying request.getQueryParameters() before they are matched with the requested servlets. Example: http://codehustler.org/blog/jersey-cross-site-scripting-xss-filter-for-java-web-apps/
With Jersey 2 however, this is not possible by implementing the same interface since we can no longer getQueryParameters() or getPathParameters(), but instead, we are only able to getUriInfo(), but then it's useless since the query parameters are immutable. I looked into Jersey's Filters and Interceptors but unfortunately they are limited to giving access to the headers and maybe cookies.
I spent a lot of time researching but I couldn't find what I'm looking for.
Is there an alternative way to filter path and query parameters? Is there anything I'm missing?
Thanks!
I've added a filter below that works with Jersey 2.x. However, it doesn't perform the XSS fixing for Cookies as I haven't found a way to modify those.
Important to note that this needs to be used in combination with @SafeHtml on POJO properties in order to clean up those values.
@PreMatching
public class XSSFilter implements ContainerRequestFilter
{
/**
* @see ContainerRequestFilter#filter(ContainerRequest)
*/
@Override
public void filter( ContainerRequestContext request )
{
cleanQueryParams( request );
cleanHeaders( request.getHeaders() );
}
/**
* Replace the existing query parameters with ones stripped of XSS vulnerabilities
* @param request
*/
private void cleanQueryParams( ContainerRequestContext request )
{
UriBuilder builder = request.getUriInfo().getRequestUriBuilder();
MultivaluedMap<String, String> queries = request.getUriInfo().getQueryParameters();
for( Map.Entry<String, List<String>> query : queries.entrySet() )
{
String key = query.getKey();
List<String> values = query.getValue();
builder.replaceQueryParam( key );
for( String value : values ) {
builder.replaceQueryParam( key, Utils.stripXSS( value ) );
}
}
request.setRequestUri( builder.build() );
}
/**
* Replace the existing headers with ones stripped of XSS vulnerabilities
* @param headers
*/
private void cleanHeaders( MultivaluedMap<String, String> headers )
{
for( Map.Entry<String, List<String>> header : headers.entrySet() )
{
String key = header.getKey();
List<String> values = header.getValue();
List<String> cleanValues = new ArrayList<String>();
for( String value : values ) {
cleanValues.add( Utils.stripXSS( value ) );
}
headers.put( key, cleanValues );
}
}
}
The stripXSS functions are the following:
/**
* Strips any potential XSS threats out of the value
*
* @param value
* @return
*/
public static String stripXSS( String value )
{
return stripXSS( value, Whitelist.none() );
}
/**
* Strips any potential XSS threats out of the value excluding
* the white listed HTML
*
* @param value
* @param whitelist
* @return
*/
public static String stripXSS( String value, Whitelist whitelist )
{
if( StringUtils.isBlank( value ) )
return value;
// Use the ESAPI library to avoid encoded attacks.
value = ESAPI.encoder().canonicalize( value );
// Avoid null characters
value = value.replaceAll("\0", "");
// Clean out HTML
Document.OutputSettings outputSettings = new Document.OutputSettings();
outputSettings.escapeMode( EscapeMode.xhtml );
outputSettings.prettyPrint( false );
value = Jsoup.clean( value, "", whitelist, outputSettings );
return value;
}
Also updated the original post: http://codehustler.org/blog/jersey-cross-site-scripting-xss-filter-for-java-web-apps/
You can use a ContainerRequestFilter, construct a new URI (based on the existing one), and set the URI in the ContainerRequestContext via the setRequestUri method.
@PreMatching
public class MyFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
UriBuilder builder = requestContext.getUriInfo().getRequestUriBuilder();
// Replace a query param
builder.replaceQueryParam("foo", "bar");
// Remove a query param
builder.replaceQueryParam("baz");
// Replace path
builder.replacePath("newPath");
requestContext.setRequestUri(builder.build());
}
}
With this approach however, I haven't found a way to replace individual path params based on param names from the matched Jersey resource's URI template, since "setRequestUri" is only allowed in the pre resource matching phase. So the whole path will need to be replaced.