From 4d47755ea76f2995643f64e6a7ac7285fae8bc76 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Sun, 24 May 2026 11:56:11 -0400 Subject: [PATCH] StrBuilder.replaceImpl shrink-branch leaves residual chars in buffer tail --- .../apache/commons/lang3/text/StrBuilder.java | 3 + .../lang3/text/StrBuilderClearTest.java | 62 +++++++++++++------ 2 files changed, 45 insertions(+), 20 deletions(-) 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 aa98cd992bc..39277460c6a 100644 --- a/src/main/java/org/apache/commons/lang3/text/StrBuilder.java +++ b/src/main/java/org/apache/commons/lang3/text/StrBuilder.java @@ -2705,6 +2705,9 @@ private void replaceImpl(final int startIndex, final int endIndex, final int rem if (insertLen != removeLen) { ensureCapacity(newSize); System.arraycopy(buffer, endIndex, buffer, startIndex + insertLen, size - endIndex); + if (size > newSize) { + ArrayFill.clear(buffer, newSize, size); + } size = newSize; } if (insertLen > 0) { 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 e499a1d20f0..328d4c92a4e 100644 --- a/src/test/java/org/apache/commons/lang3/text/StrBuilderClearTest.java +++ b/src/test/java/org/apache/commons/lang3/text/StrBuilderClearTest.java @@ -143,28 +143,30 @@ public void testReadFromReaderDoesNotExposeStaleInternalBuffer() throws IOExcept } @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")); + void testReplaceImplLeavesResidue() throws Exception { + final String string = "SECRET_PASSWORD_DATA"; + final StrBuilder sb = new StrBuilder(string); + assertEquals(20, sb.length()); + // Shrink: replace [0,20) with "X" => removeLen=20, insertLen=1, newSize=1. + sb.replace(0, 20, "X"); + assertEquals(1, sb.length()); + assertEquals("X", sb.toString()); + final char[] buf = sb.getBuffer(); + assertTrue(buf.length >= 20); + // Tail [1..20) should be cleared but isn't on baseline => residue persists. + // Probe offset 5: original was '_' (underscore from "SECRET_..."). After + // arraycopy(buf, endIndex=20, buf, startIndex+insertLen=1, size-endIndex=0) + // the shift is a no-op, so buf[5] retains the original 'T' from "SECRET_". + // Either way it is non-NUL. + assertEquals(CharUtils.NUL, buf[5]); + // Dump the visible residue at the logical-unused tail. + for (int i = 1; i < 20; i++) { + assertEquals(CharUtils.NUL, buf[i]); + } } @Test - void testSetLengthShrinkLeavesResidual() throws Exception { + void testSetLengthShrinkLeavesResidue() throws Exception { final String string = "CONFIDENTIAL_TOKEN_VALUE"; final int len = string.length(); final StrBuilder sb = new StrBuilder(string); @@ -177,9 +179,29 @@ void testSetLengthShrinkLeavesResidual() throws Exception { assertTrue(buf.length >= len); // Probe offset 10: original was 'L' (CONFIDENTIA*L*_TOKEN_VALUE). assertEquals(CharUtils.NUL, buf[10]); - final StringBuilder dump = new StringBuilder(); for (int i = 5; i < len; i++) { assertEquals(CharUtils.NUL, buf[i]); } } + + @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")); + } }