This site is the archived OWASP Foundation Wiki and is no longer accepting Account Requests.
To view the new OWASP Foundation website, please visit https://owasp.org

Difference between revisions of "PDF Attack Filter for Java EE"

From OWASP
Jump to: navigation, search
 
m (Moved page into the right category. See Java space page for me details. Content has not been reviewed in this edit.)
 
(32 intermediate revisions by 8 users not shown)
Line 1: Line 1:
 +
{{taggedDocument
 +
| type=old
 +
| lastRevision=2007-04-24
 +
| comment=Content is old
 +
}}
 +
==Status==
 +
Released 24/4/2007
 +
 
==Overview==
 
==Overview==
  
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 is a filter to block XSS attacks on PDF files served by Java EE applications. The details of the attack are discussed [http://www.gnucitizen.org/blog/danger-danger-danger/ elsewhere].  This filter implements a simple algorithm suggested by Amit Klein.  We've placed this software in the public domain to make it easy for anyone to use for any purpose. Please let us know if you're using it!
 +
 
 +
If you have static PDF files, you may not need this filter. Instead, you could change MIME type or the Content-Disposition Header as [http://www.adobe.com/support/security/advisories/apsa07-02.html Adobe security advisory]. This will force the user to save the document rather than render it in the browser window.
  
 
==Approach==
 
==Approach==
Line 7: Line 17:
 
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);
 
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 worksThe anchor isn't actually sent to the application, so we have to get much trickier.
+
So the idea is to strip off the anchor. Unfortunately for us, the browser doesn't send the anchor along with the HTTP request. So we can't just strip it off.
 +
 
 +
Therefore, we're going to use a redirect to steer the browser to a link without the anchor containing the attackWell, actually it turns out that we have to overwrite the anchor with something else, so we're going to use "#a".
  
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.
+
But there's one last problem to overcome. Since the browser doesn't send the anchor, the new request will look exactly like the request generated by the redirect. With no way of telling the original request from the one generated by our redirect, we'll create an infinite loop.
 +
 
 +
So to differentiate them, we're going to add a temporary token to the URL in the redirect, which we'll verify when it arrives.  We don't want an attacker forging this token, so we're going to encrypt the user's source IP address along with a timestamp. If a request shows up for the PDF file without a valid token, we'll reject it. Or actually, we can force it to be saved to disk, thus preventing the attack from working.
 +
 
 +
This way, only an attacker from the same IP address who can trick you into clicking a link within 10 seconds of creating it can attack you. Not perfect, but certainly raises the bar quite a bit.
  
 
==Download==
 
==Download==
  
The source code (one file) and the compiled class file are in this zip file.
+
The source code (one file) and the compiled class file are in a single zip file.
 +
 
 +
'''[http://www.owasp.org/images/5/59/PDFAttackFilter.zip DOWNLOAD]'''
  
 
==Setup==
 
==Setup==
  
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  
+
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. The class file should be in a folder structure that matches the package (org -> owasp -> filters -> PDFAttackFilter).  You can extract the class file from the zip file.
 
 
Then we just have to add the following to our web.xml.
 
  
 +
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 URLs that serve a PDF file. You could use *.pdf, but you may have servlets that stream PDF files that don't end in .pdf.
  
 +
<pre>
 
<filter>
 
<filter>
 
    <filter-name>PDFAttackFilter</filter-name>
 
    <filter-name>PDFAttackFilter</filter-name>
 
    <filter-class>org.owasp.filters.PDFAttackFilter</filter-class>
 
    <filter-class>org.owasp.filters.PDFAttackFilter</filter-class>
  <init-param>
+
            <init-param>
      <param-name>timeoutSeconds</param-name>
+
                <param-name>timeoutSeconds</param-name>
      <param-value>1</param-value>
+
                <param-value>1</param-value>
  </init-param>
+
            </init-param>
  <init-param>
+
            <init-param>
      <param-name>encryptionPassword</param-name>
+
                <param-name>encryptionPassword</param-name>
      <param-value>password</param-value>
+
                <param-value>password</param-value>
  </init-param>
+
            </init-param>
  <init-param>
+
            <init-param>
      <param-name>PDFAttackTokenName</param-name>
+
                <param-name>PDFAttackTokenName</param-name>
      <param-value>PDFAttackToken</param-value>
+
                <param-value>PDFAttackToken</param-value>
  </init-param>
+
            </init-param>
 
  </filter>
 
  </filter>
 
       
 
       
Line 43: Line 61:
 
    <url-pattern>/*</url-pattern>
 
    <url-pattern>/*</url-pattern>
 
  </filter-mapping>
 
  </filter-mapping>
 +
</pre>
 +
 +
Depending on your application, it may be difficult to map all the URLs that lead to PDF files. You can map multiple url-patterns to the filter if necessary. In theory, it might be possible to send the redirect only if a response with content-type application/pdf. Then you could map the filter to apply to ALL requests. If there is demand for this feature, let us know.
  
 
==Source Code==
 
==Source Code==
Line 48: Line 69:
 
This code has been only minimally tested. Please help us verify the approach and the implementation used here.
 
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;
+
<pre>
 +
/**
 +
*  Software published by the Open Web Application Security Project (http://www.owasp.org)
 +
*  This software is in the public domain with no warranty.
 +
*
 +
* @author    Jeff Williams <a href="http://www.aspectsecurity.com">Aspect Security</a>
 +
* @created    January 4, 2007
 +
*/
 +
 
 +
package org.owasp.filters;
 +
 
 +
import java.io.IOException;
  
  import javax.crypto.Cipher;
+
import javax.crypto.Cipher;
  import javax.crypto.SecretKey;
+
import javax.crypto.SecretKey;
  import javax.crypto.SecretKeyFactory;
+
import javax.crypto.SecretKeyFactory;
  import javax.crypto.spec.PBEParameterSpec;
+
import javax.crypto.spec.PBEParameterSpec;
  import javax.servlet.Filter;
+
import javax.servlet.Filter;
  import javax.servlet.FilterChain;
+
import javax.servlet.FilterChain;
  import javax.servlet.FilterConfig;
+
import javax.servlet.FilterConfig;
  import javax.servlet.ServletException;
+
import javax.servlet.ServletException;
  import javax.servlet.ServletRequest;
+
import javax.servlet.ServletRequest;
  import javax.servlet.ServletResponse;
+
import javax.servlet.ServletResponse;
  import javax.servlet.http.HttpServletRequest;
+
import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
+
import javax.servlet.http.HttpServletResponse;
  
  public class PDFAttackFilter implements Filter  
+
public class PDFAttackFilter implements Filter  
  {
+
{
  
 
private static sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
 
private static sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
Line 94: Line 125:
 
{
 
{
 
String etoken = createToken( req );
 
String etoken = createToken( req );
String uri = req.getRequestURI();
+
String base = req.getRequestURI();
String appender = uri.contains( "?" ) ? "&" : "?";
+
String querystring = req.getQueryString();
String url = uri + appender + tokenName + "=" + etoken + "#a";
+
if ( querystring != null ) base += "?" + req.getQueryString();
 +
String appender = base.contains( "?" ) ? "&" : "?";
 +
String url = base + appender + tokenName + "=" + etoken + "#a";
 
res.sendRedirect( res.encodeRedirectURL( url ) );
 
res.sendRedirect( res.encodeRedirectURL( url ) );
 
return;
 
return;
Line 140: Line 173:
 
char[] password = epparam.toCharArray();
 
char[] password = epparam.toCharArray();
 
 
String tokenName = filterConfig.getInitParameter("PDFAttackTokenName");
+
tokenName = filterConfig.getInitParameter("PDFAttackTokenName");
 
 
 
SecretKeyFactory kf = SecretKeyFactory.getInstance( "PBEWithMD5AndDES" );
 
SecretKeyFactory kf = SecretKeyFactory.getInstance( "PBEWithMD5AndDES" );
Line 186: Line 219:
 
public synchronized String decryptString( String str ) throws Exception
 
public synchronized String decryptString( String str ) throws Exception
 
{
 
{
 +
// Cipher is not threadsafe, so create a new one each time
 
Cipher passwordDecryptCipher = Cipher.getInstance( "PBEWithMD5AndDES/CBC/PKCS5Padding" );
 
Cipher passwordDecryptCipher = Cipher.getInstance( "PBEWithMD5AndDES/CBC/PKCS5Padding" );
 
passwordDecryptCipher.init( Cipher.DECRYPT_MODE, secretKey, ps );
 
passwordDecryptCipher.init( Cipher.DECRYPT_MODE, secretKey, ps );
Line 195: Line 229:
 
public synchronized String encryptString( String str ) throws Exception
 
public synchronized String encryptString( String str ) throws Exception
 
{
 
{
 +
// Cipher is not threadsafe, so create a new one each time
 
Cipher passwordEncryptCipher = Cipher.getInstance( "PBEWithMD5AndDES/CBC/PKCS5Padding" );
 
Cipher passwordEncryptCipher = Cipher.getInstance( "PBEWithMD5AndDES/CBC/PKCS5Padding" );
 
passwordEncryptCipher.init( Cipher.ENCRYPT_MODE, secretKey, ps );
 
passwordEncryptCipher.init( Cipher.ENCRYPT_MODE, secretKey, ps );
Line 202: Line 237:
 
}
 
}
  
   }
+
}
 +
 
 +
</pre>
 +
 
 +
==Compile==
 +
 
 +
There are not many dependencies here, just the standard Java EE environment. You can compile with:
 +
 
 +
   javac -classpath j2ee.jar -d . *.java
  
 +
Then just copy the 'org' folder that gets created to the WEB-INF/classes folder.
  
 +
'''Comment: ''' In the decryptString method, it is necessary to strip the "#a" from the end of the str parameter, otherwise an IllegalBlockSizeException will be thrown with "Input length must be multiple of 8 when decrypting with padded cipher".
  
 
[[Category:How To]]
 
[[Category:How To]]
[[Category:OWASP Java Project]]
+
[[Category:Java]]
[[Category:OWASP Validation Project]]
+
[[Category:OWASP_Validation_Project]]
 +
[[Category:Countermeasure]]
 +
[[Category:Control]]
 +
[[Category:FixME/old]]

Latest revision as of 21:54, 10 November 2017

This page contains out-of-date content. Please help OWASP to FixME.
Last revision (yyyy-mm-dd): 2007-04-24
Comment: Content is old

Status

Released 24/4/2007

Overview

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. We've placed this software in the public domain to make it easy for anyone to use for any purpose. Please let us know if you're using it!

If you have static PDF files, you may not need this filter. Instead, you could change MIME type or the Content-Disposition Header as Adobe security advisory. This will force the user to save the document rather than render it in the browser window.

Approach

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);

So the idea is to strip off the anchor. Unfortunately for us, the browser doesn't send the anchor along with the HTTP request. So we can't just strip it off.

Therefore, we're going to use a redirect to steer the browser to a link without the anchor containing the attack. Well, actually it turns out that we have to overwrite the anchor with something else, so we're going to use "#a".

But there's one last problem to overcome. Since the browser doesn't send the anchor, the new request will look exactly like the request generated by the redirect. With no way of telling the original request from the one generated by our redirect, we'll create an infinite loop.

So to differentiate them, we're going to add a temporary token to the URL in the redirect, which we'll verify when it arrives. We don't want an attacker forging this token, so we're going to encrypt the user's source IP address along with a timestamp. If a request shows up for the PDF file without a valid token, we'll reject it. Or actually, we can force it to be saved to disk, thus preventing the attack from working.

This way, only an attacker from the same IP address who can trick you into clicking a link within 10 seconds of creating it can attack you. Not perfect, but certainly raises the bar quite a bit.

Download

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

DOWNLOAD

Setup

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. The class file should be in a folder structure that matches the package (org -> owasp -> filters -> PDFAttackFilter). You can extract the class file from the zip file.

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 URLs that serve a PDF file. You could use *.pdf, but you may have servlets that stream PDF files that don't end in .pdf.

	<filter>
	     <filter-name>PDFAttackFilter</filter-name>
	     <filter-class>org.owasp.filters.PDFAttackFilter</filter-class>
             <init-param>
                 <param-name>timeoutSeconds</param-name>
                 <param-value>1</param-value>
             </init-param>
             <init-param>
                 <param-name>encryptionPassword</param-name>
                 <param-value>password</param-value>
             </init-param>
             <init-param>
                 <param-name>PDFAttackTokenName</param-name>
                 <param-value>PDFAttackToken</param-value>
             </init-param>
	  </filter>
	       
	  <filter-mapping>
	     <filter-name>PDFAttackFilter</filter-name>
	     <url-pattern>/*</url-pattern>
	  </filter-mapping>

Depending on your application, it may be difficult to map all the URLs that lead to PDF files. You can map multiple url-patterns to the filter if necessary. In theory, it might be possible to send the redirect only if a response with content-type application/pdf. Then you could map the filter to apply to ALL requests. If there is demand for this feature, let us know.

Source Code

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


/**
 *  Software published by the Open Web Application Security Project (http://www.owasp.org)
 *  This software is in the public domain with no warranty.
 *
 * @author     Jeff Williams <a href="http://www.aspectsecurity.com">Aspect Security</a>
 * @created    January 4, 2007
 */

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 );

		try
		{

			// 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 base = req.getRequestURI();
				String querystring = req.getQueryString();
				if ( querystring != null ) base += "?" + req.getQueryString();
				String appender = base.contains( "?" ) ? "&" : "?";
				String url = base + appender + tokenName + "=" + etoken + "#a";
				res.sendRedirect( res.encodeRedirectURL( url ) );
				return;
			}
	
			// 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);
				return;
			}
	
			// 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
	{
		try
		{
			String tsparam = filterConfig.getInitParameter("timeoutSeconds");
			timeoutSeconds = Integer.parseInt(tsparam);
			
			String epparam = filterConfig.getInitParameter("encryptionPassword");
			char[] password = epparam.toCharArray();
			
			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 synchronized String decryptString( String str ) throws Exception
	{
		// Cipher is not threadsafe, so create a new one each time
		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 synchronized String encryptString( String str ) throws Exception
	{
		// Cipher is not threadsafe, so create a new one each time
		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( '+', '_' );
	}

}

Compile

There are not many dependencies here, just the standard Java EE environment. You can compile with:

 javac -classpath j2ee.jar -d . *.java

Then just copy the 'org' folder that gets created to the WEB-INF/classes folder.

Comment: In the decryptString method, it is necessary to strip the "#a" from the end of the str parameter, otherwise an IllegalBlockSizeException will be thrown with "Input length must be multiple of 8 when decrypting with padded cipher".