1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.text;
19
20 import static org.junit.jupiter.api.Assertions.assertEquals;
21 import static org.junit.jupiter.api.Assertions.assertFalse;
22 import static org.junit.jupiter.api.Assertions.assertTrue;
23
24 import java.io.ByteArrayInputStream;
25 import java.io.IOException;
26 import java.io.ObjectInputStream;
27 import java.nio.charset.StandardCharsets;
28
29 import org.apache.commons.lang3.CharUtils;
30 import org.apache.commons.lang3.SerializationUtils;
31 import org.junit.jupiter.api.Test;
32
33 /**
34 * Tests {@link StrBuilder#clear()} zero-copy APIs expose backing char buffer.
35 * <p>
36 * 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
37 * beyond the logical content.
38 * </p>
39 * <p>
40 * Pre-patch: A Reader can inspect chars beyond the current write position.
41 * </p>
42 * <p>
43 * Post-patch: readFrom uses a temporary buffer so stale internal chars are not exposed.
44 * </p>
45 */
46 @SuppressWarnings("deprecation")
47 public class StrBuilderClearTest {
48
49 /** Search for a string encoded as UTF-16BE (2 bytes per char) in a byte array. */
50 private static boolean containsUtf16Be(final byte[] haystack, final String needle) throws IOException {
51 final byte[] needleBytes = needle.getBytes(StandardCharsets.UTF_16BE);
52 outer: for (int i = 0; i <= haystack.length - needleBytes.length; i++) {
53 for (int j = 0; j < needleBytes.length; j++) {
54 if (haystack[i + j] != needleBytes[j]) {
55 continue outer;
56 }
57 }
58 return true;
59 }
60 return false;
61 }
62
63 @Test
64 public void testDeserializedStrBuilderHasNoStaleBufferContent() throws Exception {
65 final StrBuilder sb = new StrBuilder("secret_password_xyzzy");
66 sb.clear();
67 sb.append("safe");
68 final byte[] serialized = SerializationUtils.serialize(sb);
69 final StrBuilder sb2;
70 // Deserialize and inspect the buffer
71 try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialized))) {
72 sb2 = (StrBuilder) ois.readObject();
73 }
74 final char[] buf2 = sb2.buffer;
75 final String bufContent = new String(buf2);
76 assertFalse(bufContent.contains("secret_password"), "Deserialized StrBuilder buffer must not contain stale chars: " + bufContent);
77 }
78
79 @Test
80 public void testReadFromReaderDoesNotExposeStaleInternalBuffer() throws IOException {
81 final StrBuilder sb = new StrBuilder();
82 // Write a long "secret" string to fill the internal buffer
83 sb.append("SECRET_DATA_SHOULD_NOT_LEAK_ABCDEFGHIJ");
84 // Now clear it (logical size goes to 0, but internal buffer still holds old chars)
85 sb.clear();
86 // sb now has logical size 0 but internal buffer still contains "SECRET_DATA..."
87 // Now readFrom a SpyReader that only supplies short data but inspects the buffer.
88 // The spy observes what's in the buffer BEYOND the bytes it writes.
89 // We write "hi" (2 chars), so stale content starts at position 2:
90 // "CRET_DATA_SHOULD_NOT_LEAK_ABCDEFGHIJ..." is at positions 2+.
91 try (SpyReader spy = new SpyReader("hi")) {
92 sb.readFrom(spy);
93 // Post-patch: stale chars must NOT be visible to the Reader
94 assertFalse(spy.observedStaleChars("_DATA_SHOULD_NOT_LEAK"));
95 }
96 }
97
98 @Test
99 void testReplaceImplLeavesResidue() throws Exception {
100 final String string = "SECRET_PASSWORD_DATA";
101 final StrBuilder sb = new StrBuilder(string);
102 assertEquals(20, sb.length());
103 // Shrink: replace [0,20) with "X" => removeLen=20, insertLen=1, newSize=1.
104 sb.replace(0, 20, "X");
105 assertEquals(1, sb.length());
106 assertEquals("X", sb.toString());
107 final char[] buf = sb.getBuffer();
108 assertTrue(buf.length >= 20);
109 // Tail [1..20) should be cleared but isn't on baseline => residue persists.
110 // Probe offset 5: original was '_' (underscore from "SECRET_..."). After
111 // arraycopy(buf, endIndex=20, buf, startIndex+insertLen=1, size-endIndex=0)
112 // the shift is a no-op, so buf[5] retains the original 'T' from "SECRET_".
113 // Either way it is non-NUL.
114 assertEquals(CharUtils.NUL, buf[5]);
115 // Dump the visible residue at the logical-unused tail.
116 for (int i = 1; i < 20; i++) {
117 assertEquals(CharUtils.NUL, buf[i]);
118 }
119 }
120
121 @Test
122 void testSetLengthShrinkLeavesResidue() throws Exception {
123 final String string = "CONFIDENTIAL_TOKEN_VALUE";
124 final int len = string.length();
125 final StrBuilder sb = new StrBuilder(string);
126 assertEquals(len, sb.length());
127 // setLength(5) shrinks: size = 5, but [5..24) is NOT cleared.
128 sb.setLength(5);
129 assertEquals(5, sb.length());
130 assertEquals("CONFI", sb.toString());
131 final char[] buf = sb.getBuffer();
132 assertTrue(buf.length >= len);
133 // Probe offset 10: original was 'L' (CONFIDENTIA*L*_TOKEN_VALUE).
134 assertEquals(CharUtils.NUL, buf[10]);
135 for (int i = 5; i < len; i++) {
136 assertEquals(CharUtils.NUL, buf[i]);
137 }
138 }
139
140 @Test
141 public void testStaleCharsNotLeakedAfterClear() throws Exception {
142 final StrBuilder sb = new StrBuilder("secret_password_xyzzy_leak");
143 // clear() resets logical size to 0 but leaves chars in buffer
144 sb.clear();
145 // append something shorter than the original
146 sb.append("ok");
147 // Stale content is serialized as UTF-16BE char[] data.
148 // "xyzzy_leak" was at positions 15+, well beyond "ok" (len=2), so must not appear.
149 assertFalse(containsUtf16Be(SerializationUtils.serialize(sb), "xyzzy_leak"));
150 }
151
152 @Test
153 public void testStaleCharsNotLeakedAfterTruncate() throws Exception {
154 final StrBuilder sb = new StrBuilder("top_secret_key_material");
155 // truncate to a short length â tail remains in buffer
156 sb.delete(6, sb.length());
157 // sb now logically contains "top_se"
158 assertFalse(containsUtf16Be(SerializationUtils.serialize(sb), "secret_key_material"));
159 }
160 }