diff --git a/src/main/java/org/apache/commons/text/StrBuilder.java b/src/main/java/org/apache/commons/text/StrBuilder.java index 6c2c568538..7dbd1b89de 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 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, '0'); } /** diff --git a/src/test/java/org/apache/commons/text/StrBuilderClearTest.java b/src/test/java/org/apache/commons/text/StrBuilderClearTest.java index cecaa7ecf9..8088355061 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 @@ 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 @@ * 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. *

- * *

* Pre-patch: A Reader can inspect chars beyond the current write position. *

@@ -86,6 +90,40 @@ 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 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 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")); + } }