TAPESTRY-281: fixes security flaw in asset service


git-svn-id: https://svn.apache.org/repos/asf/jakarta/tapestry/branches/branch-3-0@244167 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/framework/src/org/apache/tapestry/IEngine.java b/framework/src/org/apache/tapestry/IEngine.java
index 33fab26..c340012 100644
--- a/framework/src/org/apache/tapestry/IEngine.java
+++ b/framework/src/org/apache/tapestry/IEngine.java
@@ -19,6 +19,7 @@
 
 import javax.servlet.ServletException;
 
+import org.apache.tapestry.asset.ResourceChecksumSource;
 import org.apache.tapestry.engine.IComponentClassEnhancer;
 import org.apache.tapestry.engine.IComponentMessagesSource;
 import org.apache.tapestry.engine.IEngineService;
@@ -376,4 +377,10 @@
      **/
     
     public String getOutputEncoding();
+    
+    /**
+     * Returns an object that can compute the checksum of a resource.
+     * @since 3.0.3
+     */
+    public ResourceChecksumSource getResourceChecksumSource();
 }
diff --git a/framework/src/org/apache/tapestry/TapestryStrings.properties b/framework/src/org/apache/tapestry/TapestryStrings.properties
index 443554b..6e6a2d7 100644
--- a/framework/src/org/apache/tapestry/TapestryStrings.properties
+++ b/framework/src/org/apache/tapestry/TapestryStrings.properties
@@ -91,6 +91,8 @@
 
 AssetExternalizer.externalize-failure=Could not externalize asset {0} to {1}.
 AssetService.exception-report-title=Failure to export asset {0}.
+AssetService.checksum-failure=Checksum {0} does not match that of resource {1}.
+AssetService.checksum-compute-failure=Failed to compute checksum for resource {1}.
 
 ExternalAsset.resource-missing=Could not access external asset {0}.
 
diff --git a/framework/src/org/apache/tapestry/asset/AssetService.java b/framework/src/org/apache/tapestry/asset/AssetService.java
index 8398f20..a695a27 100644
--- a/framework/src/org/apache/tapestry/asset/AssetService.java
+++ b/framework/src/org/apache/tapestry/asset/AssetService.java
@@ -22,7 +22,6 @@
 import java.util.Map;
 
 import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
 
 import org.apache.tapestry.ApplicationRuntimeException;
 import org.apache.tapestry.IComponent;
@@ -85,7 +84,7 @@
 
     public ILink getLink(IRequestCycle cycle, IComponent component, Object[] parameters)
     {
-        if (Tapestry.size(parameters) != 1)
+        if (Tapestry.size(parameters) != 2)
             throw new ApplicationRuntimeException(
                 Tapestry.format("service-single-parameter", Tapestry.ASSET_SERVICE));
 
@@ -129,22 +128,31 @@
         IEngineServiceView engine,
         IRequestCycle cycle,
         ResponseOutputStream output)
-        throws ServletException, IOException
+        throws IOException
     {
         Object[] parameters = getParameters(cycle);
 
-        if (Tapestry.size(parameters) != 1)
+        if (Tapestry.size(parameters) != 2)
             throw new ApplicationRuntimeException(
-                Tapestry.format("service-single-parameter", Tapestry.ASSET_SERVICE));
+                Tapestry.format("service-incorrect-parameter-count", Tapestry.ASSET_SERVICE, new Integer(2)));
 
         String resourcePath = (String) parameters[0];
+        String checksum = (String) parameters[1];
 
-        URL resourceURL = cycle.getEngine().getResourceResolver().getResource(resourcePath);
+        URL resourceURL = engine.getResourceResolver().getResource(resourcePath);
 
         if (resourceURL == null)
             throw new ApplicationRuntimeException(
                 Tapestry.format("missing-resource", resourcePath));
 
+        String actualChecksum = engine.getResourceChecksumSource().getChecksum(resourceURL);
+        
+        if (!actualChecksum.equals(checksum))
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("AssetService.checksum-failure", checksum, resourcePath));
+        }
+        
         URLConnection resourceConnection = resourceURL.openConnection();
 
         ServletContext servletContext = cycle.getRequestContext().getServlet().getServletContext();
diff --git a/framework/src/org/apache/tapestry/asset/PrivateAsset.java b/framework/src/org/apache/tapestry/asset/PrivateAsset.java
index 53a4d47..fe2a701 100644
--- a/framework/src/org/apache/tapestry/asset/PrivateAsset.java
+++ b/framework/src/org/apache/tapestry/asset/PrivateAsset.java
@@ -18,11 +18,11 @@
 import java.net.URL;
 
 import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.IEngine;
 import org.apache.tapestry.ILocation;
 import org.apache.tapestry.IRequestCycle;
 import org.apache.tapestry.IResourceLocation;
 import org.apache.tapestry.Tapestry;
