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