This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-codec.git
The following commit(s) were added to refs/heads/master by this push:
new 8470693f [CODEC-344] Validate Base64 custom alphabets (#438)
8470693f is described below
commit 8470693f2de9d8f43615d407ad7c08f42c2a5250
Author: OldTruckDriver <[email protected]>
AuthorDate: Fri Jun 19 05:40:17 2026 +1000
[CODEC-344] Validate Base64 custom alphabets (#438)
* [CODEC-344] Validate Base64 custom alphabets
Reject custom encode tables that are not 64 unique bytes or that contain the
configured padding byte (checked at construction, since padding is
configurable).
Support non-ASCII alphabet bytes via unsigned lookup in
calculateDecodeTable,
decode, and isInAlphabet.
Add tests for duplicate, padding-byte (both setter orders), and non-ASCII
alphabets.
Reviewed-by: OpenAI Codex
Reviewed-by: Anthropic Claude Code
* Reduce vertical whitespace.
---------
Co-authored-by: Gary Gregory <[email protected]>
---
src/changes/changes.xml | 1 +
.../org/apache/commons/codec/binary/Base64.java | 51 ++++++++++++++++++----
.../apache/commons/codec/binary/Base64Test.java | 36 +++++++++++++++
3 files changed, 79 insertions(+), 9 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index ba07a02d..29a5e1bf 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -45,6 +45,7 @@ The <action> type attribute can be add,update,fix,remove.
<body>
<release version="1.22.1" date="YYYY-MM-DD" description="This is a feature
and maintenance release. Java 8 or later is required.">
<!-- FIX -->
+ <action type="fix" issue="CODEC-344" dev="ggregory" due-to="Ruiqi Dong,
Gary Gregory">Base64.Builder.setEncodeTable(byte...) accepts invalid custom
alphabets.</action>
<action type="fix" issue="CODEC-340" dev="ggregory" due-to="Ruiqi Dong,
Gary Gregory">Base58.Builder.setEncodeTable(byte...) is ignored when encoding
and decoding.</action>
<action type="fix" issue="CODEC-342" dev="ggregory" due-to="Ruiqi Dong,
Gary Gregory">Base32.Builder.setEncodeTable(byte...) can create a codec that
cannot decode its own output.</action>
<action type="fix" issue="CODEC-343" dev="ggregory" due-to="Ruiqi Dong,
Gary Gregory">Base32.Builder.setHexDecodeTable(boolean) sets the encode table
to a decode lookup table.</action>
diff --git a/src/main/java/org/apache/commons/codec/binary/Base64.java
b/src/main/java/org/apache/commons/codec/binary/Base64.java
index 22941b42..a022ee65 100644
--- a/src/main/java/org/apache/commons/codec/binary/Base64.java
+++ b/src/main/java/org/apache/commons/codec/binary/Base64.java
@@ -142,11 +142,16 @@ public class Base64 extends BaseNCodec {
}
}
+ /**
+ * Sets the encode table.
+ *
+ * @param encodeTable the encode table with exactly 64 unique entries,
null resets to the default.
+ * @return {@code this} instance.
+ * @throws IllegalArgumentException if {@code encodeTable} does not
contain exactly 64 unique entries.
+ */
@Override
public Builder setEncodeTable(final byte... encodeTable) {
- final boolean isStandardEncodeTable = Arrays.equals(encodeTable,
STANDARD_ENCODE_TABLE);
- final boolean isUrlSafe = Arrays.equals(encodeTable,
URL_SAFE_ENCODE_TABLE);
- setDecodeTableRaw(isStandardEncodeTable || isUrlSafe ?
DECODE_TABLE : calculateDecodeTable(encodeTable));
+ setDecodeTableRaw(toDecodeTable(encodeTable));
return super.setEncodeTable(encodeTable);
}
@@ -351,14 +356,38 @@ public class Base64 extends BaseNCodec {
* @return A new decode table.
*/
private static byte[] calculateDecodeTable(final byte[] encodeTable) {
+ if (encodeTable.length != STANDARD_ENCODE_TABLE.length) {
+ throw new IllegalArgumentException("encodeTable must have exactly
64 entries.");
+ }
final byte[] decodeTable = new byte[DECODING_TABLE_LENGTH];
Arrays.fill(decodeTable, (byte) -1);
for (int i = 0; i < encodeTable.length; i++) {
- decodeTable[encodeTable[i]] = (byte) i;
+ final int encodedByte = encodeTable[i] & 0xff;
+ if (decodeTable[encodedByte] != -1) {
+ throw new IllegalArgumentException("encodeTable must not
contain duplicate entries.");
+ }
+ decodeTable[encodedByte] = (byte) i;
}
return decodeTable;
}
+ private static boolean contains(final byte[] bytes, final byte value) {
+ for (final byte element : bytes) {
+ if (element == value) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static byte[] toDecodeTable(final byte[] encodeTable) {
+ final byte[] table = encodeTable != null ? encodeTable :
STANDARD_ENCODE_TABLE;
+ if (Arrays.equals(table, STANDARD_ENCODE_TABLE) ||
Arrays.equals(table, URL_SAFE_ENCODE_TABLE)) {
+ return DECODE_TABLE;
+ }
+ return calculateDecodeTable(table);
+ }
+
/**
* Decodes Base64 data into octets.
* <p>
@@ -839,6 +868,9 @@ public class Base64 extends BaseNCodec {
if (encTable.length != STANDARD_ENCODE_TABLE.length) {
throw new IllegalArgumentException("encodeTable must have exactly
64 entries.");
}
+ if (contains(encTable, pad)) {
+ throw new IllegalArgumentException("encodeTable must not contain
the padding byte.");
+ }
this.isStandardEncodeTable = Arrays.equals(encTable,
STANDARD_ENCODE_TABLE);
this.isUrlSafe = Arrays.equals(encTable, URL_SAFE_ENCODE_TABLE);
// TODO could be simplified if there is no requirement to reject
invalid line sep when length <=0
@@ -992,14 +1024,14 @@ public class Base64 extends BaseNCodec {
}
final int decodeSize = this.encodeSize - 1;
for (int i = 0; i < inAvail; i++) {
- final byte[] buffer = ensureBufferSize(decodeSize, context);
- final byte b = input[inPos++];
- if (b == pad) {
+ final int b = input[inPos++] & 0xff;
+ if (b == (pad & 0xff)) {
// We're done.
context.eof = true;
break;
}
- if (b >= 0 && b < decodeTable.length) {
+ final byte[] buffer = ensureBufferSize(decodeSize, context);
+ if (b < decodeTable.length) {
final int result = decodeTable[b];
if (result >= 0) {
context.modulus = (context.modulus + 1) %
BYTES_PER_ENCODED_BLOCK;
@@ -1150,7 +1182,8 @@ public class Base64 extends BaseNCodec {
*/
@Override
protected boolean isInAlphabet(final byte octet) {
- return isInAlphabet(octet, decodeTable);
+ final int value = octet & 0xff;
+ return value < decodeTable.length && decodeTable[value] != -1;
}
/**
diff --git a/src/test/java/org/apache/commons/codec/binary/Base64Test.java
b/src/test/java/org/apache/commons/codec/binary/Base64Test.java
index d616c4e2..229587fe 100644
--- a/src/test/java/org/apache/commons/codec/binary/Base64Test.java
+++ b/src/test/java/org/apache/commons/codec/binary/Base64Test.java
@@ -529,6 +529,42 @@ class Base64Test {
assertEquals(content, decodeString);
}
+ @Test
+ void testCustomEncodingAlphabetAllowsNonAsciiBytes() {
+ final byte[] encodeTable = STANDARD_ENCODE_TABLE.clone();
+ encodeTable[0] = (byte) 0x80;
+ final Base64 base64 =
Base64.builder().setEncodeTable(encodeTable).get();
+ final byte[] data = { 0 };
+ final byte[] encoded = base64.encode(data);
+ assertArrayEquals(new byte[] { (byte) 0x80, (byte) 0x80 }, encoded);
+ assertTrue(base64.isInAlphabet(encoded, false));
+ assertArrayEquals(data, base64.decode(encoded));
+ }
+
+ @Test
+ void testCustomEncodingAlphabetAllowsPaddingByteWhenPaddingChanges() {
+ final byte[] encodeTable = STANDARD_ENCODE_TABLE.clone();
+ encodeTable[0] = '=';
+ final Base64 base64 =
Base64.builder().setEncodeTable(encodeTable).setPadding((byte) '.').get();
+ final byte[] data = { 0 };
+ assertArrayEquals(data, base64.decode(base64.encode(data)));
+ }
+
+ @Test
+ void testCustomEncodingAlphabetRejectsConfiguredPaddingByte() {
+ final byte[] encodeTable = STANDARD_ENCODE_TABLE.clone();
+ encodeTable[0] = '=';
+ assertThrows(IllegalArgumentException.class, () ->
Base64.builder().setEncodeTable(encodeTable).get());
+ assertThrows(IllegalArgumentException.class, () ->
Base64.builder().setPadding((byte) 'A').get());
+ }
+
+ @Test
+ void testCustomEncodingAlphabetRejectsDuplicateEntries() {
+ final byte[] encodeTable = STANDARD_ENCODE_TABLE.clone();
+ encodeTable[1] = encodeTable[0];
+ assertThrows(IllegalArgumentException.class, () ->
Base64.builder().setEncodeTable(encodeTable));
+ }
+
@Test
void testCustomEncodingAlphabet_illegal() {
final byte[] encodeTable = {