-import org.apache.tapestry.engine.IEngineService;
 import org.apache.tapestry.engine.ILink;
 import org.apache.tapestry.resource.ClasspathResourceLocation;
 
@@ -71,10 +71,14 @@
         // Otherwise, the service is responsible for dynamically retrieving the
         // resource.
 
-        String[] parameters = new String[] { path };
+        IEngine engine = cycle.getEngine();
+        
+        URL resourceURL = engine.getResourceResolver().getResource(path);
+        String checksum = engine.getResourceChecksumSource().getChecksum(resourceURL);
+        
+        String[] parameters = new String[] { path, checksum };
 
-        IEngineService service = cycle.getEngine().getService(Tapestry.ASSET_SERVICE);
-
+        AssetService service = (AssetService) engine.getService(Tapestry.ASSET_SERVICE);
         ILink link = service.getLink(cycle, null, parameters);
 
         return link.getURL();
diff --git a/framework/src/org/apache/tapestry/asset/ResourceChecksumSource.java b/framework/src/org/apache/tapestry/asset/ResourceChecksumSource.java
new file mode 100644
index 0000000..6c21fe8
--- /dev/null
+++ b/framework/src/org/apache/tapestry/asset/ResourceChecksumSource.java
@@ -0,0 +1,42 @@
+// Copyright 2004, 2005 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry.asset;
+
+import java.net.URL;
+
+/**
+ * Calculates the checksum value, as a string, for a particular classpath resource. This is primarily
+ * used by the {@link org.apache.tapestry.asset.AssetService} to authenticate requests (you are not
+ * allowed access to a resource unless you can provide the correct checksum value).
+ * 
+ * This code is based on code from Howard Lewis Ship from the upcoming 3.1 release.
+ * 
+ * @author  Paul Ferraro
+ * @since   3.0.3
+ */
+public interface ResourceChecksumSource
+{
+    /**
+     * Returns the checksum value for the given resource.
+     * @param resourceURL the url of a resource
+     * @return the checksum value of the specified resource
+     */
+    public String getChecksum(URL resourceURL);
+    
+    /**
+     * Clears the internal cache.
+     */
+    public void reset();
+}
diff --git a/framework/src/org/apache/tapestry/asset/ResourceChecksumSourceImpl.java b/framework/src/org/apache/tapestry/asset/ResourceChecksumSourceImpl.java
new file mode 100644
index 0000000..c9015e2
--- /dev/null
+++ b/framework/src/org/apache/tapestry/asset/ResourceChecksumSourceImpl.java
@@ -0,0 +1,120 @@
+//Copyright 2005 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry.asset;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.codec.BinaryEncoder;
+import org.apache.tapestry.ApplicationRuntimeException;
+import org.apache.tapestry.Tapestry;
+
+/**
+ * Implementation of {@link org.apache.tapestry.asset.ResourceDigestSource} that calculates an
+ * checksum using a message digest and configured encoder.
+ * 
+ * This code is based on code from Howard Lewis Ship from the upcoming 3.1 release.
+ * 
+ * @author Paul Ferraro
+ * @since 3.0.3
+ */
+public class ResourceChecksumSourceImpl implements ResourceChecksumSource
+{
+    private static final int BUFFER_SIZE = 4096;
+
+    private Map _cache = new HashMap();
+    
+    private String _digestAlgorithm;
+
+    private BinaryEncoder _encoder;
+    
+    public ResourceChecksumSourceImpl(String digestAlgorithm, BinaryEncoder encoder)
+    {
+        _digestAlgorithm = digestAlgorithm;
+        _encoder = encoder;
+    }
+    
+    /**
+     * Checksum is obtained from cache if possible.
+     * If not, checksum is computed using {@link #computeChecksum(URL)}
+     * @see org.apache.tapestry.asset.ResourceDigestSource#getChecksum(java.net.URL)
+     */
+    public String getChecksum(URL resourceURL)
+    {
+        synchronized (_cache)
+        {
+            String checksum = (String) _cache.get(resourceURL);
+            
+            if (checksum == null)
+            {
+                checksum = computeChecksum(resourceURL);
+                
+                _cache.put(resourceURL, checksum);
+            }
+            
+            return checksum;
+        }
+    }
+    
+    /**
+     * @see org.apache.tapestry.asset.ResourceDigestSource#reset()
+     */
+    public void reset()
+    {
+        synchronized (_cache)
+        {
+            _cache.clear();
+        }
+    }
+    
+    /**
+     * Computes a message digest of the specified resource and encodes it into a string.
+     * @param resourceURL the url of a resource
+     * @return the checksum value of the specified resource
+     */
+    protected String computeChecksum(URL resourceURL)
+    {
+        try
+        {
+	        MessageDigest digest = MessageDigest.getInstance(_digestAlgorithm);
+	        
+	        InputStream inputStream = new BufferedInputStream(resourceURL.openStream(), BUFFER_SIZE);
+	        
+	        byte[] block = new byte[BUFFER_SIZE];
+	        
+	        int read = inputStream.read(block);
+	        
+	        while (read >= 0)
+	        {
+	            digest.update(block, 0, read);
+	            
+	            read = inputStream.read(block);
+	        }
+	        
+	        inputStream.close();
+	        
+	        return new String(_encoder.encode(digest.digest()));
+        }
+        catch (Exception e)
+        {
+            throw new ApplicationRuntimeException(
+                Tapestry.format("AssetService.checksum-compute-failure", resourceURL), e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/framework/src/org/apache/tapestry/engine/AbstractEngine.java b/framework/src/org/apache/tapestry/engine/AbstractEngine.java
index f099c19..ed18e8b 100644
--- a/framework/src/org/apache/tapestry/engine/AbstractEngine.java
+++ b/framework/src/org/apache/tapestry/engine/AbstractEngine.java
@@ -40,6 +40,7 @@
 import javax.servlet.http.HttpSessionBindingListener;
 
 import org.apache.bsf.BSFManager;
+import org.apache.commons.codec.binary.Hex;
 import org.apache.commons.lang.builder.ToStringBuilder;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
@@ -56,6 +57,8 @@
 import org.apache.tapestry.StaleLinkException;
 import org.apache.tapestry.StaleSessionException;
 import org.apache.tapestry.Tapestry;
+import org.apache.tapestry.asset.ResourceChecksumSource;
+import org.apache.tapestry.asset.ResourceChecksumSourceImpl;
 import org.apache.tapestry.enhance.DefaultComponentClassEnhancer;
 import org.apache.tapestry.listener.ListenerMap;
 import org.apache.tapestry.pageload.PageSource;
@@ -318,6 +321,14 @@
     protected static final String DATA_SQUEEZER_NAME = "org.apache.tapestry.DataSqueezer";
 
     /**
+     * Servlet context attribute name for a shared instance
+     * of {@link ResourceChecksumSource}.
+     * @since 3.0.3
+     */
+    protected static final String RESOURCE_CHECKSUM_SOURCE_NAME = 
+        "org.apache.tapestry.ResourceChecksumSource";
+    
+    /**
      *  The source for pages, which acts as a pool, but is capable of
      *  creating pages as needed.  Stored in the
      *  {@link ServletContext}, like {@link #_templateSource}.
@@ -435,6 +446,12 @@
     private transient IMonitorFactory _monitorFactory;
 
     /**
+     * Used to obtain resource checksums for the asset service.
+     * @since 3.0.3
+     */
+    private transient ResourceChecksumSource _resourceChecksumSource;
+    
+    /**
      *  Sets the Exception page's exception property, then renders the Exception page.
      *
      *  <p>If the render throws an exception, then copious output is sent to
@@ -1124,6 +1141,7 @@
         _scriptSource.reset();
         _stringsSource.reset();
         _enhancer.reset();
+        _resourceChecksumSource.reset();
     }
 
     /**
@@ -1390,6 +1408,20 @@
                 servletContext.setAttribute(name, _global);
             }
         }
+        
+        if (_resourceChecksumSource == null)
+        {
+            String name = RESOURCE_CHECKSUM_SOURCE_NAME + ":" + servletName;
+
+            _resourceChecksumSource = (ResourceChecksumSource) servletContext.getAttribute(name);
+
+            if (_resourceChecksumSource == null)
+            {
+                _resourceChecksumSource = createResourceChecksumSource();
+
+                servletContext.setAttribute(name, _resourceChecksumSource);
+            }
+        }
 
         String encoding = request.getCharacterEncoding();
         if (encoding == null)
@@ -1496,6 +1528,19 @@
     }
 
     /**
+     * Invoked from {@link #setupForRequest(RequestContext)} to provide
+     * an instance of {@link ResourceChecksumSource} that will be stored into
+     * the {@link ServletContext}.  Subclasses may override this method
+     * to provide a different implementation.
+     * @return an instance of {@link ResourceChecksumSourceImpl} that uses MD5 and Hex encoding.
+     * @since 3.0.3
+     */
+    protected ResourceChecksumSource createResourceChecksumSource()
+    {
+        return new ResourceChecksumSourceImpl("MD5", new Hex());
+    }
+    
+    /**
      *  Returns an object which can find resources and classes.
      *
      **/
@@ -2036,6 +2081,13 @@
         return _propertySource;
     }
 
+    /** @since 3.0.3 */
+    
+    public ResourceChecksumSource getResourceChecksumSource()
+    {
+        return _resourceChecksumSource;
+    }
+    
     /** @since 3.0 **/
 
     protected String getExceptionPageName()
diff --git a/junit/mock-scripts/TestAssetService.xml b/junit/mock-scripts/TestAssetService.xml
index 449eac4..070ea38 100644
--- a/junit/mock-scripts/TestAssetService.xml
+++ b/junit/mock-scripts/TestAssetService.xml
@@ -34,14 +34,17 @@
 		
 		<assert-output name="Image Tag">
 <![CDATA[
-<img src="/c16/app?service=asset&amp;sp=S%2Forg%2Fapache%2Ftapestry%2Fjunit%2Fmock%2Fc16%2Flogo.png" border="0"/>
+<img src="/c16/app?service=asset&amp;sp=S%2Forg%2Fapache%2Ftapestry%2Fjunit%2Fmock%2Fc16%2Flogo.png&amp;sp=Sf6324ac8f24f0a7f4850221b0f14c865" border="0"/>
 ]]>
 		</assert-output>
 	</request>
 	
 	<request>
 		<parameter name="service" value="asset"/>
-		<parameter name="sp" value="/org/apache/tapestry/junit/mock/c16/logo.png"/>
+		<parameter name="sp">
+      <value>/org/apache/tapestry/junit/mock/c16/logo.png</value>
+      <value>Sf6324ac8f24f0a7f4850221b0f14c865</value>
+		</parameter>
 		
 		<assert-output-stream name="Image Content"
 				content-type="image/png"
@@ -50,19 +53,46 @@
 	
 	<request>
 		<parameter name="service" value="asset"/>
-		<parameter name="sp" value="/org/apache/tapestry/junit/mock/c16/globe.jpg"/>
+		<parameter name="sp">
+      <value>/org/apache/tapestry/junit/mock/c16/globe.jpg</value>
+      <value>S52463f90c449b2546814d8929aa7d5cd</value>
+		</parameter>
 		
 		<assert-output-stream name="Image Content"
 				content-type="image/jpeg"
 				path="src/org/apache/tapestry/junit/mock/c16/globe.jpg"/>	
 	</request>	
+
+	<!-- Request a file using a bad checksum. -->
+	
+	<request>
+		<parameter name="service" value="asset"/>
+		<parameter name="sp">
+      <value>/org/apache/tapestry/junit/mock/c16/logo.png</value>
+      <value>Sabcdef</value>
+		</parameter>
+    
+		<assert-output name="Page Title">
+<![CDATA[
+<title>Exception</title>
+]]>			
+		</assert-output>
+		
+		<assert-output name="Message">
+    Checksum abcdef does not match that of resource /org/apache/tapestry/junit/mock/c16/logo.png.
+		</assert-output>
+		
+	</request>
 	
 	<!-- Request a file which does not exist. -->
 	
 	<request>
 		<parameter name="service" value="asset"/>
-		<parameter name="sp" value="/org/apache/tapestry/junit/mock/c16/MISSING.gif"/>
-		
+    <parameter name="sp">
+      <value>/org/apache/tapestry/junit/mock/c16/MISSING.gif</value>
+      <value>Sabcdef</value>
+    </parameter>
+    
 		<assert-output name="Page Title">
 <![CDATA[
 <title>Exception</title>
@@ -87,7 +117,24 @@
 		</assert-output>
 			
 		<assert-output name="Message">
-		Service asset requires exactly one service parameter.	
+    Service asset requires exactly 2 service parameters.
+		</assert-output>
+	</request>
+
+	<!-- Request missing the checksum -->
+	
+	<request>
+		<parameter name="service" value="asset"/>
+    <parameter name="sp" value="/org/apache/tapestry/junit/mock/c16/MISSING.gif"/>
+		
+		<assert-output name="Page Title">
+<![CDATA[
+<title>Exception</title>
+]]>			
+		</assert-output>
+			
+		<assert-output name="Message">
+    Service asset requires exactly 2 service parameters.
 		</assert-output>
 	</request>