PDF Attack Filter for Java EE

Revision as of 23:43, 4 January 2007 by Jeff Williams (talk | contribs)

Jump to: navigation, search


This is a filter to block XSS attacks on PDF files served by Java EE applications. The details of the attack are discussed elsewhere. This filter implements a simple algorithm suggested by Amit Klein.


This attack relies on having some javascript in an anchor after the url like this: http://www.site.com/file.pdf#blah=javascript:alert(document.cookie);

We're going to use a Java EE filter to intercept requests before they reach our application. We could have just stripped off the anchor part of the URL, but that's not how HTTP works. The anchor isn't actually sent to the application, so we have to get much trickier.

We're going to use a redirect to set the browser's URL to the same URL without the anchor, thus preventing the attack. But we have to be able to tell the difference between the first request, and the redirected request. So we're going to add a temporary token to the URL, which we'll verify when it arrives. We don't want an attacker forging one of these tokens, so we're going to encrypt the user's source IP address along with a timestamp.


The source code (one file) and the compiled class file are in a single zip file.



The first step is to add the filter to our application. All we have to do is put the PDFAttackFilter class on our application's classpath, probably by putting it in the classes folder in WEB-INF. You can extract the class file from the

Then we just have to add the following to our web.xml. You should paste this in right above your servlet definitions. You'll want to change the mapping so that it only applies to URL's that serve a PDF file. You could use /*.pdf, but you may have servlets that stream PDF files that down't end in .pdf.


Source Code

This code has been only minimally tested. Please help us verify the approach and the implementation used here.

  package org.owasp.filters;

  import java.io.IOException;

  import javax.crypto.Cipher;
  import javax.crypto.SecretKey;
  import javax.crypto.SecretKeyFactory;
  import javax.crypto.spec.PBEParameterSpec;
  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.HttpServletResponse;

  public class PDFAttackFilter implements Filter 

	private static sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
	private static sun.misc.BASE64Encoder encoder = new sun.misc.BASE64Encoder();
	private static byte[] salt = { (byte) 0x23, (byte) 0x3f, (byte) 0x28, (byte) 0x00, (byte) 0x11, (byte) 0xc2, (byte) 0xd1, (byte) 0xff };
	private static PBEParameterSpec ps = new PBEParameterSpec( salt, 20 );
	private static SecretKey secretKey;
	private static int timeoutSeconds = 10;
	private static String tokenName = "PDFAttackToken";
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
		HttpServletRequest req = (HttpServletRequest)request;
		HttpServletResponse res = (HttpServletResponse)response;
		String token = req.getParameter( tokenName );


			// IF the URL doesn't contain token, then:
			//  calculate X=encrypt_with_key(server_time, client_IP_address)
			//  redirect to file.pdf?token=X
			//  add #a to the end of the url to eliminate any remaining anchors
			if ( token == null )
				String etoken = createToken( req );
				String uri = req.getRequestURI();
				String appender = uri.contains( "?" ) ? "&" : "?";
				String url = uri + appender + tokenName + "=" + etoken + "#a";
				res.sendRedirect( res.encodeRedirectURL( url ) );
			// ELSE IF the URL contains token, then:	
			// if decrypt(token_query).IP_address==client_IP_address and
			// decrypt(token_query).time>server_time-10sec
			//  serve the PDF resource as an in-line resource
			if ( checkToken( token, req ) )
				chain.doFilter(req, res);
			// ELSE IF the token doesn't match, then:
			// serve the PDF resource as a "save to disk" resource via a proper
			// choice of the Content-Type header (and/or an attachment, via
			// Content-Disposition).

			res.addHeader("Content-Disposition", "Attachment" );				
			res.setContentType( "application/octet" );  // may be overwritten
			chain.doFilter(req, res);
		catch( Exception e )
			throw new ServletException( e );

	public void destroy() {

	public void init(FilterConfig filterConfig) throws ServletException
			String tsparam = filterConfig.getInitParameter("timeoutSeconds");
			timeoutSeconds = Integer.parseInt(tsparam);
			String epparam = filterConfig.getInitParameter("encryptionPassword");
			char[] password = epparam.toCharArray();
			String tokenName = filterConfig.getInitParameter("PDFAttackTokenName");
			SecretKeyFactory kf = SecretKeyFactory.getInstance( "PBEWithMD5AndDES" );
			secretKey = kf.generateSecret( new javax.crypto.spec.PBEKeySpec( password ) );
		catch( Exception e )
			throw new ServletException( e );

	public String createToken( HttpServletRequest request ) throws Exception
		String address = request.getRemoteAddr();
		String time = ""+System.currentTimeMillis();
		return encryptString( address + "|" + time );
	public boolean checkToken( String etoken, HttpServletRequest request ) throws Exception
		String token = decryptString( etoken );
		String currentAddress = request.getRemoteAddr();
		String tokenAddress = getAddressFromToken( token );
		long currentTime = System.currentTimeMillis();
		long tokenTime = getTimeFromToken( token );
		return (currentAddress.equals( tokenAddress )) && (tokenTime > currentTime - timeoutSeconds * 1000);

	public String getAddressFromToken( String token )
		String address = token.substring( 0, token.indexOf("|") );
		return address;
	public long getTimeFromToken( String token )
		String date = token.substring( token.indexOf("|") + 1 );
		Long longdate = Long.parseLong( date );
		return longdate.longValue();
	public String decryptString( String str ) throws Exception
		Cipher passwordDecryptCipher = Cipher.getInstance( "PBEWithMD5AndDES/CBC/PKCS5Padding" );
		passwordDecryptCipher.init( Cipher.DECRYPT_MODE, secretKey, ps );
		byte[] dec = decoder.decodeBuffer( str.replace( '_', '+') );
		byte[] utf8 = passwordDecryptCipher.doFinal( dec );
		return new String( utf8, "UTF-8" );

	public String encryptString( String str ) throws Exception
		Cipher passwordEncryptCipher = Cipher.getInstance( "PBEWithMD5AndDES/CBC/PKCS5Padding" );
		passwordEncryptCipher.init( Cipher.ENCRYPT_MODE, secretKey, ps );
		byte[] utf8 = str.getBytes( "UTF-8" );
		byte[] enc = passwordEncryptCipher.doFinal( utf8 );
		return encoder.encode( enc ).replace( '+', '_' );