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

commit ca8f52da755dcc334b03a8f6d00f198a3089cb08
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri Sep 18 17:41:56 2026 +0100

    Redact passwords from UsernameToken toString (#711)
---
 .../wss4j/dom/message/token/UsernameToken.java     |  13 ++-
 .../message/token/UsernameTokenToStringTest.java   | 118 +++++++++++++++++++++
 2 files changed, 129 insertions(+), 2 deletions(-)

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 71c2c7698..db47b5335 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
@@ -544,12 +544,21 @@ public class UsernameToken {
     }
 
     /**
-     * Returns the string representation of the token.
+     * Returns the string representation of the token, with the content of any 
wsse:Password
+     * redacted.
      *
      * @return a XML string representation
      */
     public String toString() {
-        return DOM2Writer.nodeToString(element);
+        Element redacted = (Element)element.cloneNode(true);
+        Element passwordElement =
+            XMLUtils.getDirectChildElement(
+                redacted, WSConstants.PASSWORD_LN, WSConstants.WSSE_NS
+            );
+        if (passwordElement != null) {
+            passwordElement.setTextContent("***");
+        }
+        return DOM2Writer.nodeToString(redacted);
     }
 
     /**
diff --git 
a/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/token/UsernameTokenToStringTest.java
 
b/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/token/UsernameTokenToStringTest.java
new file mode 100644
index 000000000..51f063af3
--- /dev/null
+++ 
b/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/token/UsernameTokenToStringTest.java
@@ -0,0 +1,118 @@
+/**
+ * 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.message.token;
+
+import org.apache.wss4j.common.util.SOAPUtil;
+import org.apache.wss4j.dom.WSConstants;
+import org.apache.wss4j.dom.engine.WSSConfig;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A UsernameToken object reaches a log line or an exception message far more 
readily than a
+ * password does by any other route, so toString() must not hand the password 
over with it.
+ */
+public class UsernameTokenToStringTest {
+
+    private static final String PASSWORD = "SuperSecretPassword123";
+
+    public UsernameTokenToStringTest() {
+        WSSConfig.init();
+    }
+
+    @Test
+    public void testPlaintextPasswordIsRedacted() throws Exception {
+        Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
+        UsernameToken usernameToken = new UsernameToken(true, doc, 
WSConstants.PASSWORD_TEXT);
+        usernameToken.setName("bob");
+        usernameToken.setPassword(PASSWORD);
+
+        String serialized = usernameToken.toString();
+
+        assertFalse(serialized.contains(PASSWORD),
+            "toString() must not disclose the password: " + serialized);
+        assertTrue(serialized.contains("bob"),
+            "toString() should still identify the token: " + serialized);
+        assertTrue(serialized.contains("***"),
+            "the password element should still be there, redacted: " + 
serialized);
+    }
+
+    /**
+     * A password digest is not the password, but it is the value an offline 
attack runs against,
+     * so it is withheld in the same way.
+     */
+    @Test
+    public void testDigestPasswordIsRedacted() throws Exception {
+        Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
+        // the PASSWORD_DIGEST constructor adds the Nonce and Created itself
+        UsernameToken usernameToken = new UsernameToken(true, doc, 
WSConstants.PASSWORD_DIGEST);
+        usernameToken.setName("bob");
+        usernameToken.setPassword(PASSWORD);
+
+        String digest = usernameToken.getPassword();
+        String serialized = usernameToken.toString();
+
+        assertFalse(serialized.contains(PASSWORD), "toString() disclosed the 
password");
+        assertFalse(serialized.contains(digest),
+            "toString() must not disclose the password digest: " + serialized);
+        assertTrue(serialized.contains("***"), serialized);
+    }
+
+    /**
+     * Redaction happens on a copy: the token itself must be untouched, or the 
password would be
+     * destroyed by the act of logging it.
+     */
+    @Test
+    public void testRedactionDoesNotAlterTheToken() throws Exception {
+        Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
+        UsernameToken usernameToken = new UsernameToken(true, doc, 
WSConstants.PASSWORD_TEXT);
+        usernameToken.setName("bob");
+        usernameToken.setPassword(PASSWORD);
+
+        usernameToken.toString();
+
+        assertEquals(PASSWORD, usernameToken.getPassword(),
+            "toString() must not modify the token it was called on");
+        assertEquals(PASSWORD,
+            usernameToken.getElement().getElementsByTagNameNS(
+                WSConstants.WSSE_NS, 
WSConstants.PASSWORD_LN).item(0).getTextContent(),
+            "toString() must not modify the underlying element");
+    }
+
+    /**
+     * A UsernameToken carrying no password at all - the key derivation case - 
still serialises.
+     */
+    @Test
+    public void testTokenWithoutPasswordIsUnaffected() throws Exception {
+        Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
+        UsernameToken usernameToken = new UsernameToken(true, doc, null);
+        usernameToken.setName("bob");
+
+        String serialized = usernameToken.toString();
+
+        assertTrue(serialized.contains("bob"), serialized);
+        assertFalse(serialized.contains("***"),
+            "nothing to redact, so nothing should be redacted: " + serialized);
+    }
+}

Reply via email to