You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ws.apache.org by co...@apache.org on 2026/09/18 10:38:41 UTC
(ws-wss4j) branch 3_0_x-fixes updated: Putting max on UsernameToken stax iterations (#707)
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 24258ddf1 Putting max on UsernameToken stax iterations (#707)
24258ddf1 is described below
commit 24258ddf115f0dbfa4af8066ba3011165ae193e2
Author: Colm O hEigeartaigh <co...@users.noreply.github.com>
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 9989367d6..c2f0dd9d7 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 a008bd902..71c2c7698 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 aa6e4f701..552de57fb 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 e83a831f1..c06d5edbf 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());
+ });
+ }
+}