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 = {

Reply via email to