From a705a2776919bf40c7573377354ac14c8a6cb05a Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sun, 4 Oct 2026 17:01:58 +0100 Subject: [PATCH] Decode base64Binary strictly in value holders and the rich parser Add Base64Bin.decode, which ignores XML whitespace and otherwise requires the base64 alphabet in groups of four with padding only at the end. JavaBase64Holder.lex and the XMLStreamReaderExtImpl base64 getters now use it instead of the JDK MIME decoder, which drops non-alphabet chars and accepts unpadded input. Follow-up to #123: the rich parser getters still accepted stray chars, and unpadded or wrongly sized values (e.g. "SGVsbG8") still validated. Co-Authored-By: Claude Opus 5.5 --- .../richParser/XMLStreamReaderExtImpl.java | 26 +++---- .../apache/xmlbeans/impl/util/Base64Bin.java | 69 +++++++++++++++++++ .../impl/values/JavaBase64Holder.java | 25 ++----- src/test/java/misc/checkin/Base64BinTest.java | 61 ++++++++++++++++ .../checkin/Base64BinaryValidateTest.java | 29 ++++++-- .../java/misc/checkin/RichParserTests.java | 14 +++- 6 files changed, 181 insertions(+), 43 deletions(-) create mode 100644 src/main/java/org/apache/xmlbeans/impl/util/Base64Bin.java create mode 100644 src/test/java/misc/checkin/Base64BinTest.java diff --git a/src/main/java/org/apache/xmlbeans/impl/richParser/XMLStreamReaderExtImpl.java b/src/main/java/org/apache/xmlbeans/impl/richParser/XMLStreamReaderExtImpl.java index a6ce66a1e..8adb49e24 100644 --- a/src/main/java/org/apache/xmlbeans/impl/richParser/XMLStreamReaderExtImpl.java +++ b/src/main/java/org/apache/xmlbeans/impl/richParser/XMLStreamReaderExtImpl.java @@ -22,6 +22,7 @@ import org.apache.xmlbeans.impl.common.InvalidLexicalValueException; import org.apache.xmlbeans.impl.common.XMLChar; import org.apache.xmlbeans.impl.common.XmlWhitespace; +import org.apache.xmlbeans.impl.util.Base64Bin; import org.apache.xmlbeans.impl.util.HexBin; import org.apache.xmlbeans.impl.util.XsTypeConverter; @@ -35,7 +36,6 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.StandardCharsets; -import java.util.Base64; import java.util.Date; /** @@ -182,11 +182,11 @@ public InputStream getBase64Value() throws XMLStreamException, InvalidLexicalValueException { _charSeq.reload(CharSeqTrimWS.XMLWHITESPACE_TRIM); String text = _charSeq.toString(); - try { - byte[] buf = Base64.getMimeDecoder().decode(text.getBytes(StandardCharsets.ISO_8859_1)); + byte[] buf = Base64Bin.decode(text); + if (buf != null) { return new ByteArrayInputStream(buf); - } catch (IllegalArgumentException e) { - throw new InvalidLexicalValueException("invalid base64Binary value", e, _charSeq.getLocation()); + } else { + throw new InvalidLexicalValueException("invalid base64Binary value", _charSeq.getLocation()); } } @@ -332,11 +332,11 @@ public InputStream getAttributeHexBinaryValue(int index) throws XMLStreamExcepti public InputStream getAttributeBase64Value(int index) throws XMLStreamException { String text = _charSeq.reloadAtt(index, CharSeqTrimWS.XMLWHITESPACE_TRIM).toString(); - try { - byte[] buf = Base64.getMimeDecoder().decode(text.getBytes(StandardCharsets.ISO_8859_1)); + byte[] buf = Base64Bin.decode(text); + if (buf != null) { return new ByteArrayInputStream(buf); - } catch (IllegalArgumentException e) { - throw new InvalidLexicalValueException("invalid base64Binary value", e, _charSeq.getLocation()); + } else { + throw new InvalidLexicalValueException("invalid base64Binary value", _charSeq.getLocation()); } } @@ -486,11 +486,11 @@ public InputStream getAttributeHexBinaryValue(String uri, String local) throws X public InputStream getAttributeBase64Value(String uri, String local) throws XMLStreamException { CharSequence cs = _charSeq.reloadAtt(uri, local, CharSeqTrimWS.XMLWHITESPACE_TRIM); String text = cs.toString(); - try { - byte[] buf = Base64.getMimeDecoder().decode(text.getBytes(StandardCharsets.ISO_8859_1)); + byte[] buf = Base64Bin.decode(text); + if (buf != null) { return new ByteArrayInputStream(buf); - } catch (IllegalArgumentException e) { - throw new InvalidLexicalValueException("invalid base64Binary value", e, _charSeq.getLocation()); + } else { + throw new InvalidLexicalValueException("invalid base64Binary value", _charSeq.getLocation()); } } diff --git a/src/main/java/org/apache/xmlbeans/impl/util/Base64Bin.java b/src/main/java/org/apache/xmlbeans/impl/util/Base64Bin.java new file mode 100644 index 000000000..ca756e698 --- /dev/null +++ b/src/main/java/org/apache/xmlbeans/impl/util/Base64Bin.java @@ -0,0 +1,69 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlbeans.impl.util; + +import org.apache.xmlbeans.impl.common.XMLChar; + +import java.util.Base64; + +/** + * Strict decoding of the xsd:base64Binary lexical space. + *

