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

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git


The following commit(s) were added to refs/heads/master by this push:
     new 18979f30e StrBuilder.deleteImpl(int, int, int) doesn't clear its 
unused bytes. (#1654)
18979f30e is described below

commit 18979f30e963081e5e0f018448dd3aadc429f51e
Author: Gary Gregory <[email protected]>
AuthorDate: Sun May 17 22:35:33 2026 -0400

    StrBuilder.deleteImpl(int, int, int) doesn't clear its unused bytes. (#1654)
    
    * StrBuilder.deleteImpl(int, int, int) doesn't clear its unused bytes.
    
    * Simplify test
---
 .../org/apache/commons/lang3/text/StrBuilder.java  |  3 +-
 .../commons/lang3/text/StrBuilderClearTest.java    | 55 ++++++++++++++++++++++
 2 files changed, 57 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/apache/commons/lang3/text/StrBuilder.java 
b/src/main/java/org/apache/commons/lang3/text/StrBuilder.java
index 7bb7a97d4..a72c22745 100644
--- a/src/main/java/org/apache/commons/lang3/text/StrBuilder.java
+++ b/src/main/java/org/apache/commons/lang3/text/StrBuilder.java
@@ -1614,7 +1614,7 @@ public char charAt(final int index) {
      */
     public StrBuilder clear() {
         size = 0;
-        Arrays.fill(buffer, '0');
+        Arrays.fill(buffer, CharUtils.NUL);
         return this;
     }
 
@@ -1810,6 +1810,7 @@ public StrBuilder deleteFirst(final StrMatcher matcher) {
     private void deleteImpl(final int startIndex, final int endIndex, final 
int len) {
         System.arraycopy(buffer, endIndex, buffer, startIndex, size - 
endIndex);
         size -= len;
+        Arrays.fill(buffer, size, size + len, CharUtils.NUL);
     }
 
     /**
diff --git 
a/src/test/java/org/apache/commons/lang3/text/StrBuilderClearTest.java 
b/src/test/java/org/apache/commons/lang3/text/StrBuilderClearTest.java
index 8953b95d8..9765362b0 100644
--- a/src/test/java/org/apache/commons/lang3/text/StrBuilderClearTest.java
+++ b/src/test/java/org/apache/commons/lang3/text/StrBuilderClearTest.java
@@ -19,9 +19,13 @@
 
 import static org.junit.jupiter.api.Assertions.assertFalse;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.io.ObjectInputStream;
 import java.io.Reader;
+import java.nio.charset.StandardCharsets;
 
+import org.apache.commons.lang3.SerializationUtils;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -86,6 +90,36 @@ public int read(final char[] cbuf, final int off, final int 
len) {
         }
     }
 
+    /** Search for a string encoded as UTF-16BE (2 bytes per char) in a byte 
array. */
+    private static boolean containsUtf16Be(final byte[] haystack, final String 
needle) throws IOException {
+        final byte[] needleBytes = needle.getBytes(StandardCharsets.UTF_16BE);
+        outer: for (int i = 0; i <= haystack.length - needleBytes.length; i++) 
{
+            for (int j = 0; j < needleBytes.length; j++) {
+                if (haystack[i + j] != needleBytes[j]) {
+                    continue outer;
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Test
+    public void testDeserializedStrBuilderHasNoStaleBufferContent() throws 
Exception {
+        final StrBuilder sb = new StrBuilder("secret_password_xyzzy");
+        sb.clear();
+        sb.append("safe");
+        final byte[] serialized = SerializationUtils.serialize(sb);
+        final StrBuilder sb2;
+        // Deserialize and inspect the buffer
+        try (ObjectInputStream ois = new ObjectInputStream(new 
ByteArrayInputStream(serialized))) {
+            sb2 = (StrBuilder) ois.readObject();
+        }
+        final char[] buf2 = sb2.buffer;
+        final String bufContent = new String(buf2);
+        assertFalse(bufContent.contains("secret_password"), "Deserialized 
StrBuilder buffer must not contain stale chars: " + bufContent);
+    }
+
     @Test
     public void testReadFromReaderDoesNotExposeStaleInternalBuffer() throws 
IOException {
         final StrBuilder sb = new StrBuilder();
@@ -104,4 +138,25 @@ public void 
testReadFromReaderDoesNotExposeStaleInternalBuffer() throws IOExcept
             assertFalse(spy.observedStaleChars("_DATA_SHOULD_NOT_LEAK"));
         }
     }
+
+    @Test
+    public void testStaleCharsNotLeakedAfterClear() throws Exception {
+        final StrBuilder sb = new StrBuilder("secret_password_xyzzy_leak");
+        // clear() resets logical size to 0 but leaves chars in buffer
+        sb.clear();
+        // append something shorter than the original
+        sb.append("ok");
+        // Stale content is serialized as UTF-16BE char[] data.
+        // "xyzzy_leak" was at positions 15+, well beyond "ok" (len=2), so 
must not appear.
+        assertFalse(containsUtf16Be(SerializationUtils.serialize(sb), 
"xyzzy_leak"));
+    }
+
+    @Test
+    public void testStaleCharsNotLeakedAfterTruncate() throws Exception {
+        final StrBuilder sb = new StrBuilder("top_secret_key_material");
+        // truncate to a short length – tail remains in buffer
+        sb.delete(6, sb.length());
+        // sb now logically contains "top_se"
+        assertFalse(containsUtf16Be(SerializationUtils.serialize(sb), 
"secret_key_material"));
+    }
 }

Reply via email to