High severityNVD Advisory· Published Aug 22, 2015· Updated May 6, 2026
CVE-2014-1972
CVE-2014-1972
Description
Apache Tapestry before 5.3.6 relies on client-side object storage without checking whether a client has modified an object, which allows remote attackers to cause a denial of service (resource consumption) or execute arbitrary code via crafted serialized data.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.tapestry:tapestry-coreMaven | < 5.3.6 | 5.3.6 |
Affected products
1Patches
15ad5257fdfacTAP5-2008: Implement HMAC signatures on object streams stored on the client
9 files changed · +378 −20
tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ClientDataEncoderImpl.java+68 −10 modified@@ -1,4 +1,4 @@ -// Copyright 2009 The Apache Software Foundation +// Copyright 2009, 2012 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. @@ -14,32 +14,54 @@ package org.apache.tapestry5.internal.services; +import org.apache.tapestry5.SymbolConstants; +import org.apache.tapestry5.internal.TapestryInternalUtils; import org.apache.tapestry5.internal.util.Base64InputStream; +import org.apache.tapestry5.internal.util.MacOutputStream; +import org.apache.tapestry5.ioc.annotations.Symbol; import org.apache.tapestry5.services.ClientDataEncoder; import org.apache.tapestry5.services.ClientDataSink; import org.apache.tapestry5.services.URLEncoder; +import org.slf4j.Logger; +import javax.crypto.spec.SecretKeySpec; import java.io.BufferedInputStream; import java.io.IOException; import java.io.ObjectInputStream; +import java.io.UnsupportedEncodingException; +import java.security.Key; import java.util.zip.GZIPInputStream; public class ClientDataEncoderImpl implements ClientDataEncoder { private final URLEncoder urlEncoder; - public ClientDataEncoderImpl(URLEncoder urlEncoder) + private final Key hmacKey; + + public ClientDataEncoderImpl(URLEncoder urlEncoder, @Symbol(SymbolConstants.HMAC_PASSPHRASE) String passphrase, Logger logger) throws UnsupportedEncodingException { this.urlEncoder = urlEncoder; + + if (passphrase.equals("")) + { + logger.error(String.format("The symbol '%s' has not been configured. " + + "This is used to configure hash-based message authentication of Tapestry data stored in forms, or in the URL. " + + "You application is less secure, and more vulnerable to denial-of-service attacks, when this symbol is left unconfigured.", + SymbolConstants.HMAC_PASSPHRASE)); + + // Errors at lower levels if the passphrase is empty, so override the parameter to set a default value. + passphrase = "DEFAULT"; + } + + hmacKey = new SecretKeySpec(passphrase.getBytes("UTF8"), "HmacSHA1"); } public ClientDataSink createSink() { try { - return new ClientDataSinkImpl(urlEncoder); - } - catch (IOException ex) + return new ClientDataSinkImpl(urlEncoder, hmacKey); + } catch (IOException ex) { throw new RuntimeException(ex); } @@ -48,21 +70,57 @@ public ClientDataSink createSink() public ObjectInputStream decodeClientData(String clientData) { // The clientData is Base64 that's been gzip'ed (i.e., this matches - // what ClientDataSinkImpl does. + // what ClientDataSinkImpl does). + + int colonx = clientData.indexOf(':'); + + if (colonx < 0) + { + throw new IllegalArgumentException("Client data must be prefixed with its HMAC code."); + } + + // Extract the string presumably encoded by the server using the secret key. + + String storedHmacResult = clientData.substring(0, colonx); + + String clientStream = clientData.substring(colonx + 1); try { - BufferedInputStream buffered = new BufferedInputStream( - new GZIPInputStream(new Base64InputStream(clientData))); + Base64InputStream b64in = new Base64InputStream(clientStream); + + validateHMAC(storedHmacResult, b64in); + + // After reading it once to validate, reset it for the actual read (which includes the GZip decompression). + + b64in.reset(); + + BufferedInputStream buffered = new BufferedInputStream(new GZIPInputStream(b64in)); return new ObjectInputStream(buffered); - } - catch (IOException ex) + } catch (IOException ex) { throw new RuntimeException(ex); } } + private void validateHMAC(String storedHmacResult, Base64InputStream b64in) throws IOException + { + MacOutputStream macOs = MacOutputStream.streamFor(hmacKey); + + TapestryInternalUtils.copy(b64in, macOs); + + macOs.close(); + + String actual = macOs.getResult(); + + if (!storedHmacResult.equals(actual)) + { + throw new IOException("Client data associated with the current request appears to have been tampered with " + + "(the HMAC signature does not match)."); + } + } + public ObjectInputStream decodeEncodedClientData(String clientData) throws IOException { return decodeClientData(urlEncoder.decode(clientData));
tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ClientDataSinkImpl.java+13 −6 modified@@ -1,4 +1,4 @@ -// Copyright 2009 The Apache Software Foundation +// Copyright 2009, 2012 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. @@ -15,13 +15,16 @@ package org.apache.tapestry5.internal.services; import org.apache.tapestry5.internal.util.Base64OutputStream; +import org.apache.tapestry5.internal.util.MacOutputStream; +import org.apache.tapestry5.internal.util.TeeOutputStream; import org.apache.tapestry5.services.ClientDataSink; import org.apache.tapestry5.services.URLEncoder; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.OutputStream; +import java.security.Key; import java.util.zip.GZIPOutputStream; public class ClientDataSinkImpl implements ClientDataSink @@ -34,12 +37,17 @@ public class ClientDataSinkImpl implements ClientDataSink private boolean closed; - public ClientDataSinkImpl(URLEncoder urlEncoder) throws IOException + private final MacOutputStream macOutputStream; + + public ClientDataSinkImpl(URLEncoder urlEncoder, Key hmacKey) throws IOException { this.urlEncoder = urlEncoder; + base64OutputStream = new Base64OutputStream(); + macOutputStream = MacOutputStream.streamFor(hmacKey); - final BufferedOutputStream pipeline = new BufferedOutputStream(new GZIPOutputStream(base64OutputStream)); + final BufferedOutputStream pipeline = new BufferedOutputStream(new GZIPOutputStream( + new TeeOutputStream(macOutputStream, base64OutputStream))); OutputStream guard = new OutputStream() { @@ -92,14 +100,13 @@ public String getClientData() try { objectOutputStream.close(); - } - catch (IOException ex) + } catch (IOException ex) { // Ignore. } } - return base64OutputStream.toBase64(); + return macOutputStream.getResult() + ":" + base64OutputStream.toBase64(); } public String getEncodedClientData()
tapestry-core/src/main/java/org/apache/tapestry5/internal/util/MacOutputStream.java+86 −0 added@@ -0,0 +1,86 @@ +// Copyright 2012 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.tapestry5.internal.util; + +import org.apache.commons.codec.binary.Base64; +import org.apache.tapestry5.ioc.internal.util.InternalUtils; + +import javax.crypto.Mac; +import java.io.IOException; +import java.io.OutputStream; +import java.security.Key; + +/** + * An output stream that wraps around a {@link Mac} (message authentication code algorithm). This is currently + * used for symmetric (private) keys, but in theory could be used with assymetric (public/private) keys. + * + * @since 5.3.6 + */ +public class MacOutputStream extends OutputStream +{ + private final Mac mac; + + public static MacOutputStream streamFor(Key key) throws IOException + { + try + { + Mac mac = Mac.getInstance(key.getAlgorithm()); + mac.init(key); + + return new MacOutputStream(mac); + } catch (Exception ex) + { + throw new IOException("Unable to create MacOutputStream: " + InternalUtils.toMessage(ex), ex); + } + } + + public MacOutputStream(Mac mac) + { + assert mac != null; + + this.mac = mac; + } + + /** + * Should only be invoked once, immediately after this stream is closed; it generates the final MAC result, and + * returns it as a Base64 encoded string. + * + * @return Base64 encoded MAC result + */ + public String getResult() + { + byte[] result = mac.doFinal(); + + return new String(Base64.encodeBase64(result)); + } + + @Override + public void write(int b) throws IOException + { + mac.update((byte) b); + } + + @Override + public void write(byte[] b) throws IOException + { + mac.update(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException + { + mac.update(b, off, len); + } +}
tapestry-core/src/main/java/org/apache/tapestry5/internal/util/TeeOutputStream.java+73 −0 added@@ -0,0 +1,73 @@ +// Copyright 2012 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.tapestry5.internal.util; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * An output stream that acts like a "tee", copying all provided bytes to two output streams. This is used, for example, + * to accumulate a hash of content even as it is being written. + * + * @since 5.3.5 + */ +public class TeeOutputStream extends OutputStream +{ + private final OutputStream left, right; + + public TeeOutputStream(OutputStream left, OutputStream right) + { + assert left != null; + assert right != null; + + this.left = left; + this.right = right; + } + + @Override + public void write(int b) throws IOException + { + left.write(b); + right.write(b); + } + + @Override + public void write(byte[] b) throws IOException + { + left.write(b); + right.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException + { + left.write(b, off, len); + right.write(b, off, len); + } + + @Override + public void flush() throws IOException + { + left.flush(); + right.flush(); + } + + @Override + public void close() throws IOException + { + left.close(); + right.close(); + } +}
tapestry-core/src/main/java/org/apache/tapestry5/services/ClientDataEncoder.java+13 −4 modified@@ -1,4 +1,4 @@ -// Copyright 2009 The Apache Software Foundation +// Copyright 2009, 2012 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. @@ -21,6 +21,10 @@ * A service used when a component or service needs to encode some amount of data on the client as a string. The string * may be a query parameter, hidden form field, or a portion of a URL. The default implementation converts the object * output stream into a Base64 string. + * <p/> + * Starting in release 5.3.6, the encoded data incorporates an HMAC (hash based message authentication code) signature, + * as a prefix. HMAC requires a secret key, configured using the + * {@link org.apache.tapestry5.SymbolConstants#HMAC_PASSPHRASE} symbol. * * @since 5.1.0.1 */ @@ -37,17 +41,22 @@ public interface ClientDataEncoder /** * Decodes data previously obtained from {@link ClientDataSink#getClientData()}. * - * @param clientData encoded client data + * @param clientData + * encoded client data * @return stream of decoded data + * @throws IOException + * if the client data has been corrupted (verified via the HMAC) */ ObjectInputStream decodeClientData(String clientData) throws IOException; /** - * Decoes client data obtained via {@link ClientDataSink#getEncodedClientData()}. + * Decodes client data obtained via {@link ClientDataSink#getEncodedClientData()}. * - * @param clientData URLEncoded client data + * @param clientData + * URLEncoded client data * @return stream of objects * @throws IOException + * if the client data has been corrupted (verified via the HMAC) * @since 5.1.0.4 */ ObjectInputStream decodeEncodedClientData(String clientData) throws IOException;
tapestry-core/src/main/java/org/apache/tapestry5/services/TapestryModule.java+4 −0 modified@@ -2394,6 +2394,10 @@ public static void contributeFactoryDefaults(MappedConfiguration<String, Object> // By default, no page is on the whitelist unless it has the @WhitelistAccessOnly annotation configuration.add(MetaDataConstants.WHITELIST_ONLY_PAGE, false); + + // Leaving this as the default results in a runtime error logged to the console (and a default password is used); + // you are expected to override this symbol. + configuration.add(SymbolConstants.HMAC_PASSPHRASE, ""); } /**
tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java+11 −0 modified@@ -365,4 +365,15 @@ public class SymbolConstants * Prefix to be used for all asset paths */ public static final String ASSET_PATH_PREFIX = "tapestry.asset-path-prefix"; + + /** + * A passphrase used as the basis of hash-based message authentication (HMAC) for any object stream data stored on + * the client. The default phrase is the empty string, which will result in a logged runtime <em>error</em>. + * You should configure this to a reasonable value (longer is better) and ensure that all servers in your cluster + * share the same value (configuring this in code, rather than the command line, is preferred). + * + * @see org.apache.tapestry5.services.ClientDataEncoder + * @since 5.3.6 + */ + public static final String HMAC_PASSPHRASE = "tapestry.hmac-passphrase"; }
tapestry-core/src/test/groovy/org/apache/tapestry5/internal/services/ClientDataEncoderImplTest.groovy+108 −0 added@@ -0,0 +1,108 @@ +package org.apache.tapestry5.internal.services + +import org.apache.tapestry5.ioc.test.TestBase +import org.apache.tapestry5.services.ClientDataEncoder +import org.easymock.EasyMock +import org.slf4j.Logger +import org.testng.annotations.Test + +class ClientDataEncoderImplTest extends TestBase { + + def tryEncodeAndDecode(ClientDataEncoder cde) { + def now = new Date() + def input = "The current time is $now" + + + String clientData = convertToClientData cde, input + + def stream = cde.decodeClientData clientData + + def output = stream.readObject() + + assert !input.is(output) + assert input == output + } + + def String convertToClientData(ClientDataEncoder cde, input) { + def sink = cde.createSink() + + sink.getObjectOutputStream().with { stream -> + stream << input + stream.close() + } + + sink.clientData + } + + def extractData(String encoded) { + def colonx = encoded.indexOf(':') + + encoded.substring(colonx + 1) + } + + @Test + void blank_passphrase_works_but_logs_error() { + Logger logger = newMock Logger + + logger.error(EasyMock.isA(String)) + + replay() + + ClientDataEncoder cde = new ClientDataEncoderImpl(null, "", logger) + + tryEncodeAndDecode cde + + verify() + } + + @Test + void no_logged_error_with_non_blank_passphrase() { + ClientDataEncoder cde = new ClientDataEncoderImpl(null, "Testing, Testing, 1.., 2.., 3...", null) + + tryEncodeAndDecode cde + } + + @Test + void passphrase_affects_encoded_output() { + ClientDataEncoder first = new ClientDataEncoderImpl(null, "first passphrase", null) + ClientDataEncoder second = new ClientDataEncoderImpl(null, " different passphrase ", null) + + def input = "current time millis is ${System.currentTimeMillis()} ms" + + def output1 = convertToClientData first, input + def output2 = convertToClientData second, input + + assert output1 != output2 + + assert extractData(output1) == extractData(output2) + } + + @Test(expectedExceptions = IllegalArgumentException) + void decode_with_missing_hmac_prefix_is_a_failure() { + ClientDataEncoder cde = new ClientDataEncoderImpl(null, "a passphrase", null) + + cde.decodeClientData("so completely invalid") + } + + @Test + void incorrect_hmac_is_detected() { + + // Simulate tampering by encoding with one passphrase and attempting to decode with a different + // passphrase. + ClientDataEncoder first = new ClientDataEncoderImpl(null, "first passphrase", null) + ClientDataEncoder second = new ClientDataEncoderImpl(null, " different passphrase ", null) + + def input = "current time millis is ${System.currentTimeMillis()} ms" + + def output = convertToClientData first, input + + try { + second.decodeClientData(output) + unreachable() + } + catch (Exception e) { + assert e.message.contains("HMAC signature does not match") + } + } + +}
tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java+2 −0 modified@@ -157,6 +157,8 @@ public static void contributeApplicationDefaults(MappedConfiguration<String, Str configuration.add(SymbolConstants.SECURE_ENABLED, "true"); configuration.add("app.injected-symbol", "Symbol contributed to ApplicationDefaults"); + + configuration.add(SymbolConstants.HMAC_PASSPHRASE, "testing, testing, 1... 2... 3..."); } public static void contributeIgnoredPathsFilter(Configuration<String> configuration)
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
17- jvn.jp/en/jp/JVN17611367/index.htmlnvdVendor AdvisoryWEB
- jvndb.jvn.jp/jvndb/JVNDB-2015-000118nvdVendor AdvisoryWEB
- github.com/advisories/GHSA-c438-8cvq-pxxxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2014-1972ghsaADVISORY
- tapestry.apache.org/release-notes-536.htmlnvdVendor AdvisoryWEB
- seclists.org/fulldisclosure/2019/Aug/20nvdWEB
- www.openwall.com/lists/oss-security/2019/08/23/5nvdWEB
- github.com/apache/tapestry-5/commit/5ad5257fdfacbad2c7c480fdf2afa15d9a37e6b0ghsaWEB
- issues.apache.org/jira/browse/TAP5-2008nvdWEB
- lists.apache.org/thread.html/84e99dedad2ecb4676de93c3ab73a8a10882951ab6984f514707f3d9@%3Cusers.tapestry.apache.org%3EghsaWEB
- lists.apache.org/thread.html/bac8d6f9e1b4059b319d9cba6f33219a99b81623476ec896138f851c@%3Cusers.tapestry.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r7d9c54beb1dc97dcccc58d9b5d31f0f7166f9a25ad1beba5f8091e0c@%3Ccommits.tapestry.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r87523dd07886223aa086edc25fe9b8ddb9c1090f7db25b068dc30843@%3Ccommits.tapestry.apache.org%3EghsaWEB
- lists.apache.org/thread.html/84e99dedad2ecb4676de93c3ab73a8a10882951ab6984f514707f3d9%40%3Cusers.tapestry.apache.org%3Envd
- lists.apache.org/thread.html/bac8d6f9e1b4059b319d9cba6f33219a99b81623476ec896138f851c%40%3Cusers.tapestry.apache.org%3Envd
- lists.apache.org/thread.html/r7d9c54beb1dc97dcccc58d9b5d31f0f7166f9a25ad1beba5f8091e0c%40%3Ccommits.tapestry.apache.org%3Envd
- lists.apache.org/thread.html/r87523dd07886223aa086edc25fe9b8ddb9c1090f7db25b068dc30843%40%3Ccommits.tapestry.apache.org%3Envd
News mentions
0No linked articles in our index yet.