+ * This class is for internal use in Apache XMLBeans. If you need to do your own base64 + * encoding/decoding, use {@link java.util.Base64}. + *

+ */ +public final class Base64Bin { + + private Base64Bin() { + } + + /** + * Decodes an xsd:base64Binary value. XML whitespace is ignored; anything else + * must be in the base64 alphabet, in groups of four characters with + * {@code =} padding only at the end. + *

+ * The JDK MIME decoder is deliberately not used: it silently drops characters + * outside the alphabet and accepts unpadded input, both of which fall outside + * the base64Binary lexical space. + * + * @param value the lexical value + * @return decoded bytes, or null if the input is null or not valid base64Binary + */ + public static byte[] decode(String value) { + if (value == null) { + return null; + } + StringBuilder sb = new StringBuilder(value.length()); + for (int i = 0, len = value.length(); i < len; i++) { + char ch = value.charAt(i); + if (!XMLChar.isSpace(ch)) { + sb.append(ch); + } + } + if (sb.length() % 4 != 0) { + return null; + } + try { + // the basic decoder rejects any char outside the alphabet and + // misplaced padding + return Base64.getDecoder().decode(sb.toString()); + } catch (IllegalArgumentException e) { + return null; + } + } +} diff --git a/src/main/java/org/apache/xmlbeans/impl/values/JavaBase64Holder.java b/src/main/java/org/apache/xmlbeans/impl/values/JavaBase64Holder.java index d6d3ca35b..8acdd2c87 100644 --- a/src/main/java/org/apache/xmlbeans/impl/values/JavaBase64Holder.java +++ b/src/main/java/org/apache/xmlbeans/impl/values/JavaBase64Holder.java @@ -21,8 +21,8 @@ import org.apache.xmlbeans.XmlObject; import org.apache.xmlbeans.impl.common.QNameHelper; import org.apache.xmlbeans.impl.common.ValidationContext; -import org.apache.xmlbeans.impl.common.XMLChar; import org.apache.xmlbeans.impl.schema.BuiltinSchemaTypeSystem; +import org.apache.xmlbeans.impl.util.Base64Bin; import java.util.Arrays; import java.util.Base64; @@ -54,29 +54,12 @@ protected void set_nil() { } public static byte[] lex(String v, ValidationContext c) { - // The MIME decoder silently discards any character outside the base64 - // alphabet, so a value carrying stray characters decodes to a truncated - // result instead of being rejected. The base64Binary lexical space only - // permits the alphabet and XML whitespace, so reject anything else here. - for (int i = 0, len = v.length(); i < len; i++) { - char ch = v.charAt(i); - if (!isBase64Char(ch) && !XMLChar.isSpace(ch)) { - c.invalid(XmlErrorCodes.BASE64BINARY, new Object[]{"not encoded properly"}); - return null; - } - } - try { - return Base64.getMimeDecoder().decode(v); - } catch (IllegalArgumentException e) { + byte[] bytes = Base64Bin.decode(v); + if (bytes == null) { // TODO - get a decent error with line numbers and such here c.invalid(XmlErrorCodes.BASE64BINARY, new Object[]{"not encoded properly"}); - return null; } - } - - private static boolean isBase64Char(char ch) { - return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || - (ch >= '0' && ch <= '9') || ch == '+' || ch == '/' || ch == '='; + return bytes; } public static byte[] validateLexical(String v, SchemaType sType, ValidationContext context) { diff --git a/src/test/java/misc/checkin/Base64BinTest.java b/src/test/java/misc/checkin/Base64BinTest.java new file mode 100644 index 000000000..81b15f0ed --- /dev/null +++ b/src/test/java/misc/checkin/Base64BinTest.java @@ -0,0 +1,61 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package misc.checkin; + +import org.apache.xmlbeans.impl.util.Base64Bin; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class Base64BinTest { + + private static final byte[] HELLO = "Hello".getBytes(StandardCharsets.US_ASCII); + + @Test + void decodesValidValues() { + assertArrayEquals(HELLO, Base64Bin.decode("SGVsbG8=")); + assertArrayEquals(HELLO, Base64Bin.decode(" SGVs bG8= ")); + assertArrayEquals(HELLO, Base64Bin.decode("SGVs\r\n\tbG8=")); + assertArrayEquals(new byte[]{'H'}, Base64Bin.decode("SA==")); + assertArrayEquals(new byte[0], Base64Bin.decode("")); + assertArrayEquals(new byte[0], Base64Bin.decode(" \n ")); + } + + @Test + void rejectsInvalidValues() { + assertNull(Base64Bin.decode(null)); + // chars outside the alphabet + assertNull(Base64Bin.decode("SGVsbG8=!!!!")); + assertNull(Base64Bin.decode("SGV!!!sbG8=")); + assertNull(Base64Bin.decode("!!!!")); + // URL-safe alphabet is not base64Binary + assertNull(Base64Bin.decode("-_-_")); + // missing padding / wrong length + assertNull(Base64Bin.decode("SGVsbG8")); + assertNull(Base64Bin.decode("SGVsbG")); + assertNull(Base64Bin.decode("A")); + // misplaced or excess padding + assertNull(Base64Bin.decode("SG=VsbG8=")); + assertNull(Base64Bin.decode("SGVsbG8=SGVs")); + assertNull(Base64Bin.decode("SGVsbG8==")); + assertNull(Base64Bin.decode("S===")); + assertNull(Base64Bin.decode("====")); + } +} diff --git a/src/test/java/misc/checkin/Base64BinaryValidateTest.java b/src/test/java/misc/checkin/Base64BinaryValidateTest.java index 1873e8159..5cb85eaca 100644 --- a/src/test/java/misc/checkin/Base64BinaryValidateTest.java +++ b/src/test/java/misc/checkin/Base64BinaryValidateTest.java @@ -20,6 +20,7 @@ import org.apache.xmlbeans.XmlBeans; import org.apache.xmlbeans.XmlObject; import org.apache.xmlbeans.impl.xb.xsdschema.SchemaDocument; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -27,13 +28,20 @@ public class Base64BinaryValidateTest { + private static final String XSD = + "" + + " " + + ""; + + private static SchemaTypeLoader loader; + + @BeforeAll + static void compileSchema() throws Exception { + loader = XmlBeans.loadXsd(new XmlObject[]{SchemaDocument.Factory.parse(XSD)}); + } + private static boolean validates(String value) throws Exception { - String xsd = - "" + - " " + - ""; - SchemaTypeLoader loader = XmlBeans.loadXsd(new XmlObject[]{SchemaDocument.Factory.parse(xsd)}); XmlObject doc = loader.parse("" + value + "", null, null); return doc.validate(); } @@ -47,6 +55,15 @@ void charsOutsideAlphabetAreReported() throws Exception { assertFalse(validates("!!!!")); } + @Test + void missingPaddingAndBadLengthAreReported() throws Exception { + // the JDK decoders treat padding as optional + assertFalse(validates("SGVsbG8")); + assertFalse(validates("SGVsbG")); + assertFalse(validates("SGVsbG8==")); + assertFalse(validates("SG=VsbG8=")); + } + @Test void validValuesStillValidate() throws Exception { assertTrue(validates("SGVsbG8=")); diff --git a/src/test/java/misc/checkin/RichParserTests.java b/src/test/java/misc/checkin/RichParserTests.java index a60f3ebcc..50a78de9d 100755 --- a/src/test/java/misc/checkin/RichParserTests.java +++ b/src/test/java/misc/checkin/RichParserTests.java @@ -70,9 +70,9 @@ void testPrimitiveTypes() throws Exception { @Test void testInvalidBase64ThrowsInvalidLexicalValue() throws Exception { - // "A" is a single base64 char, the MIME decoder rejects it with - // IllegalArgumentException. The rich parser must surface that as the - // documented InvalidLexicalValueException, like the other getters. + // "A" is a single base64 char and so not valid base64Binary. The rich + // parser must surface that as the documented InvalidLexicalValueException, + // like the other getters. XMLStreamReaderExt elem = atFirstStartElement("A"); assertThrows(InvalidLexicalValueException.class, elem::getBase64Value); @@ -81,6 +81,14 @@ void testInvalidBase64ThrowsInvalidLexicalValue() throws Exception { XMLStreamReaderExt attByName = atFirstStartElement(""); assertThrows(InvalidLexicalValueException.class, () -> attByName.getAttributeBase64Value("", "b")); + + // non-alphabet chars and missing padding used to be accepted by the MIME decoder + for (String bad : new String[]{"SGVsbG8=!!!!", "SGV!!!sbG8=", "SGVsbG8"}) { + XMLStreamReaderExt e = atFirstStartElement("" + bad + ""); + assertThrows(InvalidLexicalValueException.class, e::getBase64Value, bad); + XMLStreamReaderExt a = atFirstStartElement(""); + assertThrows(InvalidLexicalValueException.class, () -> a.getAttributeBase64Value(0), bad); + } } @Test