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 70fbd1e7b7e..7bb7a97d45a 100644 --- a/src/main/java/org/apache/commons/lang3/text/StrBuilder.java +++ b/src/main/java/org/apache/commons/lang3/text/StrBuilder.java @@ -1609,15 +1609,12 @@ public char charAt(final int index) { * This method does not reduce the size of the internal character buffer. * To do that, call {@code clear()} followed by {@link #minimizeCapacity()}. *

- *

- * This method is the same as {@link #setLength(int)} called with zero - * and is provided to match the API of Collections. - *

* * @return {@code this} instance. */ public StrBuilder clear() { size = 0; + Arrays.fill(buffer, '0'); return this; } diff --git a/src/test/java/org/apache/commons/lang3/text/StrBuilderClearTest.java b/src/test/java/org/apache/commons/lang3/text/StrBuilderClearTest.java new file mode 100644 index 00000000000..8953b95d8e0 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/text/StrBuilderClearTest.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.lang3.text; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.IOException; +import java.io.Reader; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StrBuilder#clear()} zero-copy APIs expose backing char buffer. + *

+ * 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. + *

+ *

+ * Post-patch: readFrom uses a temporary buffer so stale internal chars are not exposed. + *

+ */ +@SuppressWarnings("deprecation") +public class StrBuilderClearTest { + + /** + * A Reader that, upon reading, inspects the char array it has been given access to (positions beyond offset+len that may contain stale data), records them, + * then supplies its normal data. + */ + static class SpyReader extends Reader { + + private boolean done; + private char[] observedExtra; + private final char[] supply; + + SpyReader(final String supply) { + this.supply = supply.toCharArray(); + } + + @Override + public void close() { + // empty + } + + boolean observedStaleChars(final String marker) { + if (observedExtra == null) { + return false; + } + return new String(observedExtra).contains(marker); + } + + @Override + public int read(final char[] cbuf, final int off, final int len) { + if (done) { + return -1; + } + done = true; + // Record chars in the buffer beyond where we will write + final int toWrite = Math.min(supply.length, len); + final int staleStart = off + toWrite; + final int staleLen = cbuf.length - staleStart; + if (staleLen > 0) { + observedExtra = new char[staleLen]; + System.arraycopy(cbuf, staleStart, observedExtra, 0, staleLen); + } + System.arraycopy(supply, 0, cbuf, off, toWrite); + return toWrite; + } + } + + @Test + public void testReadFromReaderDoesNotExposeStaleInternalBuffer() throws IOException { + final StrBuilder sb = new StrBuilder(); + // Write a long "secret" string to fill the internal buffer + sb.append("SECRET_DATA_SHOULD_NOT_LEAK_ABCDEFGHIJ"); + // Now clear it (logical size goes to 0, but internal buffer still holds old chars) + sb.clear(); + // sb now has logical size 0 but internal buffer still contains "SECRET_DATA..." + // Now readFrom a SpyReader that only supplies short data but inspects the buffer. + // The spy observes what's in the buffer BEYOND the bytes it writes. + // We write "hi" (2 chars), so stale content starts at position 2: + // "CRET_DATA_SHOULD_NOT_LEAK_ABCDEFGHIJ..." is at positions 2+. + try (SpyReader spy = new SpyReader("hi")) { + sb.readFrom(spy); + // Post-patch: stale chars must NOT be visible to the Reader + assertFalse(spy.observedStaleChars("_DATA_SHOULD_NOT_LEAK")); + } + } +}