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-text.git


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

commit 6a08b42bf91ad3daba4215a05f9c496e99ce1002
Author: Gary Gregory <[email protected]>
AuthorDate: Sun May 17 17:10:35 2026 -0400

    StrBuilder.deleteImpl(int, int, int) doesn't clear its unused bytes (#742)
---
 .../java/org/apache/commons/text/StrBuilder.java   |  1 +
 .../apache/commons/text/StrBuilderClearTest.java   | 61 +++++++++++++++++++++-
 2 files changed, 61 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/apache/commons/text/StrBuilder.java 
b/src/main/java/org/apache/commons/text/StrBuilder.java
index 6c2c5685..7dbd1b89 100644
--- a/src/main/java/org/apache/commons/text/StrBuilder.java
+++ b/src/main/java/org/apache/commons/text/StrBuilder.java
@@ -1698,6 +1698,7 @@ public class StrBuilder implements CharSequence, 
Appendable, Serializable, Build
     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, '0');
     }
 
     /**
diff --git a/src/test/java/org/apache/commons/text/StrBuilderClearTest.java 
b/src/test/java/org/apache/commons/text/StrBuilderClearTest.java
index cecaa7ec..80883550 100644
--- a/src/test/java/org/apache/commons/text/StrBuilderClearTest.java
+++ b/src/test/java/org/apache/commons/text/StrBuilderClearTest.java
@@ -19,9 +19,14 @@ package org.apache.commons.text;
 
 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.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
 
+import org.apache.commons.lang3.SerializationUtils;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -30,7 +35,6 @@ import org.junit.jupiter.api.Test;
  * readFrom(Readable) Reader branch: reads directly into the internal char[] 
buffer, so a Reader that is also an attacker can observe stale chars in that 
buffer
  * beyond the logical content.
  * </p>
- *
  * <p>
  * Pre-patch: A Reader can inspect chars beyond the current write position.
  * </p>
@@ -86,6 +90,40 @@ public class StrBuilderClearTest {
         }
     }
 
+    /** 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 Field bufField = StrBuilder.class.getDeclaredField("buffer");
+        bufField.setAccessible(true);
+        final Field sizeField = StrBuilder.class.getDeclaredField("size");
+        sizeField.setAccessible(true);
+        final char[] buf2 = (char[]) bufField.get(sb2);
+        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 +142,25 @@ public class StrBuilderClearTest {
             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