This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch 2_4_x-fixes
in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git
The following commit(s) were added to refs/heads/2_4_x-fixes by this push:
new 587b46df3 Putting max on UsernameToken stax iterations (#707)
587b46df3 is described below
commit 587b46df313f56249a5792dfed63af71527c742d
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri Sep 18 11:38:13 2026 +0100
Putting max on UsernameToken stax iterations (#707)
---
.../wss4j/common/util/UsernameTokenUtil.java | 8 ++
.../wss4j/dom/message/token/UsernameToken.java | 6 +-
.../apache/wss4j/dom/message/UTDerivedKeyTest.java | 40 ++++++
.../securityToken/UsernameSecurityTokenImpl.java | 15 ++
.../stax/test/UsernameTokenIterationTest.java | 156 +++++++++++++++++++++
5 files changed, 223 insertions(+), 2 deletions(-)
diff --git
a/ws-security-common/src/main/java/org/apache/wss4j/common/util/UsernameTokenUtil.java
b/ws-security-common/src/main/java/org/apache/wss4j/common/util/UsernameTokenUtil.java
index 5caf0c3a8..12096245d 100644
---
a/ws-security-common/src/main/java/org/apache/wss4j/common/util/UsernameTokenUtil.java
+++
b/ws-security-common/src/main/java/org/apache/wss4j/common/util/UsernameTokenUtil.java
@@ -36,6 +36,14 @@ import org.apache.xml.security.stax.ext.XMLSecurityConstants;
public final class UsernameTokenUtil {
public static final int DEFAULT_ITERATION = 1000;
+ /**
+ * The maximum number of hash rounds that an inbound UsernameToken may
request through its
+ * wsse11:Iteration element. The Iteration value is attacker-controlled
message content and
+ * the key derivation below performs one SHA-1 round per iteration, so
leaving it unbounded
+ * lets a small request buy an arbitrary amount of CPU time on the
receiver.
+ */
+ public static final int MAX_ITERATION = 10000;
+
private static final org.slf4j.Logger LOG =
org.slf4j.LoggerFactory.getLogger(UsernameTokenUtil.class);
diff --git
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/message/token/UsernameToken.java
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/message/token/UsernameToken.java
index 73929c526..8a5b7f7c3 100644
---
a/ws-security-dom/src/main/java/org/apache/wss4j/dom/message/token/UsernameToken.java
+++
b/ws-security-dom/src/main/java/org/apache/wss4j/dom/message/token/UsernameToken.java
@@ -153,11 +153,13 @@ public class UsernameToken {
if (iter != null) {
try {
iteration = Integer.parseInt(iter);
- if (iteration < 0 || iteration > 10000) {
+ if (iteration < 0 || iteration >
UsernameTokenUtil.MAX_ITERATION) {
throw new WSSecurityException(
WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
"badUsernameToken",
- new Object[] {"Iteration is missing"}
+ new Object[] {"Iteration of " + iteration
+ + " is outside the allowed range [0,
"
+ + UsernameTokenUtil.MAX_ITERATION +
"]"}
);
}
} catch (NumberFormatException ex) {
diff --git
a/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/UTDerivedKeyTest.java
b/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/UTDerivedKeyTest.java
index 7ab706cc4..01fdf700c 100644
---
a/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/UTDerivedKeyTest.java
+++
b/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/UTDerivedKeyTest.java
@@ -25,6 +25,7 @@ import java.util.Collections;
import javax.security.auth.callback.CallbackHandler;
+import org.apache.wss4j.common.bsp.BSPEnforcer;
import org.apache.wss4j.common.bsp.BSPRule;
import org.apache.wss4j.common.crypto.Crypto;
import org.apache.wss4j.common.crypto.CryptoFactory;
@@ -47,9 +48,12 @@ import org.apache.wss4j.dom.util.WSSecurityUtil;
import org.junit.jupiter.api.Test;
import org.w3c.dom.Document;
+import org.w3c.dom.Element;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -839,6 +843,42 @@ public class UTDerivedKeyTest {
}
+ /**
+ * The wsse11:Iteration value is attacker-controlled message content and
the key derivation
+ * performs one SHA-1 round per iteration, so the parser bounds it.
Regression test for the
+ * bound itself, which is shared with the streaming engine
+ * (see UsernameTokenUtil.MAX_ITERATION).
+ */
+ @Test
+ public void testIterationAboveMaximumIsRejected() throws Exception {
+ Element tokenElement =
buildDerivedKeyTokenElement(UsernameTokenUtil.MAX_ITERATION + 1);
+
+ WSSecurityException exception = assertThrows(WSSecurityException.class,
+ () -> new UsernameToken(tokenElement, false, new
BSPEnforcer(true)));
+ assertEquals(WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
exception.getErrorCode());
+ }
+
+ @Test
+ public void testMaximumIterationIsAccepted() throws Exception {
+ Element tokenElement =
buildDerivedKeyTokenElement(UsernameTokenUtil.MAX_ITERATION);
+
+ UsernameToken token = new UsernameToken(tokenElement, false, new
BSPEnforcer(true));
+ assertEquals(UsernameTokenUtil.MAX_ITERATION, token.getIteration());
+ }
+
+ private Element buildDerivedKeyTokenElement(int iteration) throws
Exception {
+ Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
+ WSSecHeader secHeader = new WSSecHeader(doc);
+ secHeader.insertSecurityHeader();
+
+ WSSecUsernameToken builder = new WSSecUsernameToken(secHeader);
+ builder.setUserInfo("bob", "security");
+ builder.addDerivedKey(iteration);
+ builder.prepare(UsernameTokenUtil.generateSalt(false));
+
+ return builder.getUsernameTokenElement();
+ }
+
/**
* Verifies the soap envelope.
*
diff --git
a/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/securityToken/UsernameSecurityTokenImpl.java
b/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/securityToken/UsernameSecurityTokenImpl.java
index 3ac3bc5d2..df524f557 100644
---
a/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/securityToken/UsernameSecurityTokenImpl.java
+++
b/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/securityToken/UsernameSecurityTokenImpl.java
@@ -115,6 +115,21 @@ public class UsernameSecurityTokenImpl extends
AbstractInboundSecurityToken impl
*/
protected byte[] generateDerivedKey(WSInboundSecurityContext
wsInboundSecurityContext) throws WSSecurityException {
+ // Guard against a malicious user sending a bogus iteration value. The
derivation performs
+ // one SHA-1 round per iteration, so an unbounded value turns a small
request into an
+ // arbitrary amount of CPU work on the receiver. The DOM code rejects
anything outside this
+ // range while parsing the token (see UsernameToken); this is the
streaming equivalent.
+ //
+ // The bound is applied to the Long before it is narrowed to an int,
so that a value larger
+ // than Integer.MAX_VALUE cannot wrap round into a small,
acceptable-looking iteration count.
+ if (iteration != null && (iteration < 0 || iteration >
UsernameTokenUtil.MAX_ITERATION)) {
+ throw new WSSecurityException(
+ WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
+ "badUsernameToken",
+ new Object[] {"Iteration of " + iteration + " is outside the
allowed range [0, "
+ + UsernameTokenUtil.MAX_ITERATION + "]"});
+ }
+
if (wsInboundSecurityContext != null) {
if (salt == null || salt.length == 0) {
wsInboundSecurityContext.handleBSPRule(BSPRule.R4217);
diff --git
a/ws-security-stax/src/test/java/org/apache/wss4j/stax/test/UsernameTokenIterationTest.java
b/ws-security-stax/src/test/java/org/apache/wss4j/stax/test/UsernameTokenIterationTest.java
new file mode 100644
index 000000000..6e9b6b987
--- /dev/null
+++
b/ws-security-stax/src/test/java/org/apache/wss4j/stax/test/UsernameTokenIterationTest.java
@@ -0,0 +1,156 @@
+/**
+ * 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.stax.test;
+
+import java.time.Duration;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+
+import org.apache.wss4j.common.ext.WSSecurityException;
+import org.apache.wss4j.common.util.DateUtil;
+import org.apache.wss4j.common.util.UsernameTokenUtil;
+import org.apache.wss4j.stax.ext.WSInboundSecurityContext;
+import org.apache.wss4j.stax.ext.WSSConstants;
+import org.apache.wss4j.stax.impl.InboundWSSecurityContextImpl;
+import org.apache.wss4j.stax.impl.securityToken.UsernameSecurityTokenImpl;
+import org.apache.wss4j.stax.securityToken.WSSecurityTokenConstants;
+import org.apache.wss4j.stax.setup.WSSec;
+import org.apache.xml.security.stax.impl.util.IDGenerator;
+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.assertTimeoutPreemptively;
+
+/**
+ * Tests that the wsse11:Iteration value of an inbound UsernameToken is
bounded before it is used
+ * to derive a key. The value is attacker-controlled message content and the
derivation performs
+ * one SHA-1 round per iteration, so without a bound a ~1 KB request buys an
arbitrary amount of
+ * CPU time on the receiver. The DOM engine has always rejected out-of-range
values while parsing
+ * the token; these tests cover the streaming equivalent.
+ */
+public class UsernameTokenIterationTest {
+
+ private static final byte[] SALT = new byte[16];
+
+ @BeforeAll
+ public static void setUp() throws Exception {
+ WSSec.init();
+ }
+
+ private UsernameSecurityTokenImpl createToken(Long iteration,
WSInboundSecurityContext context) {
+ String created =
+
DateUtil.getDateTimeFormatter(true).format(ZonedDateTime.now(ZoneOffset.UTC));
+ return new UsernameSecurityTokenImpl(
+ WSSConstants.UsernameTokenPasswordType.PASSWORD_NONE,
+ "username", "password", created, null, SALT, iteration,
+ context, IDGenerator.generateID(null),
+
WSSecurityTokenConstants.KEYIDENTIFIER_SECURITY_TOKEN_DIRECT_REFERENCE);
+ }
+
+ @Test
+ public void testIterationAboveMaximumIsRejected() {
+ UsernameSecurityTokenImpl token =
+ createToken((long)UsernameTokenUtil.MAX_ITERATION + 1, null);
+
+ WSSecurityException exception =
+ assertThrows(WSSecurityException.class, token::generateDerivedKey);
+ assertEquals(WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
exception.getErrorCode());
+ }
+
+ /**
+ * The amplification case: a single small request asking for ~2^31 SHA-1
rounds. The bound has
+ * to reject it rather than perform the work, so the derivation must
return promptly. The
+ * timeout is generous - the guarded path fails in microseconds, whereas
actually running
+ * Integer.MAX_VALUE rounds takes minutes - so this only fails if the
bound is gone.
+ */
+ @Test
+ public void testHugeIterationIsRejectedWithoutDoingTheWork() {
+ UsernameSecurityTokenImpl token = createToken((long)Integer.MAX_VALUE,
null);
+
+ assertTimeoutPreemptively(Duration.ofSeconds(60), () -> {
+ WSSecurityException exception =
+ assertThrows(WSSecurityException.class,
token::generateDerivedKey);
+ assertEquals(WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
+ exception.getErrorCode());
+ });
+ }
+
+ /**
+ * An Iteration larger than Integer.MAX_VALUE must be rejected outright.
It must not narrow to
+ * a negative or zero int and thereby be silently accepted as the default
iteration count -
+ * which is what happens if the bound is applied after the Long has been
converted to an int.
+ */
+ @Test
+ public void testIterationAboveIntegerRangeIsRejected() {
+ for (long iteration : new long[] {Integer.MAX_VALUE + 1L, 1L << 32,
Long.MAX_VALUE}) {
+ UsernameSecurityTokenImpl token = createToken(iteration, null);
+
+ WSSecurityException exception =
+ assertThrows(WSSecurityException.class,
token::generateDerivedKey,
+ "Iteration " + iteration + " should have been
rejected");
+ assertEquals(WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
+ exception.getErrorCode());
+ }
+ }
+
+ @Test
+ public void testNegativeIterationIsRejected() {
+ UsernameSecurityTokenImpl token = createToken(-1L, null);
+
+ WSSecurityException exception =
+ assertThrows(WSSecurityException.class, token::generateDerivedKey);
+ assertEquals(WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
exception.getErrorCode());
+ }
+
+ @Test
+ public void testMaximumIterationIsAccepted() throws Exception {
+ UsernameSecurityTokenImpl token =
+ createToken((long)UsernameTokenUtil.MAX_ITERATION, null);
+
+ assertEquals(20, token.generateDerivedKey().length);
+ }
+
+ @Test
+ public void testDefaultIterationIsAccepted() throws Exception {
+ UsernameSecurityTokenImpl token =
createToken((long)UsernameTokenUtil.DEFAULT_ITERATION, null);
+
+ assertEquals(20, token.generateDerivedKey().length);
+ }
+
+ /**
+ * The bound is an engine-level limit, not a BSP rule, so turning BSP
enforcement off must not
+ * re-open it.
+ */
+ @Test
+ public void testBoundIsEnforcedWithBSPEnforcementDisabled() {
+ InboundWSSecurityContextImpl securityContext = new
InboundWSSecurityContextImpl();
+ securityContext.setDisableBSPEnforcement(true);
+
+ UsernameSecurityTokenImpl token = createToken((long)Integer.MAX_VALUE,
securityContext);
+
+ assertTimeoutPreemptively(Duration.ofSeconds(60), () -> {
+ WSSecurityException exception =
+ assertThrows(WSSecurityException.class,
token::generateDerivedKey);
+ assertEquals(WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
+ exception.getErrorCode());
+ });
+ }
+}