You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by re...@apache.org on 2026/04/13 14:54:50 UTC

(jackrabbit) branch 2.22 updated: JCR-5226: Base64 decoder does not handle lack of padding correctly (#332)

This is an automated email from the ASF dual-hosted git repository.

reschke pushed a commit to branch 2.22
in repository https://gitbox.apache.org/repos/asf/jackrabbit.git


The following commit(s) were added to refs/heads/2.22 by this push:
     new aef48094b JCR-5226: Base64 decoder does not handle lack of padding correctly (#332)
aef48094b is described below

commit aef48094b8b730467020ca6a0c23061eaf4b6990
Author: Julian Reschke <re...@apache.org>
AuthorDate: Mon Feb 9 18:03:36 2026 +0100

    JCR-5226: Base64 decoder does not handle lack of padding correctly (#332)
    
    * JCR-5226: Base64 decoder does not handle input without padding correctly - failing test case
    
    * JCR-5226: Base64 decoder does not handle input without padding correctly - minimal change to handle missing padding (instead of returning garbage)
    
    * JCR-5226: more test coverage
    
    * JCR-5226: fix one more case and add more test coverage
    
    * JCR-5226: clean up test class
    
    * JCR-5226: minor refactoring of base class
    
    * JCR-5226: javadoc
---
 .../java/org/apache/jackrabbit/util/Base64.java    | 57 ++++++++++++++++---
 .../org/apache/jackrabbit/util/Base64Test.java     | 65 ++++++++++++++++++----
 2 files changed, 105 insertions(+), 17 deletions(-)

diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Base64.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Base64.java
index 6a8557ee7..ae2925f57 100644
--- a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Base64.java
+++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Base64.java
@@ -32,9 +32,12 @@ import java.util.Arrays;
 /**
  * <code>Base64</code> provides Base64 encoding/decoding of strings and streams.
  * <p>
+ * <em>NOTE:</em> the decoder accepts invalid input (such as non-trailing padding characters)
+ * and just returns broken output (see JCR-5227).
+ * <p>
  * See <a href="https://datatracker.ietf.org/doc/html/rfc4648#section-4">RFC 4648, Section 4</a>.
  * <p>
- * See {@link java.util.Base64} for a JDK alternative.
+ * See {@link java.util.Base64} for a better JDK alternative.
  */
 public class Base64 {
 
@@ -323,6 +326,20 @@ public class Base64 {
         decode(chars, 0, chars.length, out);
     }
 
+    // utility methods to decode into 1st, 2nd and 3rd position of output
+
+    private static byte decodeFirst(int b0, int b1) {
+        return (byte) (b0 << 2 & 0xfc | b1 >> 4 & 0x3);
+    }
+
+    private static byte decodeSecond(int b1, int b2) {
+        return (byte) (b1 << 4 & 0xf0 | b2 >> 2 & 0xf);
+    }
+
+    private static byte decodeThird(int b2, int b3) {
+        return (byte) (b2 << 6 & 0xc0 | b3 & 0x3f);
+    }
+
     /**
      * Decode base64 encoded data.
      *
@@ -356,16 +373,16 @@ public class Base64 {
                     int b2 = DECODETABLE[chunk[2]];
                     int b3 = DECODETABLE[chunk[3]];
                     if (chunk[3] == BASE64PAD && chunk[2] == BASE64PAD) {
-                        dec[0] = (byte) (b0 << 2 & 0xfc | b1 >> 4 & 0x3);
+                        dec[0] = decodeFirst(b0, b1);
                         out.write(dec, 0, 1);
                     } else if (chunk[3] == BASE64PAD) {
-                        dec[0] = (byte) (b0 << 2 & 0xfc | b1 >> 4 & 0x3);
-                        dec[1] = (byte) (b1 << 4 & 0xf0 | b2 >> 2 & 0xf);
+                        dec[0] = decodeFirst(b0, b1);
+                        dec[1] = decodeSecond(b1, b2);
                         out.write(dec, 0, 2);
                     } else {
-                        dec[0] = (byte) (b0 << 2 & 0xfc | b1 >> 4 & 0x3);
-                        dec[1] = (byte) (b1 << 4 & 0xf0 | b2 >> 2 & 0xf);
-                        dec[2] = (byte) (b2 << 6 & 0xc0 | b3 & 0x3f);
+                        dec[0] = decodeFirst(b0, b1);
+                        dec[1] = decodeSecond(b1, b2);
+                        dec[2] = decodeThird(b2, b3);
                         out.write(dec, 0, 3);
                     }
                     posChunk = 0;
@@ -374,5 +391,31 @@ public class Base64 {
                 throw new IllegalArgumentException("specified data is not base64 encoded");
             }
         }
+
+        // if there is an incomplete chunk...
+        if (posChunk != 0) {
+            boolean lastCharWasPad = chunk[posChunk - 1] == BASE64PAD;
+            if (lastCharWasPad) {
+                throw new IllegalArgumentException("specified data is not base64 encoded (input ends with unexpected pad character");
+            }
+
+            // handle missing padding gracefully, inspired by
+            // https://datatracker.ietf.org/doc/html/rfc7515#appendix-C
+
+            if (posChunk == 1) {
+                throw new IllegalArgumentException("specified data is not base64 encoded (extra non-pad character");
+            }
+
+            if (posChunk == 2) {
+                // no padding, input length == 2; add two pad characters
+                chunk[2] = BASE64PAD;
+                chunk[3] = BASE64PAD;
+            } else {
+                // no padding, input length == 3: add one pad character
+                chunk[3] = BASE64PAD;
+            }
+
+            decode(chunk, out);
+        }
     }
 }
diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/Base64Test.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/Base64Test.java
index ac1b461d8..c79ef15d9 100644
--- a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/Base64Test.java
+++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/Base64Test.java
@@ -18,19 +18,20 @@ package org.apache.jackrabbit.util;
 
 import junit.framework.TestCase;
 
-import java.io.IOException;
 import java.io.StringWriter;
 import java.io.ByteArrayOutputStream;
 import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.Random;
-import java.util.Arrays;
+
+import static org.junit.Assert.assertArrayEquals;
 
 /**
  * Test cases for Base64 encode / decode.
  */
 public class Base64Test extends TestCase {
 
-    private Random _random = new Random();
+    private final Random _random = new Random();
 
     /**
      * @return Returns the _random.
@@ -45,8 +46,7 @@ public class Base64Test extends TestCase {
     public void testWhitespace() throws Exception {
         ByteArrayOutputStream buffer = new ByteArrayOutputStream();
         Base64.decode(" d G\tV \tzdA\n= =\n", buffer);
-        byte[] data = buffer.toByteArray();
-        assertEquals("test", new String(data, "US-ASCII"));
+        assertEquals("test", buffer.toString(StandardCharsets.US_ASCII));
     }
 
     /**
@@ -71,7 +71,7 @@ public class Base64Test extends TestCase {
         baos = new ByteArrayOutputStream();
         Base64.decode(new ByteArrayInputStream(encData), baos);
         byte[] decData = baos.toByteArray();
-        assertTrue(Arrays.equals(data, decData));       
+        assertArrayEquals(data, decData);
     }
 
     private void base64RoundTrip() throws Exception {
@@ -98,26 +98,71 @@ public class Base64Test extends TestCase {
         }
     }
 
-    public void testDecodeOrEncode() throws IOException {
+    public void testDecodeOrEncode() {
         assertEquals("", Base64.decodeOrEncode(Base64.decodeOrEncode("")));
         assertEquals("test", Base64.decodeOrEncode(Base64.decodeOrEncode("test")));
         assertEquals("{base64}dGVzdA==", Base64.decodeOrEncode("test"));
         assertEquals("test", Base64.decodeOrEncode("{base64}dGVzdA=="));
     }
 
-    public void testDecodeIfEncoded() throws IOException {
-        assertEquals(null, Base64.decodeIfEncoded(null));
+    public void testDecodeIfEncoded() {
+        assertNull(Base64.decodeIfEncoded(null));
         assertEquals("", Base64.decodeIfEncoded(""));
         assertEquals("", Base64.decodeIfEncoded("{base64}"));
         assertEquals("test", Base64.decodeIfEncoded("test"));
         assertEquals("test", Base64.decodeIfEncoded("{base64}dGVzdA=="));
     }
 
-    public void testStringEncodeDecode() throws IOException {
+    public void testStringEncodeDecode() {
         assertEquals("", Base64.decode(Base64.encode("")));
         assertEquals("test", Base64.decode(Base64.encode("test")));
         assertEquals("dGVzdA==", Base64.encode("test"));
         assertEquals("test", Base64.decode("dGVzdA=="));
     }
 
+    public void testStringDecodeNoPadding() {
+        // no padding variants tested in comparison to padded ones
+        // https://datatracker.ietf.org/doc/html/rfc4648#section-10
+
+        assertEquals("foob", Base64.decode("Zm9vYg=="));
+        assertEquals("foob", Base64.decode("Zm9vYg"));
+
+        assertEquals("fooba", Base64.decode("Zm9vYmE="));
+        assertEquals("fooba", Base64.decode("Zm9vYmE"));
+    }
+
+    public void testStringDecodeBrokenPadding() {
+        // incorrect number of padding characters (when padding, the total
+        // length needs to be a multiple of 4)
+
+        // confirming with jdk decoder
+        try {
+            java.util.Base64.getDecoder().decode("Zm9vYg");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        // Jackrabbit API is weird; for broken input, it just returns
+        // the input
+        assertEquals("Zm9vYg=", Base64.decode("Zm9vYg="));
+    }
+
+    public void testStringIllegalExtraChar() {
+        // last (4 byte) chunk of length 1
+        // test with pad and non-pad
+
+        // confirming with jdk decoder
+        try {
+            java.util.Base64.getDecoder().decode("extra");
+        } catch (IllegalArgumentException expected) {
+        }
+       try {
+            java.util.Base64.getDecoder().decode("extr=");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        // Jackrabbit API is weird; for broken input, it just returns
+        // the input
+        assertEquals("extra", Base64.decode("extra"));
+        assertEquals("extr=", Base64.decode("extr="));
+    }
 }
\ No newline at end of file