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.codec.binary;
19  
20  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertThrows;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  
25  import java.io.ByteArrayInputStream;
26  import java.io.ByteArrayOutputStream;
27  import java.io.IOException;
28  import java.util.Arrays;
29  
30  import org.apache.commons.io.IOUtils;
31  import org.junit.jupiter.api.Test;
32  import org.junit.jupiter.params.ParameterizedTest;
33  import org.junit.jupiter.params.provider.ValueSource;
34  
35  /**
36   * Tests the maximum decode length limit that bounds the quadratic-cost Base58 decode against oversized untrusted input.
37   */
38  class Base58MaxDecodeLengthTest {
39  
40      private static byte[] ones(final int length) {
41          final byte[] bytes = new byte[length];
42          // '1' encodes a leading zero byte, so decoding stays cheap regardless of length.
43          Arrays.fill(bytes, (byte) '1');
44          return bytes;
45      }
46  
47      @Test
48      void testConfiguredLimit() {
49          final Base58 small = Base58.builder().setMaxDecodeLength(10).get();
50          assertThrows(IllegalArgumentException.class, () -> small.decode(ones(11)));
51          assertEquals(10, small.decode(ones(10)).length);
52          final Base58 unlimited = Base58.builder().setMaxDecodeLength(Integer.MAX_VALUE).get();
53          assertEquals(Base58.DEFAULT_MAX_DECODE_LENGTH + 1, unlimited.decode(ones(Base58.DEFAULT_MAX_DECODE_LENGTH + 1)).length);
54      }
55  
56      @Test
57      void testDecodeAtLimitAccepted() {
58          assertEquals(Base58.DEFAULT_MAX_DECODE_LENGTH, new Base58().decode(ones(Base58.DEFAULT_MAX_DECODE_LENGTH)).length);
59      }
60  
61      @Test
62      void testDecodeOverLimitRejected() {
63          assertThrows(IllegalArgumentException.class, () -> new Base58().decode(ones(Base58.DEFAULT_MAX_DECODE_LENGTH + 1)));
64      }
65  
66      @Test
67      void testDefaultStreamsRejectOversizedInput() throws IOException {
68          final byte[] encoded = ones(Base58.DEFAULT_MAX_DECODE_LENGTH + 1);
69          try (Base58InputStream stream = new Base58InputStream(new ByteArrayInputStream(encoded))) {
70              assertThrows(IOException.class, stream::read);
71          }
72          try (Base58OutputStream stream = Base58OutputStream.builder().setOutputStream(new ByteArrayOutputStream()).setEncode(false).get()) {
73              assertThrows(IOException.class, () -> stream.write(encoded));
74          }
75      }
76  
77      @Test
78      void testEncodingMayExceedDecodeLimit() throws IOException {
79          final byte[] input = new byte[Base58.DEFAULT_MAX_DECODE_LENGTH + 1];
80          final Base58 codec = Base58.builder().setMaxEncodeLength(input.length).get();
81          final byte[] encoded = codec.encode(input);
82          assertEquals(input.length, encoded.length);
83          assertThrows(IllegalArgumentException.class, () -> codec.decode(encoded));
84          final ByteArrayOutputStream sink = new ByteArrayOutputStream();
85          try (Base58OutputStream stream = Base58OutputStream.builder().setOutputStream(sink).setBaseNCodec(codec).get()) {
86              stream.write(input);
87          }
88          assertArrayEquals(encoded, sink.toByteArray());
89          try (Base58InputStream stream = Base58InputStream.builder().setByteArray(input).setEncode(true).setBaseNCodec(codec).get()) {
90              assertArrayEquals(encoded, IOUtils.toByteArray(stream));
91          }
92          final ByteArrayOutputStream decoded = new ByteArrayOutputStream();
93          try (Base58OutputStream stream = Base58OutputStream.builder().setOutputStream(decoded).setEncode(false)
94                  .setBaseNCodec(Base58.builder().setMaxDecodeLength(encoded.length).get()).get()) {
95              stream.write(encoded);
96          }
97          assertArrayEquals(input, decoded.toByteArray());
98      }
99  
100     @ParameterizedTest
101     @ValueSource(ints = { 10, Base58.DEFAULT_MAX_DECODE_LENGTH, Base58.DEFAULT_MAX_DECODE_LENGTH + 1 })
102     void testInputStreamAtConfiguredLimit(final int limit) throws IOException {
103         try (Base58InputStream stream = Base58InputStream.builder().setByteArray(ones(limit)).setBaseNCodec(Base58.builder().setMaxDecodeLength(limit).get())
104                 .get()) {
105             assertArrayEquals(new byte[limit], IOUtils.toByteArray(stream));
106         }
107     }
108 
109     @Test
110     void testInputStreamRejectsBeforeEof() throws IOException {
111         final ByteArrayInputStream source = new ByteArrayInputStream(ones(100)) {
112 
113             @Override
114             public synchronized int read(final byte[] bytes, final int offset, final int length) {
115                 return super.read(bytes, offset, Math.min(length, 3));
116             }
117         };
118         try (Base58InputStream stream = Base58InputStream.builder().setInputStream(source).setBaseNCodec(Base58.builder().setMaxDecodeLength(10).get()).get()) {
119             final IOException exception = assertThrows(IOException.class, stream::read);
120             assertTrue(exception.getCause() instanceof IllegalArgumentException);
121             assertEquals(88, source.available());
122         }
123     }
124 
125     @Test
126     void testInvalidLimitRejected() {
127         assertThrows(IllegalArgumentException.class, () -> Base58.builder().setMaxDecodeLength(0));
128         assertThrows(IllegalArgumentException.class, () -> Base58.builder().setMaxDecodeLength(-1));
129     }
130 
131     @Test
132     void testNonzeroDigitsAtLimit() {
133         final Base58 codec = new Base58();
134         final byte[] encoded = new byte[Base58.DEFAULT_MAX_DECODE_LENGTH];
135         Arrays.fill(encoded, (byte) 'z');
136         assertArrayEquals(encoded, codec.encode(codec.decode(encoded)));
137         final byte[] over = Arrays.copyOf(encoded, encoded.length + 1);
138         over[encoded.length] = 'z';
139         assertThrows(IllegalArgumentException.class, () -> codec.decode(over));
140         assertThrows(IllegalArgumentException.class, () -> codec.decode(StringUtils.newStringUsAscii(over)));
141         assertThrows(IllegalArgumentException.class, () -> codec.decode((Object) over));
142     }
143 
144     @ParameterizedTest
145     @ValueSource(ints = { 1, 3, 10 })
146     void testOutputStreamRejectsCrossingChunk(final int chunkSize) throws IOException {
147         final ByteArrayOutputStream sink = new ByteArrayOutputStream();
148         try (Base58OutputStream stream = Base58OutputStream.builder().setOutputStream(sink).setEncode(false)
149                 .setBaseNCodec(Base58.builder().setMaxDecodeLength(10).get()).get()) {
150             int written = 0;
151             while (written < 10) {
152                 final int length = Math.min(chunkSize, 10 - written);
153                 stream.write(ones(length));
154                 written += length;
155             }
156             final IOException exception = assertThrows(IOException.class, () -> stream.write('1'));
157             assertTrue(exception.getCause() instanceof IllegalArgumentException);
158             assertEquals(0, sink.size());
159         }
160         assertArrayEquals(new byte[10], sink.toByteArray());
161     }
162 
163     @Test
164     void testRejectsBeforeBuffering() {
165         final Base58 codec = Base58.builder().setMaxDecodeLength(10).get();
166         final BaseNCodec.Context context = new BaseNCodec.Context();
167         codec.decode(ones(8), 0, 8, context);
168         assertThrows(IllegalArgumentException.class, () -> codec.decode(ones(3), 0, 3, context));
169         assertArrayEquals(ones(8), context.buffer);
170         // The cumulative check must not wrap around when adding the chunk length.
171         assertThrows(IllegalArgumentException.class, () -> codec.decode(ones(1), 0, Integer.MAX_VALUE, context));
172         assertArrayEquals(ones(8), context.buffer);
173     }
174 
175     @Test
176     void testRoundTripUnaffected() {
177         final Base58 codec = new Base58();
178         final byte[] data = { 0, 0, 1, 2, 3, 4, 5, -1, 127 };
179         assertArrayEquals(data, codec.decode(codec.encode(data)));
180     }
181 }