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&sp=S%2Forg%2Fapache%2Ftapestry%2Fjunit%2Fmock%2Fc16%2Flogo.png" border="0"/>
+<img src="/c16/app?service=asset&sp=S%2Forg%2Fapache%2Ftapestry%2Fjunit%2Fmock%2Fc16%2Flogo.png&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>