| /* |
| * 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 |
| * |
| * https://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.commons.xml; |
| |
| import static org.apache.commons.xml.AttackTestSupport.assertParseFails; |
| import static org.apache.commons.xml.AttackTestSupport.assertParseSucceeds; |
| import static org.apache.commons.xml.AttackTestSupport.inputSource; |
| import static org.apache.commons.xml.AttackTestSupport.resourceUrl; |
| import static org.apache.commons.xml.AttackTestSupport.strictDocumentBuilder; |
| import static org.apache.commons.xml.AttackTestSupport.strictXMLReader; |
| |
| import javax.xml.XMLConstants; |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.SAXParser; |
| import javax.xml.parsers.SAXParserFactory; |
| |
| import org.junit.jupiter.api.Assumptions; |
| import org.junit.jupiter.api.Tag; |
| import org.junit.jupiter.api.Test; |
| import org.junit.jupiter.api.function.ThrowingSupplier; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.XMLReader; |
| |
| /** |
| * Checks that a hardened, schema-validating parser does not fetch a schema named only through a Xerces schema-location |
| * property: {@code external-noNamespaceSchemaLocation} (no-namespace schema) and {@code external-schemaLocation} |
| * (namespaced schema). |
| * |
| * <p>The fixtures declare the instance's root element, so a parser that fetches the schema validates the instance |
| * cleanly and one that does not cannot. The permissive controls prove the external schema is reachable in principle, so |
| * the hardened side throwing means the fetch was refused, not merely misconfigured. The stock JDK refuses it through |
| * {@code accessExternalSchema=""}; external Apache Xerces, which ignores that property, refuses it through the deny-all |
| * entity-resolver floor.</p> |
| * |
| * <p>Not every parser supports these schema-validation knobs (Android's KXmlParser and Expat do not), so the whole |
| * configuration runs through {@link #configureOrSkip}: a parser that rejects validation, the schema language, or the |
| * schema-location property skips the test rather than failing it.</p> |
| */ |
| @Tag("schema") |
| class SchemaLocationPropertyTest { |
| |
| private static final String SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage"; |
| private static final String SCHEMA_FEATURE = "http://apache.org/xml/features/validation/schema"; |
| private static final String EXTERNAL_NO_NS = "http://apache.org/xml/properties/schema/external-noNamespaceSchemaLocation"; |
| private static final String EXTERNAL_SCHEMA_LOCATION = "http://apache.org/xml/properties/schema/external-schemaLocation"; |
| |
| /** Instance whose root, {@code <root>}, is declared by {@code no-namespace.xsd}. */ |
| private static final String NO_NS_INSTANCE = "<root>x</root>"; |
| |
| private static final String LEAKED_NS = "http://example.org/leaked"; |
| |
| /** Instance whose root, {@code l:leaked}, is declared by {@code included.xsd} in the {@value #LEAKED_NS} namespace. */ |
| private static final String NAMESPACED_INSTANCE = "<l:leaked xmlns:l=\"" + LEAKED_NS + "\">x</l:leaked>"; |
| |
| private static String noNamespaceLocation() { |
| return resourceUrl("no-namespace.xsd").toString(); |
| } |
| |
| private static String namespacedLocation() { |
| return LEAKED_NS + " " + resourceUrl("included.xsd"); |
| } |
| |
| /** |
| * Runs the parser setup, skipping the test (rather than failing it) on parsers that do not accept these |
| * schema-validation features/properties, such as Android's KXmlParser and Expat. |
| */ |
| private static <T> T configureOrSkip(final ThrowingSupplier<T> setup) { |
| try { |
| return setup.get(); |
| } catch (final Throwable t) { |
| return Assumptions.abort("Parser does not support schema validation through these features/properties: " + t); |
| } |
| } |
| |
| private static DocumentBuilder hardenedValidatingDom(final String property, final String value) { |
| return configureOrSkip(() -> { |
| final DocumentBuilderFactory factory = XmlFactories.newDocumentBuilderFactory(); |
| factory.setNamespaceAware(true); |
| factory.setValidating(true); |
| factory.setAttribute(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI); |
| factory.setAttribute(property, value); |
| return strictDocumentBuilder(factory); |
| }); |
| } |
| |
| private static DocumentBuilder permissiveValidatingDom(final String property, final String value) { |
| return configureOrSkip(() -> { |
| final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| factory.setNamespaceAware(true); |
| factory.setValidating(true); |
| factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); |
| factory.setAttribute(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI); |
| factory.setAttribute(property, value); |
| return strictDocumentBuilder(factory); |
| }); |
| } |
| |
| private static XMLReader hardenedValidatingSax(final String property, final String value) { |
| return configureOrSkip(() -> { |
| final SAXParserFactory factory = XmlFactories.newSAXParserFactory(); |
| factory.setNamespaceAware(true); |
| factory.setValidating(true); |
| final SAXParser parser = factory.newSAXParser(); |
| parser.setProperty(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI); |
| parser.setProperty(property, value); |
| return strictXMLReader(parser.getXMLReader()); |
| }); |
| } |
| |
| private static XMLReader permissiveValidatingSax(final String property, final String value) { |
| return configureOrSkip(() -> { |
| final SAXParserFactory factory = SAXParserFactory.newInstance(); |
| factory.setNamespaceAware(true); |
| factory.setValidating(true); |
| factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); |
| factory.setFeature(SCHEMA_FEATURE, true); |
| final SAXParser parser = factory.newSAXParser(); |
| parser.setProperty(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI); |
| parser.setProperty(property, value); |
| return strictXMLReader(parser.getXMLReader()); |
| }); |
| } |
| |
| @Test |
| void hardenedDomRefusesNoNamespaceSchemaLocation() { |
| final DocumentBuilder builder = hardenedValidatingDom(EXTERNAL_NO_NS, noNamespaceLocation()); |
| assertParseFails(() -> builder.parse(inputSource(NO_NS_INSTANCE)), "DOM external-noNamespaceSchemaLocation", SAXException.class); |
| } |
| |
| @Test |
| void permissiveDomFetchesNoNamespaceSchemaLocation() { |
| final DocumentBuilder builder = permissiveValidatingDom(EXTERNAL_NO_NS, noNamespaceLocation()); |
| assertParseSucceeds(() -> builder.parse(inputSource(NO_NS_INSTANCE)), "DOM external-noNamespaceSchemaLocation (permissive)"); |
| } |
| |
| @Test |
| void hardenedDomRefusesSchemaLocation() { |
| final DocumentBuilder builder = hardenedValidatingDom(EXTERNAL_SCHEMA_LOCATION, namespacedLocation()); |
| assertParseFails(() -> builder.parse(inputSource(NAMESPACED_INSTANCE)), "DOM external-schemaLocation", SAXException.class); |
| } |
| |
| @Test |
| void permissiveDomFetchesSchemaLocation() { |
| final DocumentBuilder builder = permissiveValidatingDom(EXTERNAL_SCHEMA_LOCATION, namespacedLocation()); |
| assertParseSucceeds(() -> builder.parse(inputSource(NAMESPACED_INSTANCE)), "DOM external-schemaLocation (permissive)"); |
| } |
| |
| @Test |
| void hardenedSaxRefusesNoNamespaceSchemaLocation() { |
| final XMLReader reader = hardenedValidatingSax(EXTERNAL_NO_NS, noNamespaceLocation()); |
| assertParseFails(() -> reader.parse(inputSource(NO_NS_INSTANCE)), "SAX external-noNamespaceSchemaLocation", SAXException.class); |
| } |
| |
| @Test |
| void permissiveSaxFetchesNoNamespaceSchemaLocation() { |
| final XMLReader reader = permissiveValidatingSax(EXTERNAL_NO_NS, noNamespaceLocation()); |
| assertParseSucceeds(() -> reader.parse(inputSource(NO_NS_INSTANCE)), "SAX external-noNamespaceSchemaLocation (permissive)"); |
| } |
| |
| @Test |
| void hardenedSaxRefusesSchemaLocation() { |
| final XMLReader reader = hardenedValidatingSax(EXTERNAL_SCHEMA_LOCATION, namespacedLocation()); |
| assertParseFails(() -> reader.parse(inputSource(NAMESPACED_INSTANCE)), "SAX external-schemaLocation", SAXException.class); |
| } |
| |
| @Test |
| void permissiveSaxFetchesSchemaLocation() { |
| final XMLReader reader = permissiveValidatingSax(EXTERNAL_SCHEMA_LOCATION, namespacedLocation()); |
| assertParseSucceeds(() -> reader.parse(inputSource(NAMESPACED_INSTANCE)), "SAX external-schemaLocation (permissive)"); |
| } |
| } |