Skip to content

Commit 48b6157

Browse files
tmousaw-ptcgarydgregory
authored andcommitted
CODEC-134: Update commons-codec to reject decoding any impossible string encoding for Base32 and Base64. (#19)
[CODEC-134] Squash and merge. * CODEC-134: Update to reject decoding strings that are not possible in the given encoding for Base32 and Base64. * CODEC-134: Fix tabs instead of spaces. * CODEC-134: Update such that the right shift is not indirectly accomplished via a method call. * CODEC-134: Fix spaces versus tabs (again). * CODEC-134: Add test to cover missed exception case in BCodec.java. * CODEC-134: Update changes.xml to describe change.
1 parent 0bf5b8a commit 48b6157

8 files changed

Lines changed: 134 additions & 4 deletions

File tree

src/changes/changes.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ The <action> type attribute can be add,update,fix,remove.
4545

4646
<release version="1.13" date="YYYY-MM-DD" description="TBD">
4747
<action issue="CODEC-257" dev="ggregory" type="update">Update from Java 7 to Java 8</action>
48+
<action issue="CODEC-134" dev="tmousaw-ptc" type="fix">Reject any decode request for a value that is impossible to encode to for Base32/Base64 rather than blindly decoding.</action>
4849
</release>
4950

5051
<release version="1.12" date="2019-02-04" description="Feature and fix release.">

src/main/java/org/apache/commons/codec/binary/Base32.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ public Base32(final int lineLength, final byte[] lineSeparator, final boolean us
332332
* @param inPos
333333
* Position to start reading data from.
334334
* @param inAvail
335-
* Amount of bytes available from input for encoding.
335+
* Amount of bytes available from input for decoding.
336336
* @param context the context to be used
337337
*
338338
* Output is written to {@link Context#buffer} as 8-bit octets, using {@link Context#pos} as the buffer position
@@ -381,29 +381,35 @@ void decode(final byte[] in, int inPos, final int inAvail, final Context context
381381
// we ignore partial bytes, i.e. only multiples of 8 count
382382
switch (context.modulus) {
383383
case 2 : // 10 bits, drop 2 and output one byte
384+
validateCharacter(2, context);
384385
buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 2) & MASK_8BITS);
385386
break;
386387
case 3 : // 15 bits, drop 7 and output 1 byte
388+
validateCharacter(7, context);
387389
buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 7) & MASK_8BITS);
388390
break;
389391
case 4 : // 20 bits = 2*8 + 4
392+
validateCharacter(4, context);
390393
context.lbitWorkArea = context.lbitWorkArea >> 4; // drop 4 bits
391394
buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 8) & MASK_8BITS);
392395
buffer[context.pos++] = (byte) ((context.lbitWorkArea) & MASK_8BITS);
393396
break;
394397
case 5 : // 25bits = 3*8 + 1
398+
validateCharacter(1, context);
395399
context.lbitWorkArea = context.lbitWorkArea >> 1;
396400
buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 16) & MASK_8BITS);
397401
buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 8) & MASK_8BITS);
398402
buffer[context.pos++] = (byte) ((context.lbitWorkArea) & MASK_8BITS);
399403
break;
400404
case 6 : // 30bits = 3*8 + 6
405+
validateCharacter(6, context);
401406
context.lbitWorkArea = context.lbitWorkArea >> 6;
402407
buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 16) & MASK_8BITS);
403408
buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 8) & MASK_8BITS);
404409
buffer[context.pos++] = (byte) ((context.lbitWorkArea) & MASK_8BITS);
405410
break;
406411
case 7 : // 35 = 4*8 +3
412+
validateCharacter(3, context);
407413
context.lbitWorkArea = context.lbitWorkArea >> 3;
408414
buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 24) & MASK_8BITS);
409415
buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 16) & MASK_8BITS);
@@ -540,4 +546,21 @@ void encode(final byte[] in, int inPos, final int inAvail, final Context context
540546
public boolean isInAlphabet(final byte octet) {
541547
return octet >= 0 && octet < decodeTable.length && decodeTable[octet] != -1;
542548
}
549+
550+
/**
551+
* <p>
552+
* Validates whether the character is possible in the context of the set of possible base 32 values.
553+
* </p>
554+
*
555+
* @param numBits number of least significant bits to check
556+
* @param context the context to be used
557+
*
558+
* @throws IllegalArgumentException if the bits being checked contain any non-zero value
559+
*/
560+
private void validateCharacter(int numBits, Context context) {
561+
if ((context.lbitWorkArea & numBits) != 0) {
562+
throw new IllegalArgumentException(
563+
"Last encoded character (before the paddings if any) is a valid base 32 alphabet but not a possible value");
564+
}
565+
}
543566
}

