This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git


The following commit(s) were added to refs/heads/master by this push:
     new a52aaee7c Putting max on UsernameToken stax iterations (#707)
a52aaee7c is described below

commit a52aaee7c7368e0489656176c2e03e9aa7387d70
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 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());
+        });
+    }
+}

Reply via email to