This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch 3_0_x-fixes
in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git
The following commit(s) were added to refs/heads/3_0_x-fixes by this push:
new 96a9bea18 Bound the encrypted data nesting depth (#713)
96a9bea18 is described below
commit 96a9bea185981a422c48f0f053a77d71a349352b
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon Sep 21 07:29:30 2026 +0100
Bound the encrypted data nesting depth (#713)
---
.../org/apache/wss4j/dom/handler/RequestData.java | 41 +++++++
.../dom/processor/EncryptedAssertionProcessor.java | 24 +++-
.../dom/processor/EncryptedDataProcessor.java | 16 ++-
.../wss4j/dom/handler/RequestDataNestingTest.java | 134 +++++++++++++++++++++
4 files changed, 208 insertions(+), 7 deletions(-)
diff --git
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/handler/RequestData.java
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/handler/RequestData.java
index 84bcb1717..e25604c50 100644
---
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/handler/RequestData.java
+++
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/handler/RequestData.java
@@ -84,6 +84,7 @@ public class RequestData {
private AlgorithmSuite samlAlgorithmSuite;
private boolean disableBSPEnforcement;
private boolean allowRSA15KeyTransportAlgorithm;
+ private int processorNestingDepth;
private boolean addUsernameTokenNonce;
private boolean addUsernameTokenCreated;
private Certificate[] tlsCerts;
@@ -545,6 +546,46 @@ public class RequestData {
this.disableBSPEnforcement = disableBSPEnforcement;
}
+ /**
+ * The maximum depth to which a processor may hand a token it has just
uncovered to another
+ * processor. Decrypting an EncryptedData whose plaintext is itself a
security structure does
+ * exactly that, and does it recursively, so a sender who nests those
structures is choosing
+ * how much of the receiver's stack to consume. Each level costs very
little to write - every
+ * one of them may point its KeyInfo at a single EncryptedKey, which is
decrypted once and
+ * cached - so the chain has to be bounded.
+ * <p/>
+ * Real messages stay well inside this. An EncryptedAssertion reaches
depth 2: the assertion's
+ * EncryptedData, then the decrypted Assertion handed to the SAML
processor, which dispatches
+ * no further. Re-encrypting an already encrypted assertion reaches 3.
Sibling tokens in the
+ * same header do not accumulate - each is entered and left in turn - so
the bound constrains
+ * nesting only.
+ */
+ public static final int MAXIMUM_PROCESSOR_NESTING_DEPTH = 5;
+
+ /**
+ * Record that processing is about to descend into a token uncovered by
another token, and
+ * refuse to go deeper than {@link #MAXIMUM_PROCESSOR_NESTING_DEPTH}.
Every caller must pair
+ * this with {@link #exitNestedToken()} in a finally block.
+ *
+ * @throws WSSecurityException if the message nests tokens too deeply
+ */
+ public void enterNestedToken() throws WSSecurityException {
+ if (processorNestingDepth >= MAXIMUM_PROCESSOR_NESTING_DEPTH) {
+ throw new WSSecurityException(
+ WSSecurityException.ErrorCode.INVALID_SECURITY, "empty",
+ new Object[] {"Tokens are nested more than "
+ + MAXIMUM_PROCESSOR_NESTING_DEPTH + " deep"});
+ }
+ processorNestingDepth++;
+ }
+
+ /**
+ * Record that processing has come back out of a nested token.
+ */
+ public void exitNestedToken() {
+ processorNestingDepth--;
+ }
+
public boolean isAllowRSA15KeyTransportAlgorithm() {
return allowRSA15KeyTransportAlgorithm;
}
diff --git
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedAssertionProcessor.java
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedAssertionProcessor.java
index 2faad4365..48d2b9d85 100644
---
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedAssertionProcessor.java
+++
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedAssertionProcessor.java
@@ -70,7 +70,12 @@ public class EncryptedAssertionProcessor implements
Processor {
((Element)currentChild).getLocalName());
Processor proc = request.getWssConfig().getProcessor(el);
if (proc != null) {
-
completeResults.addAll(proc.handleToken((Element)currentChild, request));
+ request.enterNestedToken();
+ try {
+
completeResults.addAll(proc.handleToken((Element)currentChild, request));
+ } finally {
+ request.exitNestedToken();
+ }
}
}
}
@@ -92,8 +97,14 @@ public class EncryptedAssertionProcessor implements
Processor {
Processor proc =
request.getWssConfig().getProcessor(el);
if (proc != null) {
LOG.debug("Processing decrypted element with:
{}", proc.getClass().getName());
- List<WSSecurityEngineResult> results =
proc.handleToken(decryptedElem, request);
- completeResults.addAll(0, results);
+ request.enterNestedToken();
+ try {
+ List<WSSecurityEngineResult> results =
+ proc.handleToken(decryptedElem,
request);
+ completeResults.addAll(0, results);
+ } finally {
+ request.exitNestedToken();
+ }
return completeResults;
}
}
@@ -119,7 +130,12 @@ public class EncryptedAssertionProcessor implements
Processor {
Processor proc = request.getWssConfig().getProcessor(el);
if (proc != null) {
LOG.debug("Processing decrypted element with: {}",
proc.getClass().getName());
- return proc.handleToken(encryptedDataElement, request);
+ request.enterNestedToken();
+ try {
+ return proc.handleToken(encryptedDataElement, request);
+ } finally {
+ request.exitNestedToken();
+ }
}
return Collections.emptyList();
diff --git
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedDataProcessor.java
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedDataProcessor.java
index 44763811e..5d9735c5c 100644
---
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedDataProcessor.java
+++
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/processor/EncryptedDataProcessor.java
@@ -120,7 +120,12 @@ public class EncryptedDataProcessor implements Processor {
} else if (encryptedKeyElement != null && data.getWssConfig() != null)
{
WSSConfig wssConfig = data.getWssConfig();
Processor encrKeyProc =
wssConfig.getProcessor(WSConstants.ENCRYPTED_KEY);
- encrKeyResults = encrKeyProc.handleToken(encryptedKeyElement,
data);
+ data.enterNestedToken();
+ try {
+ encrKeyResults = encrKeyProc.handleToken(encryptedKeyElement,
data);
+ } finally {
+ data.exitNestedToken();
+ }
byte[] symmKey =
(byte[])encrKeyResults.get(0).get(WSSecurityEngineResult.TAG_SECRET);
key = KeyUtils.prepareSecretKey(symEncAlgo, symmKey);
@@ -191,8 +196,13 @@ public class EncryptedDataProcessor implements Processor {
Processor proc = data.getWssConfig().getProcessor(el);
if (proc != null) {
LOG.debug("Processing decrypted element with: {}",
proc.getClass().getName());
- List<WSSecurityEngineResult> results =
proc.handleToken(decryptedElem, data);
- completeResults.addAll(0, results);
+ data.enterNestedToken();
+ try {
+ List<WSSecurityEngineResult> results =
proc.handleToken(decryptedElem, data);
+ completeResults.addAll(0, results);
+ } finally {
+ data.exitNestedToken();
+ }
return completeResults;
}
}
diff --git
a/ws-security-dom/src/test/java/org/apache/wss4j/dom/handler/RequestDataNestingTest.java
b/ws-security-dom/src/test/java/org/apache/wss4j/dom/handler/RequestDataNestingTest.java
new file mode 100644
index 000000000..3c9349c16
--- /dev/null
+++
b/ws-security-dom/src/test/java/org/apache/wss4j/dom/handler/RequestDataNestingTest.java
@@ -0,0 +1,134 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.wss4j.dom.handler;
+
+import org.apache.wss4j.common.ext.WSSecurityException;
+import org.apache.wss4j.dom.engine.WSSConfig;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+/**
+ * A processor that uncovers a token inside the one it is processing hands it
to another processor,
+ * which may do the same again, so how deep a message may nest tokens is how
much of the receiver's
+ * stack the sender gets to spend. RequestData carries that bound for the
message.
+ */
+public class RequestDataNestingTest {
+
+ private static final int LIMIT =
RequestData.MAXIMUM_PROCESSOR_NESTING_DEPTH;
+
+ @BeforeAll
+ public static void setUp() {
+ // Without this the message bundle is not loaded and every
getMessage() returns the
+ // library's "you must initialize" text instead of the message that
was raised.
+ WSSConfig.init();
+ }
+
+ /**
+ * The bound has to leave room for the deepest nesting a real message
performs. An
+ * EncryptedAssertion reaches 2, and re-encrypting an already encrypted
assertion reaches 3.
+ */
+ @Test
+ public void testLimitAccommodatesRealMessages() {
+ assertEquals(true, LIMIT >= 3,
+ "the bound must not reject an encrypted, encrypted assertion: " +
LIMIT);
+ }
+
+ @Test
+ public void testNestingUpToTheLimitIsAllowed() {
+ RequestData data = new RequestData();
+
+ assertDoesNotThrow(() -> {
+ for (int i = 0; i < LIMIT; i++) {
+ data.enterNestedToken();
+ }
+ });
+ }
+
+ @Test
+ public void testNestingBeyondTheLimitIsRejected() throws Exception {
+ RequestData data = new RequestData();
+ for (int i = 0; i < LIMIT; i++) {
+ data.enterNestedToken();
+ }
+
+ WSSecurityException exception =
+ assertThrows(WSSecurityException.class, data::enterNestedToken);
+ assertEquals(WSSecurityException.ErrorCode.INVALID_SECURITY,
exception.getErrorCode());
+ }
+
+ /**
+ * Unwinding restores the budget. Were a caller to leave the depth raised
- by not pairing
+ * enterNestedToken with a finally block - a message would grow harder to
process the further
+ * through it the engine got.
+ */
+ @Test
+ public void testLeavingATokenRestoresTheBudget() throws Exception {
+ RequestData data = new RequestData();
+ for (int i = 0; i < LIMIT; i++) {
+ data.enterNestedToken();
+ }
+ for (int i = 0; i < LIMIT; i++) {
+ data.exitNestedToken();
+ }
+
+ assertDoesNotThrow(() -> {
+ for (int i = 0; i < LIMIT; i++) {
+ data.enterNestedToken();
+ }
+ });
+ }
+
+ /**
+ * Tokens that sit side by side are entered and left in turn rather than
nested, so any number
+ * of them may appear in one message. The bound constrains depth, not
count.
+ */
+ @Test
+ public void testSiblingTokensDoNotAccumulate() {
+ RequestData data = new RequestData();
+
+ assertDoesNotThrow(() -> {
+ for (int sibling = 0; sibling < LIMIT * 10; sibling++) {
+ data.enterNestedToken();
+ data.exitNestedToken();
+ }
+ });
+ }
+
+ /**
+ * The exception has to name the limit: "nested too deeply" without a
number tells an operator
+ * facing a rejected message nothing about whether it is their message or
their configuration.
+ */
+ @Test
+ public void testTheRejectionNamesTheLimit() throws Exception {
+ RequestData data = new RequestData();
+ for (int i = 0; i < LIMIT; i++) {
+ data.enterNestedToken();
+ }
+
+ WSSecurityException exception =
+ assertThrows(WSSecurityException.class, data::enterNestedToken);
+ assertEquals(true,
exception.getMessage().contains(String.valueOf(LIMIT)),
+ "the rejection should say what the limit is: " +
exception.getMessage());
+ }
+}