src/main/java/org/apache/commons/codec/binary/Base64.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ void encode(final byte[] in, int inPos, final int inAvail, final Context context
421421
* @param inPos
422422
* Position to start reading data from.
423423
* @param inAvail
424-
* Amount of bytes available from input for encoding.
424+
* Amount of bytes available from input for decoding.
425425
* @param context
426426
* the context to be used
427427
*/
@@ -469,10 +469,12 @@ void decode(final byte[] in, int inPos, final int inAvail, final Context context
469469
// TODO not currently tested; perhaps it is impossible?
470470
break;
471471
case 2 : // 12 bits = 8 + 4
472+
validateCharacter(4, context);
472473
context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the extra 4 bits
473474
buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);
474475
break;
475476
case 3 : // 18 bits = 8 + 8 + 2
477+
validateCharacter(2, context);
476478
context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2 bits
477479
buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);
478480
buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);
@@ -781,4 +783,21 @@ protected boolean isInAlphabet(final byte octet) {
781783
return octet >= 0 && octet < decodeTable.length && decodeTable[octet] != -1;
782784
}
783785

786+
/**
787+
* <p>
788+
* Validates whether the character is possible in the context of the set of possible base 64 values.
789+
* </p>
790+
*
791+
* @param numBits number of least significant bits to check
792+
* @param context the context to be used
793+
*
794+
* @throws IllegalArgumentException if the bits being checked contain any non-zero value
795+
*/
796+
private long validateCharacter(int numBitsToDrop, Context context) {
797+
if ((context.ibitWorkArea & numBitsToDrop) != 0) {
798+
throw new IllegalArgumentException(
799+
"Last encoded character (before the paddings if any) is a valid base 64 alphabet but not a possible value");
800+
}
801+
return context.ibitWorkArea >> numBitsToDrop;
802+
}
784803
}

src/main/java/org/apache/commons/codec/net/BCodec.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public String decode(final String value) throws DecoderException {
178178
}
179179
try {
180180
return this.decodeText(value);
181-
} catch (final UnsupportedEncodingException e) {
181+
} catch (final UnsupportedEncodingException | IllegalArgumentException e) {
182182
throw new DecoderException(e.getMessage(), e);
183183
}
184184
}

src/test/java/org/apache/commons/codec/binary/Base32Test.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ public class Base32Test {
4646
{"foobar" ,"MZXW6YTBOI======"},
4747
};
4848

49+
private static final String[] BASE32_IMPOSSIBLE_CASES = {
50+
"MC======",
51+
"MZXE====",
52+
"MZXWB===",
53+
"MZXW6YB=",
54+
"MZXW6YTBOC======"
55+
};
56+
57+
private static final String[] BASE32_IMPOSSIBLE_CASES_CHUNKED = {
58+
"M2======\r\n",
59+
"MZX0====\r\n",
60+
"MZXW0===\r\n",
61+
"MZXW6Y2=\r\n",
62+
"MZXW6YTBO2======\r\n"
63+
};
64+
65+
private static final String[] BASE32HEX_IMPOSSIBLE_CASES = {
66+
"C2======",
67+
"CPN4====",
68+
"CPNM1===",
69+
"CPNMUO1=",
70+
"CPNMUOJ1E2======"
71+
};
72+
4973
private static final Object[][] BASE32_BINARY_TEST_CASES;
5074

