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"));
+ }
}