View Javadoc
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 that {@link TextStringBuilder} shrink and clear paths do not leave stale chars in the backing buffer.
35   * <p>
36   * readFrom(Reader) reads directly into the internal char[] buffer, so a Reader that is also an attacker can observe stale chars in that buffer beyond the
37   * logical content. The buffer is non-transient, so stale chars also survive serialization.
38   * </p>
39   * <p>
40   * Post-patch: clear(), delete(), replace() and setLength() NUL out the freed region so stale chars are not exposed.
41   * </p>
42   */
43  public class TextStringBuilderClearTest {
44  
45      /** Search for a string encoded as UTF-16BE (2 bytes per char) in a byte array. */
46      private static boolean containsUtf16Be(final byte[] haystack, final String needle) {
47          final byte[] needleBytes = needle.getBytes(StandardCharsets.UTF_16BE);
48          outer: for (int i = 0; i <= haystack.length - needleBytes.length; i++) {
49              for (int j = 0; j < needleBytes.length; j++) {
50                  if (haystack[i + j] != needleBytes[j]) {
51                      continue outer;
52                  }
53              }
54              return true;
55          }
56          return false;
57      }
58  
59      @Test
60      void testDeleteShrinkLeavesNoResidue() {
61          final String string = "SECRET_PASSWORD_DATA";
62          final int len = string.length();
63          final TextStringBuilder sb = new TextStringBuilder(string);
64          assertEquals(len, sb.length());
65          sb.delete(6, len);
66          assertEquals(6, sb.length());
67          assertEquals("SECRET", sb.toString());
68          final char[] buf = sb.getBuffer();
69          for (int i = 6; i < len; i++) {
70              assertEquals(CharUtils.NUL, buf[i]);
71          }
72      }
73  
74      @Test
75      public void testDeserializedBuilderHasNoStaleBufferContent() throws Exception {
76          final TextStringBuilder sb = new TextStringBuilder("secret_password_xyzzy");
77          sb.clear();
78          sb.append("safe");
79          final byte[] serialized = SerializationUtils.serialize(sb);
80          final TextStringBuilder sb2;
81          try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialized))) {
82              sb2 = (TextStringBuilder) ois.readObject();
83          }
84          final String bufContent = new String(sb2.getBuffer());
85          assertFalse(bufContent.contains("secret_password"), "Deserialized buffer must not contain stale chars: " + bufContent);
86      }
87  
88      @Test
89      public void testReadFromReaderDoesNotExposeStaleInternalBuffer() throws IOException {
90          final TextStringBuilder sb = new TextStringBuilder();
91          sb.append("SECRET_DATA_SHOULD_NOT_LEAK_ABCDEFGHIJ");
92          sb.clear();
93          try (SpyReader spy = new SpyReader("hi")) {
94              sb.readFrom(spy);
95              assertFalse(spy.observedStaleChars("_DATA_SHOULD_NOT_LEAK"));
96          }
97      }
98  
99      @Test
100     void testReplaceShrinkLeavesNoResidue() {
101         final String string = "SECRET_PASSWORD_DATA";
102         final TextStringBuilder sb = new TextStringBuilder(string);
103         assertEquals(20, sb.length());
104         // Shrink: replace [0,20) with "X" => removeLen=20, insertLen=1, newSize=1.
105         sb.replace(0, 20, "X");
106         assertEquals(1, sb.length());
107         assertEquals("X", sb.toString());
108         final char[] buf = sb.getBuffer();
109         assertTrue(buf.length >= 20);
110         for (int i = 1; i < 20; i++) {
111             assertEquals(CharUtils.NUL, buf[i]);
112         }
113     }
114 
115     @Test
116     void testSetLengthShrinkLeavesNoResidue() {
117         final String string = "CONFIDENTIAL_TOKEN_VALUE";
118         final int len = string.length();
119         final TextStringBuilder sb = new TextStringBuilder(string);
120         assertEquals(len, sb.length());
121         sb.setLength(5);
122         assertEquals(5, sb.length());
123         assertEquals("CONFI", sb.toString());
124         final char[] buf = sb.getBuffer();
125         assertTrue(buf.length >= len);
126         for (int i = 5; i < len; i++) {
127             assertEquals(CharUtils.NUL, buf[i]);
128         }
129     }
130 
131     @Test
132     public void testStaleCharsNotLeakedAfterClear() {
133         final TextStringBuilder sb = new TextStringBuilder("secret_password_xyzzy_leak");
134         sb.clear();
135         sb.append("ok");
136         assertFalse(containsUtf16Be(SerializationUtils.serialize(sb), "xyzzy_leak"));
137     }
138 
139     @Test
140     public void testStaleCharsNotLeakedAfterTruncate() {
141         final TextStringBuilder sb = new TextStringBuilder("top_secret_key_material");
142         sb.delete(6, sb.length());
143         assertFalse(containsUtf16Be(SerializationUtils.serialize(sb), "secret_key_material"));
144     }
145 }