5175
// { null, "O0o0O0o0" }
@@ -248,4 +272,29 @@ public void testSingleCharEncoding() {
248272
}
249273
}
250274

275+
@Test
276+
public void testBase32ImpossibleSamples() {
277+
testImpossibleCases(new Base32(), BASE32_IMPOSSIBLE_CASES);
278+
}
279+
280+
@Test
281+
public void testBase32ImpossibleChunked() {
282+
testImpossibleCases(new Base32(20), BASE32_IMPOSSIBLE_CASES_CHUNKED);
283+
}
284+
285+
@Test
286+
public void testBase32HexImpossibleSamples() {
287+
testImpossibleCases(new Base32(true), BASE32HEX_IMPOSSIBLE_CASES);
288+
}
289+
290+
private void testImpossibleCases(Base32 codec, String[] impossible_cases) {
291+
for (String impossible : impossible_cases) {
292+
try {
293+
codec.decode(impossible);
294+
fail();
295+
} catch (IllegalArgumentException ex) {
296+
// expected
297+
}
298+
}
299+
}
251300
}

src/test/java/org/apache/commons/codec/binary/Base64Test.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ public class Base64Test {
4444

4545
private static final Charset CHARSET_UTF8 = Charsets.UTF_8;
4646

47+
private static final String[] BASE64_IMPOSSIBLE_CASES = {
48+
"ZE==",
49+
"ZmC=",
50+
"Zm9vYE==",
51+
"Zm9vYmC=",
52+
};
53+
4754
private final Random random = new Random();
4855

4956
/**
@@ -1297,4 +1304,16 @@ public void testHugeLineSeparator() {
12971304
assertEquals("testDEFAULT_BUFFER_SIZE", strOriginal, strDecoded);
12981305
}
12991306

1307+
@Test
1308+
public void testBase64ImpossibleSamples() {
1309+
Base64 codec = new Base64();
1310+
for (String s : BASE64_IMPOSSIBLE_CASES) {
1311+
try {
1312+
codec.decode(s);
1313+
fail();
1314+
} catch (IllegalArgumentException ex) {
1315+
// expected
1316+
}
1317+
}
1318+
}
13001319
}

src/test/java/org/apache/commons/codec/binary/Base64TestData.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
*/
3131
public class Base64TestData {
3232

33-
public static final String CODEC_101_MULTIPLE_OF_3 = "123";
33+
public static final String CODEC_101_MULTIPLE_OF_3 = "124";
3434

3535
public static final String CODEC_98_NPE
3636
= "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM";

src/test/java/org/apache/commons/codec/net/BCodecTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333
*
3434
*/
3535
public class BCodecTest {
36+
private static final String[] BASE64_IMPOSSIBLE_CASES = {
37+
"ZE==",
38+
"ZmC=",
39+
"Zm9vYE==",
40+
"Zm9vYmC=",
41+
};
3642

3743
static final int SWISS_GERMAN_STUFF_UNICODE[] =
3844
{ 0x47, 0x72, 0xFC, 0x65, 0x7A, 0x69, 0x5F, 0x7A, 0xE4, 0x6D, 0xE4 };
@@ -147,4 +153,17 @@ public void testDecodeObjects() throws Exception {
147153
// Exception expected, test segment passes.
148154
}
149155
}
156+
157+
@Test
158+
public void testBase64ImpossibleSamples() {
159+
BCodec codec = new BCodec();
160+
for (String s : BASE64_IMPOSSIBLE_CASES) {
161+
try {
162+
codec.decode(s);
163+
fail();
164+
} catch (DecoderException ex) {
165+
// expected
166+
}
167+
}
168+
}
150169
}

0 commit comments

Comments
 (0)