Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

/**
Expand Down Expand Up @@ -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());
}
}

Expand Down Expand Up @@ -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());
}
}

Expand Down Expand Up @@ -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());
}
}

Expand Down
69 changes: 69 additions & 0 deletions src/main/java/org/apache/xmlbeans/impl/util/Base64Bin.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* This class is for internal use in Apache XMLBeans. If you need to do your own base64
* encoding/decoding, use {@link java.util.Base64}.
* </p>
*/
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.
* <p>
* 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
61 changes: 61 additions & 0 deletions src/test/java/misc/checkin/Base64BinTest.java
Original file line number Diff line number Diff line change
@@ -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("===="));
}
}
29 changes: 23 additions & 6 deletions src/test/java/misc/checkin/Base64BinaryValidateTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,28 @@
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;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class Base64BinaryValidateTest {

private static final String XSD =
"<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema' xmlns:t='urn:t' " +
"targetNamespace='urn:t' elementFormDefault='qualified'>" +
" <xs:element name='root' type='xs:base64Binary'/>" +
"</xs:schema>";

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 =
"<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema' xmlns:t='urn:t' " +
"targetNamespace='urn:t' elementFormDefault='qualified'>" +
" <xs:element name='root' type='xs:base64Binary'/>" +
"</xs:schema>";
SchemaTypeLoader loader = XmlBeans.loadXsd(new XmlObject[]{SchemaDocument.Factory.parse(xsd)});
XmlObject doc = loader.parse("<t:root xmlns:t='urn:t'>" + value + "</t:root>", null, null);
return doc.validate();
}
Expand All @@ -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="));
Expand Down
14 changes: 11 additions & 3 deletions src/test/java/misc/checkin/RichParserTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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>A</a>");
assertThrows(InvalidLexicalValueException.class, elem::getBase64Value);

Expand All @@ -81,6 +81,14 @@ void testInvalidBase64ThrowsInvalidLexicalValue() throws Exception {

XMLStreamReaderExt attByName = atFirstStartElement("<a b=\"A\"/>");
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("<a>" + bad + "</a>");
assertThrows(InvalidLexicalValueException.class, e::getBase64Value, bad);
XMLStreamReaderExt a = atFirstStartElement("<a b=\"" + bad + "\"/>");
assertThrows(InvalidLexicalValueException.class, () -> a.getAttributeBase64Value(0), bad);
}
}

@Test
Expand Down
Loading