This is an automated email from the ASF dual-hosted git repository. ppkarwasz pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/commons-xml.git
commit b913da1e262ab47ae6e4b8010dd13aacb85bde66 Author: Piotr P. Karwasz <[email protected]> AuthorDate: Thu Apr 23 20:25:22 2026 +0200 feat: add stock JDK provider Adds `StockJdkProvider`, which matches the six JAXP factory classes shipped inside the JDK. The commit also adds test to check every possible way external documents can be read by a JAXP class. Assisted-By: Claude Opus 4.7 (1M context) <[email protected]> --- pom.xml | 8 +- .../xml/factory/internal/AbstractXmlProvider.java | 136 +++++++++++++ .../xml/factory/internal/HardeningException.java | 34 ++++ .../xml/factory/internal/ProviderRegistry.java | 4 +- .../xml/factory/internal/StockJdkProvider.java | 110 +++++++++++ .../org.apache.commons.xml.factory.spi.XmlProvider | 2 + .../commons/xml/factory/XmlFactoriesTest.java | 122 ++++++++++++ .../xml/factory/attacks/AttackTestSupport.java | 219 +++++++++++++++++++++ .../xml/factory/attacks/BillionLaughsTest.java | 73 +++++++ .../xml/factory/attacks/ExternalDtdTest.java | 65 ++++++ .../factory/attacks/ExternalGeneralEntityTest.java | 67 +++++++ .../attacks/ExternalParameterEntityTest.java | 68 +++++++ .../xml/factory/attacks/ExternalSchemaTest.java | 69 +++++++ .../factory/attacks/ExternalStylesheetTest.java | 66 +++++++ .../commons/xml/factory/attacks/Payloads.java | 64 ++++++ 15 files changed, 1102 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 5b483e7..5b6f678 100644 --- a/pom.xml +++ b/pom.xml @@ -75,9 +75,9 @@ Run the test suite four times, each with exactly one JAXP implementation on the classpath: - test-stockjdk: stock JDK factories only - - test-xerces: stock JDK + external Apache Xerces - - test-woodstox: stock JDK + Woodstox (StAX) - - test-saxon: stock JDK + Saxon-HE (TrAX, XPath) + - test-xerces: stock JDK + external Apache Xerces (stashed until XercesProvider lands) + - test-woodstox: stock JDK + Woodstox (StAX) (stashed until WoodstoxProvider lands) + - test-saxon: stock JDK + Saxon-HE (TrAX, XPath) (stashed until SaxonProvider lands) --> <plugin> <groupId>org.apache.maven.plugins</groupId> @@ -94,6 +94,7 @@ <reportsDirectory>${project.build.directory}/surefire-reports/stockjdk</reportsDirectory> </configuration> </execution> + <!-- <execution> <id>test-xerces</id> <goals><goal>test</goal></goals> @@ -136,6 +137,7 @@ </additionalClassPathDependencies> </configuration> </execution> + --> </executions> </plugin> diff --git a/src/main/java/org/apache/commons/xml/factory/internal/AbstractXmlProvider.java b/src/main/java/org/apache/commons/xml/factory/internal/AbstractXmlProvider.java new file mode 100644 index 0000000..4837b43 --- /dev/null +++ b/src/main/java/org/apache/commons/xml/factory/internal/AbstractXmlProvider.java @@ -0,0 +1,136 @@ +/* + * 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.factory.internal; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.stream.XMLInputFactory; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerFactory; +import javax.xml.validation.SchemaFactory; +import javax.xml.xpath.XPathFactory; +import javax.xml.xpath.XPathFactoryConfigurationException; + +import org.apache.commons.xml.factory.spi.XmlProvider; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; + +/** + * Common scaffolding for {@link XmlProvider} implementations bundled with this library. + * + * <p>Subclasses pass the fully qualified class names of the factory implementations they handle to {@link #AbstractXmlProvider(String...)}. The inherited + * {@link #supports(Class)} then returns {@code true} for exactly those classes, matching on {@link Class#getName()}.</p> + * + * <p>The class also exposes the JAXP {@code setFeature}/{@code setAttribute}/{@code setProperty} helpers every provider uses. Each helper wraps the checked + * exception documented by the corresponding JAXP setter in a {@link HardeningException} whose message names the feature, property or attribute that failed. + * Every other throwable (including {@link LinkageError} and {@link AbstractMethodError}, which typically signal a JAXP implementation on the classpath older + * than the one this library was compiled against) is left to propagate so the caller sees the underlying problem.</p> + */ +abstract class AbstractXmlProvider implements XmlProvider { + + private static HardeningException fail(final Object factory, final String kind, final String name, final Throwable cause) { + return new HardeningException("Failed to set " + kind + " '" + name + "' on " + factory.getClass().getName(), cause); + } + + static void setAttribute(final DocumentBuilderFactory factory, final String attribute, final Object value) { + try { + factory.setAttribute(attribute, value); + } catch (final IllegalArgumentException e) { + throw fail(factory, "attribute", attribute, e); + } + } + + static void setAttribute(final TransformerFactory factory, final String attribute, final Object value) { + try { + factory.setAttribute(attribute, value); + } catch (final IllegalArgumentException e) { + throw fail(factory, "attribute", attribute, e); + } + } + + static void setFeature(final DocumentBuilderFactory factory, final String feature, final boolean value) { + try { + factory.setFeature(feature, value); + } catch (final ParserConfigurationException e) { + throw fail(factory, "feature", feature, e); + } + } + + static void setFeature(final SAXParserFactory factory, final String feature, final boolean value) { + try { + factory.setFeature(feature, value); + } catch (final ParserConfigurationException | SAXNotRecognizedException | SAXNotSupportedException e) { + throw fail(factory, "feature", feature, e); + } + } + + static void setFeature(final TransformerFactory factory, final String feature, final boolean value) { + try { + factory.setFeature(feature, value); + } catch (final TransformerConfigurationException e) { + throw fail(factory, "feature", feature, e); + } + } + + static void setFeature(final XPathFactory factory, final String feature, final boolean value) { + try { + factory.setFeature(feature, value); + } catch (final XPathFactoryConfigurationException e) { + throw fail(factory, "feature", feature, e); + } + } + + static void setFeature(final SchemaFactory factory, final String feature, final boolean value) { + try { + factory.setFeature(feature, value); + } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { + throw fail(factory, "feature", feature, e); + } + } + + static void setProperty(final XMLInputFactory factory, final String property, final Object value) { + try { + factory.setProperty(property, value); + } catch (final IllegalArgumentException e) { + throw fail(factory, "property", property, e); + } + } + + static void setProperty(final SchemaFactory factory, final String property, final Object value) { + try { + factory.setProperty(property, value); + } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { + throw fail(factory, "property", property, e); + } + } + private final Set<String> supported; + + protected AbstractXmlProvider(final String... supportedClassNames) { + this.supported = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(supportedClassNames))); + } + + @Override + public final boolean supports(final Class<?> factoryClass) { + return factoryClass != null && supported.contains(factoryClass.getName()); + } +} diff --git a/src/main/java/org/apache/commons/xml/factory/internal/HardeningException.java b/src/main/java/org/apache/commons/xml/factory/internal/HardeningException.java new file mode 100644 index 0000000..dbfaea9 --- /dev/null +++ b/src/main/java/org/apache/commons/xml/factory/internal/HardeningException.java @@ -0,0 +1,34 @@ +/* + * 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.factory.internal; + +/** + * Thrown when a provider cannot fully harden a factory. + * + * <p>The message names the specific feature or property that failed; the cause is the original checked or unchecked exception from the JAXP + * implementation.</p> + * + * <p>Package-private by design: callers should catch {@link IllegalStateException}, which this extends.</p> + */ +class HardeningException extends IllegalStateException { + + private static final long serialVersionUID = 1L; + + HardeningException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/apache/commons/xml/factory/internal/ProviderRegistry.java b/src/main/java/org/apache/commons/xml/factory/internal/ProviderRegistry.java index 0beb077..954bb68 100644 --- a/src/main/java/org/apache/commons/xml/factory/internal/ProviderRegistry.java +++ b/src/main/java/org/apache/commons/xml/factory/internal/ProviderRegistry.java @@ -33,7 +33,7 @@ * <h2>Dispatch order</h2> * * <ol> - * <li>Bundled providers in a fixed order.</li> + * <li>Bundled providers in a fixed order: {@link StockJdkProvider}, {@link XercesProvider}, {@link WoodstoxProvider}, {@link SaxonProvider}.</li> * <li>Third-party providers discovered via {@link ServiceLoader}, in the order {@link ServiceLoader} yields them.</li> * <li>If nothing matches, {@link UnsupportedXmlImplementationException}.</li> * </ol> @@ -97,7 +97,7 @@ private XmlProvider lookup(final Class<?> factoryClass) { } private static List<XmlProvider> bundledProviders() { - return Collections.emptyList(); + return Collections.singletonList(new StockJdkProvider()); } private static Iterable<XmlProvider> serviceLoaderProviders() { diff --git a/src/main/java/org/apache/commons/xml/factory/internal/StockJdkProvider.java b/src/main/java/org/apache/commons/xml/factory/internal/StockJdkProvider.java new file mode 100644 index 0000000..2b0b51c --- /dev/null +++ b/src/main/java/org/apache/commons/xml/factory/internal/StockJdkProvider.java @@ -0,0 +1,110 @@ +/* + * 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.factory.internal; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.stream.XMLInputFactory; +import javax.xml.transform.TransformerFactory; +import javax.xml.validation.SchemaFactory; +import javax.xml.xpath.XPathFactory; + +import org.apache.commons.xml.factory.spi.XmlProvider; + +/** + * {@link XmlProvider} for the stock JDK's JAXP implementation. + * + * <p>This is the internal fork of Apache Xerces, Xalan and friends shipped inside {@code com.sun.org.apache.*} and {@code com.sun.xml.internal.*} packages.</p> + * + * <p>Must be declared {@code public} so {@link java.util.ServiceLoader} can load it from {@code META-INF/services/}.</p> + */ +public final class StockJdkProvider extends AbstractXmlProvider { + + private static final String FEATURE_DISALLOW_DOCTYPE = + "http://apache.org/xml/features/disallow-doctype-decl"; + + /** Default constructor; invoked by {@link java.util.ServiceLoader} and the registry. */ + public StockJdkProvider() { + super("com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl", + "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", + "com.sun.xml.internal.stream.XMLInputFactoryImpl", + "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl", + "com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl", + "com.sun.org.apache.xerces.internal.jaxp.validation.XMLSchemaFactory"); + } + + @Override + public void configure(final DocumentBuilderFactory factory) { + setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + setFeature(factory, FEATURE_DISALLOW_DOCTYPE, true); + factory.setXIncludeAware(false); + factory.setValidating(false); + // Defence-in-depth: disallow-doctype-decl and validating=false already block external resolution on the normal parse path. These properties only fire + // if the caller re-enables DOCTYPE, turns validation on, attaches a Schema, or registers an EntityResolver that returns null instead of throwing; the + // default resolver then consults them and refuses the fetch before any I/O. + // - ACCESS_EXTERNAL_DTD: protocols allowed for external DTDs declared by DOCTYPE SYSTEM/PUBLIC. + // - ACCESS_EXTERNAL_SCHEMA: protocols allowed for xsi:schemaLocation hints and for xs:import/xs:include/xs:redefine in a validating Schema. + setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_DTD, ""); + setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + } + + @Override + public void configure(final SAXParserFactory factory) { + setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + setFeature(factory, FEATURE_DISALLOW_DOCTYPE, true); + factory.setXIncludeAware(false); + factory.setValidating(false); + // No ACCESS_EXTERNAL_* here: the JAXP SAXParserFactory API exposes neither setAttribute nor setProperty, so these properties cannot be applied at + // factory level. disallow-doctype-decl and validating=false already block external resolution on the normal parse path; callers who modify the posture + // should set parser.setProperty(XMLConstants.ACCESS_EXTERNAL_{DTD,SCHEMA}, "") on each SAXParser returned by newSAXParser(). + } + + @Override + public void configure(final XMLInputFactory factory) { + setProperty(factory, XMLInputFactory.SUPPORT_DTD, false); + setProperty(factory, XMLInputFactory.IS_VALIDATING, false); + } + + @Override + public void configure(final TransformerFactory factory) { + setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + // Defence-in-depth: FSP already tightens these to the empty string on the JDK's XMLSecurityManager path. They only fire if the caller turns FSP off + // on the returned factory; the default resolver then consults them and refuses the fetch before any I/O. + // - ACCESS_EXTERNAL_DTD: protocols allowed for external DTDs referenced by DOCTYPE SYSTEM in the stylesheet or XML input. + // - ACCESS_EXTERNAL_STYLESHEET: protocols allowed for xsl:import, xsl:include and document() URIs during compile and transform. + setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_DTD, ""); + setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + } + + @Override + public void configure(final XPathFactory factory) { + setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + } + + @Override + public void configure(final SchemaFactory factory) { + setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + // Defence-in-depth: FSP already tightens these to the empty string on the JDK's XMLSecurityManager path. They only fire if the caller turns FSP off + // on the returned factory; the default resolver then consults them and refuses the fetch before any I/O. The settings propagate into Validators + // created from the compiled Schema. + // - ACCESS_EXTERNAL_DTD: protocols allowed for external DTDs declared by DOCTYPE SYSTEM in the schema or in a validated instance. + // - ACCESS_EXTERNAL_SCHEMA: protocols allowed for xs:import, xs:include and xs:redefine schemaLocation URIs during schema compilation. + setProperty(factory, XMLConstants.ACCESS_EXTERNAL_DTD, ""); + setProperty(factory, XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + } +} diff --git a/src/main/resources/META-INF/services/org.apache.commons.xml.factory.spi.XmlProvider b/src/main/resources/META-INF/services/org.apache.commons.xml.factory.spi.XmlProvider new file mode 100644 index 0000000..667471d --- /dev/null +++ b/src/main/resources/META-INF/services/org.apache.commons.xml.factory.spi.XmlProvider @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +org.apache.commons.xml.factory.internal.StockJdkProvider diff --git a/src/test/java/org/apache/commons/xml/factory/XmlFactoriesTest.java b/src/test/java/org/apache/commons/xml/factory/XmlFactoriesTest.java new file mode 100644 index 0000000..d185c28 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/factory/XmlFactoriesTest.java @@ -0,0 +1,122 @@ +/* + * 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.factory; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.StringReader; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.stream.XMLInputFactory; +import javax.xml.transform.TransformerFactory; +import javax.xml.validation.SchemaFactory; +import javax.xml.xpath.XPathFactory; + +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; + +/** + * Public-API smoke tests for {@link XmlFactories}. + * + * <p>Attack tests live in the {@code attacks} sub-package; this file only verifies that fresh factories are returned, that they report safe defaults, and that + * a benign document still parses successfully.</p> + */ +class XmlFactoriesTest { + + private static final String BENIGN_XML = + "<?xml version=\"1.0\"?>\n<root><child>hello</child></root>\n"; + + @Test + void newDocumentBuilderFactoryReturnsFreshInstance() { + final DocumentBuilderFactory a = XmlFactories.newDocumentBuilderFactory(); + final DocumentBuilderFactory b = XmlFactories.newDocumentBuilderFactory(); + assertNotNull(a); + assertNotNull(b); + assertNotSame(a, b); + } + + @Test + void newDocumentBuilderFactoryDisablesXIncludeAndValidation() { + final DocumentBuilderFactory factory = XmlFactories.newDocumentBuilderFactory(); + assertFalse(factory.isXIncludeAware(), "XInclude must be off by default"); + assertFalse(factory.isValidating(), "Validation must be off by default"); + } + + @Test + void newDocumentBuilderFactoryEnablesSecureProcessing() throws Exception { + final DocumentBuilderFactory factory = XmlFactories.newDocumentBuilderFactory(); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING), + "FEATURE_SECURE_PROCESSING must be on"); + } + + @Test + void benignDocumentParses() throws Exception { + final Document doc = XmlFactories.newDocumentBuilderFactory().newDocumentBuilder().parse(new InputSource(new StringReader(BENIGN_XML))); + assertNotNull(doc); + assertNotNull(doc.getDocumentElement()); + } + + @Test + void newSAXParserFactoryReturnsFreshInstance() { + final SAXParserFactory a = XmlFactories.newSAXParserFactory(); + final SAXParserFactory b = XmlFactories.newSAXParserFactory(); + assertNotSame(a, b); + assertFalse(a.isValidating()); + assertFalse(a.isXIncludeAware()); + } + + @Test + void newXMLInputFactoryReturnsFreshInstance() { + final XMLInputFactory a = XmlFactories.newXMLInputFactory(); + final XMLInputFactory b = XmlFactories.newXMLInputFactory(); + assertNotSame(a, b); + assertEquals(Boolean.FALSE, a.getProperty(XMLInputFactory.SUPPORT_DTD)); + assertEquals(Boolean.FALSE, a.getProperty(XMLInputFactory.IS_VALIDATING)); + } + + @Test + void newTransformerFactoryReturnsFreshInstance() { + final TransformerFactory a = XmlFactories.newTransformerFactory(); + final TransformerFactory b = XmlFactories.newTransformerFactory(); + assertNotSame(a, b); + } + + @Test + void newXPathFactoryReturnsFreshInstance() throws Exception { + final XPathFactory a = XmlFactories.newXPathFactory(); + final XPathFactory b = XmlFactories.newXPathFactory(); + assertNotSame(a, b); + assertTrue(a.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void newSchemaFactoryReturnsFreshInstance() throws Exception { + final SchemaFactory a = XmlFactories.newSchemaFactory(); + final SchemaFactory b = XmlFactories.newSchemaFactory(); + assertNotSame(a, b); + assertTrue(a.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + +} diff --git a/src/test/java/org/apache/commons/xml/factory/attacks/AttackTestSupport.java b/src/test/java/org/apache/commons/xml/factory/attacks/AttackTestSupport.java new file mode 100644 index 0000000..b5c90e4 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/factory/attacks/AttackTestSupport.java @@ -0,0 +1,219 @@ +/* + * 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.factory.attacks; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.ByteArrayInputStream; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.apache.commons.xml.factory.XmlFactories; +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +/** + * Shared fixtures for attack tests. + * + * <p>For each factory type the helpers parse the supplied payload and assert that either the parser throws a hardening-style exception or completes without + * any attempt to resolve an external resource.</p> + */ +final class AttackTestSupport { + + /** Functional interface so test bodies can throw checked JAXP exceptions. */ + @FunctionalInterface + interface ThrowingAction { + void run() throws Exception; + } + + /** + * Keywords (lowercase) that indicate the exception comes from the hardening layer or the parser's refusal to handle a DOCTYPE/external reference, rather + * than from a successful-but-late fetch. + */ + private static final Set<String> HARDENING_KEYWORDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "doctype", "dtd", "disallow", "external", "entity", "entities", "secure", "access is not allowed", + "support_dtd", "feature_secure_processing", "prohibited"))); + + /** + * Upper bound on elapsed time for any payload to be processed. + * + * <p>A hardened parser either rejects the DOCTYPE up-front (microseconds) or silently skips the DTD (milliseconds). A parser that actually attempts a + * network fetch would either block on DNS/TCP timeouts or fail with a connection error: both incompatible with this budget.</p> + */ + private static final long MAX_MILLIS = 2_000L; + + /** + * Asserts that the supplied action threw an exception quickly and the exception chain indicates the hardening layer (not a network/file attempt). + */ + private static void assertAttackBlocked(final ThrowingAction action) { + final long start = System.nanoTime(); + Throwable thrown = null; + try { + action.run(); + } catch (final Throwable t) { + thrown = t; + } + final long elapsedMs = (System.nanoTime() - start) / 1_000_000L; + if (thrown == null) { + fail("Expected hardening to reject the attack, but parsing completed successfully."); + return; + } + if (elapsedMs >= MAX_MILLIS) { + fail(String.format("Hardening rejected the payload only after %dms: that suggests an actual resolution attempt rather than an up-front block.", + elapsedMs), thrown); + } + if (!messageOrCauseContainsHardeningKeyword(thrown)) { + fail("Exception does not look like it came from the hardening layer.", thrown); + } + } + + static void assertDomBlocks(final String payload) { + assertAttackBlocked(() -> XmlFactories.newDocumentBuilderFactory().newDocumentBuilder().parse(inputSource(payload))); + } + + static void assertSaxBlocks(final String payload) { + assertAttackBlocked(() -> { + final XMLReader reader = XmlFactories.newSAXParserFactory().newSAXParser().getXMLReader(); + reader.setContentHandler(new DefaultHandler()); + reader.setErrorHandler(new DefaultHandler()); + reader.parse(inputSource(payload)); + }); + } + + /** + * Asserts that compiling the given schema via {@code SchemaFactory.newSchema(Source)} is blocked by the hardening layer. + */ + static void assertSchemaCompilationBlocks(final String xsd) { + assertAttackBlocked(() -> XmlFactories.newSchemaFactory().newSchema(source(xsd))); + } + + /** + * Asserts that a StAX parse of the supplied payload blocks DTD resolution. + * + * <p>StAX with {@code SUPPORT_DTD=false} either throws on entity references in content (Billion Laughs, external general entity) or silently skips the + * DTD. For the benign-body payloads the skipped DTD never becomes an error, so instead of requiring an exception we plant a counting {@code XMLResolver} + * and assert it is never invoked.</p> + */ + static void assertStaxBlocks(final String payload) { + final AtomicInteger resolutions = new AtomicInteger(); + final long start = System.nanoTime(); + Throwable thrown = null; + try { + final XMLInputFactory factory = XmlFactories.newXMLInputFactory(); + factory.setXMLResolver((publicID, systemID, baseURI, namespace) -> { + resolutions.incrementAndGet(); + return new ByteArrayInputStream(new byte[0]); + }); + final XMLStreamReader stream = factory.createXMLStreamReader(new StringReader(payload)); + try { + while (stream.hasNext()) { + stream.next(); + } + } finally { + stream.close(); + } + final XMLEventReader events = factory.createXMLEventReader(new StringReader(payload)); + try { + while (events.hasNext()) { + events.nextEvent(); + } + } finally { + events.close(); + } + } catch (final Throwable t) { + thrown = t; + } + final long elapsedMs = (System.nanoTime() - start) / 1_000_000L; + if (elapsedMs >= MAX_MILLIS) { + fail(String.format("StAX parse took %dms: suggests an actual external resolution attempt.", elapsedMs)); + } + assertEquals(0, resolutions.get(), String.format( + "StAX parser invoked the external resolver %d time(s); hardening failed to block DTD processing. Exception (if any): %s", + resolutions.get(), + thrown)); + } + + /** + * Asserts that compiling the given stylesheet via {@code TransformerFactory.newTransformer(Source)} is blocked by the hardening layer. + */ + static void assertStylesheetCompilationBlocks(final String xslt) { + assertAttackBlocked(() -> XmlFactories.newTransformerFactory().newTransformer(source(xslt))); + } + + static void assertTransformerBlocks(final String payload) { + assertAttackBlocked(() -> XmlFactories.newTransformerFactory().newTransformer().transform(source(payload), new StreamResult(new StringWriter()))); + } + + /** + * Asserts that compiling the given stylesheet and then transforming the given input through it is blocked by the hardening layer. Used for attacks whose + * external fetch is triggered at {@code Transformer.transform(...)} time rather than compile time (for example {@code document()} in an XSLT template). + */ + static void assertTransformerBlocksWithStylesheet(final String xslt, final String input) { + assertAttackBlocked(() -> XmlFactories.newTransformerFactory() + .newTransformer(source(xslt)) + .transform(source(input), new StreamResult(new StringWriter()))); + } + + /** + * Asserts that validating the given XML instance through a {@link javax.xml.validation.Validator} obtained from {@link Payloads#BENIGN_SCHEMA} is blocked + * by the hardening layer. The schema itself is trusted and benign; the attack lives in the instance document. + */ + static void assertValidatorBlocks(final String xml) { + assertAttackBlocked(() -> XmlFactories.newSchemaFactory().newSchema(source(Payloads.BENIGN_SCHEMA)).newValidator().validate(source(xml))); + } + + private static InputSource inputSource(final String xml) { + return new InputSource(new StringReader(xml)); + } + + private static boolean messageOrCauseContainsHardeningKeyword(final Throwable t) { + Throwable current = t; + while (current != null) { + final String msg = current.getMessage(); + if (msg != null) { + final String lower = msg.toLowerCase(Locale.ROOT); + if (HARDENING_KEYWORDS.stream().anyMatch(lower::contains)) { + return true; + } + } + current = current.getCause(); + } + return false; + } + + private static StreamSource source(final String xml) { + return new StreamSource(new StringReader(xml)); + } + + private AttackTestSupport() { + } + +} diff --git a/src/test/java/org/apache/commons/xml/factory/attacks/BillionLaughsTest.java b/src/test/java/org/apache/commons/xml/factory/attacks/BillionLaughsTest.java new file mode 100644 index 0000000..76e0bb1 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/factory/attacks/BillionLaughsTest.java @@ -0,0 +1,73 @@ +/* + * 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.factory.attacks; + +import org.junit.jupiter.api.Test; + +class BillionLaughsTest { + + private static final String INSERTION = "&lol6;"; + + private static String withDoctype(final String rootQName, final String body) { + return "<?xml version=\"1.0\"?>\n" + + "<!DOCTYPE " + rootQName + " [\n" + + " <!ENTITY lol \"lol\">\n" + + " <!ENTITY lol1 \"&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;\">\n" + + " <!ENTITY lol2 \"&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;\">\n" + + " <!ENTITY lol3 \"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;\">\n" + + " <!ENTITY lol4 \"&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;\">\n" + + " <!ENTITY lol5 \"&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;\">\n" + + " <!ENTITY lol6 \"&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;\">\n" + + "]>\n" + + body + "\n"; + } + + @Test + void domBlocksBillionLaughs() { + AttackTestSupport.assertDomBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void saxBlocksBillionLaughs() { + AttackTestSupport.assertSaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void schemaFactoryBlocksBillionLaughs() { + AttackTestSupport.assertSchemaCompilationBlocks(withDoctype("xs:schema", Payloads.xsdBody(INSERTION))); + } + + @Test + void staxBlocksBillionLaughs() { + AttackTestSupport.assertStaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void transformerBlocksBillionLaughs() { + AttackTestSupport.assertTransformerBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void transformerStylesheetBlocksBillionLaughs() { + AttackTestSupport.assertStylesheetCompilationBlocks(withDoctype("xsl:stylesheet", Payloads.xsltBody(INSERTION))); + } + + @Test + void validatorBlocksBillionLaughs() { + AttackTestSupport.assertValidatorBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } +} diff --git a/src/test/java/org/apache/commons/xml/factory/attacks/ExternalDtdTest.java b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalDtdTest.java new file mode 100644 index 0000000..db9965b --- /dev/null +++ b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalDtdTest.java @@ -0,0 +1,65 @@ +/* + * 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.factory.attacks; + +import org.junit.jupiter.api.Test; + +class ExternalDtdTest { + + private static final String INSERTION = ""; + + private static String withDoctype(final String rootQName, final String body) { + return "<?xml version=\"1.0\"?>\n" + + "<!DOCTYPE " + rootQName + " SYSTEM \"" + Payloads.UNREACHABLE_HTTP + "\">\n" + + body + "\n"; + } + + @Test + void domBlocks() { + AttackTestSupport.assertDomBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void saxBlocks() { + AttackTestSupport.assertSaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void schemaFactoryBlocks() { + AttackTestSupport.assertSchemaCompilationBlocks(withDoctype("xs:schema", Payloads.xsdBody(INSERTION))); + } + + @Test + void staxBlocks() { + AttackTestSupport.assertStaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void transformerBlocks() { + AttackTestSupport.assertTransformerBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void transformerStylesheetBlocks() { + AttackTestSupport.assertStylesheetCompilationBlocks(withDoctype("xsl:stylesheet", Payloads.xsltBody(INSERTION))); + } + + @Test + void validatorBlocks() { + AttackTestSupport.assertValidatorBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } +} diff --git a/src/test/java/org/apache/commons/xml/factory/attacks/ExternalGeneralEntityTest.java b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalGeneralEntityTest.java new file mode 100644 index 0000000..d0cacf9 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalGeneralEntityTest.java @@ -0,0 +1,67 @@ +/* + * 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.factory.attacks; + +import org.junit.jupiter.api.Test; + +class ExternalGeneralEntityTest { + + private static final String INSERTION = "&xxe;"; + + private static String withDoctype(final String rootQName, final String body) { + return "<?xml version=\"1.0\"?>\n" + + "<!DOCTYPE " + rootQName + " [\n" + + " <!ENTITY xxe SYSTEM \"" + Payloads.UNREACHABLE_FILE + "\">\n" + + "]>\n" + + body + "\n"; + } + + @Test + void domBlocks() { + AttackTestSupport.assertDomBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void saxBlocks() { + AttackTestSupport.assertSaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void schemaFactoryBlocks() { + AttackTestSupport.assertSchemaCompilationBlocks(withDoctype("xs:schema", Payloads.xsdBody(INSERTION))); + } + + @Test + void staxBlocks() { + AttackTestSupport.assertStaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void transformerBlocks() { + AttackTestSupport.assertTransformerBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void transformerStylesheetBlocks() { + AttackTestSupport.assertStylesheetCompilationBlocks(withDoctype("xsl:stylesheet", Payloads.xsltBody(INSERTION))); + } + + @Test + void validatorBlocks() { + AttackTestSupport.assertValidatorBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } +} diff --git a/src/test/java/org/apache/commons/xml/factory/attacks/ExternalParameterEntityTest.java b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalParameterEntityTest.java new file mode 100644 index 0000000..b1f853c --- /dev/null +++ b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalParameterEntityTest.java @@ -0,0 +1,68 @@ +/* + * 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.factory.attacks; + +import org.junit.jupiter.api.Test; + +class ExternalParameterEntityTest { + + private static final String INSERTION = ""; + + private static String withDoctype(final String rootQName, final String body) { + return "<?xml version=\"1.0\"?>\n" + + "<!DOCTYPE " + rootQName + " [\n" + + " <!ENTITY % xxe SYSTEM \"" + Payloads.UNREACHABLE_HTTP + "\">\n" + + " %xxe;\n" + + "]>\n" + + body + "\n"; + } + + @Test + void domBlocks() { + AttackTestSupport.assertDomBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void saxBlocks() { + AttackTestSupport.assertSaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void schemaFactoryBlocks() { + AttackTestSupport.assertSchemaCompilationBlocks(withDoctype("xs:schema", Payloads.xsdBody(INSERTION))); + } + + @Test + void staxBlocks() { + AttackTestSupport.assertStaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void transformerBlocks() { + AttackTestSupport.assertTransformerBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } + + @Test + void transformerStylesheetBlocks() { + AttackTestSupport.assertStylesheetCompilationBlocks(withDoctype("xsl:stylesheet", Payloads.xsltBody(INSERTION))); + } + + @Test + void validatorBlocks() { + AttackTestSupport.assertValidatorBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + } +} diff --git a/src/test/java/org/apache/commons/xml/factory/attacks/ExternalSchemaTest.java b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalSchemaTest.java new file mode 100644 index 0000000..b394a4c --- /dev/null +++ b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalSchemaTest.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 + * + * 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.factory.attacks; + +import org.junit.jupiter.api.Test; + +/** + * Exercises {@link javax.xml.XMLConstants#ACCESS_EXTERNAL_SCHEMA ACCESS_EXTERNAL_SCHEMA} via the three XSD compile-time vectors it guards: + * {@code xs:import}, {@code xs:include} and {@code xs:redefine}. + */ +class ExternalSchemaTest { + + private static final String IMPORT_NAMESPACE = "http://example.org/external"; + + private static String xsdWithImport(final String namespace, final String schemaLocation) { + return "<?xml version=\"1.0\"?>\n" + + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n" + + " <xs:import namespace=\"" + namespace + "\" schemaLocation=\"" + schemaLocation + "\"/>\n" + + " <xs:element name=\"root\" type=\"xs:string\"/>\n" + + "</xs:schema>\n"; + } + + private static String xsdWithInclude(final String schemaLocation) { + return "<?xml version=\"1.0\"?>\n" + + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n" + + " <xs:include schemaLocation=\"" + schemaLocation + "\"/>\n" + + " <xs:element name=\"root\" type=\"xs:string\"/>\n" + + "</xs:schema>\n"; + } + + private static String xsdWithRedefine(final String schemaLocation) { + return "<?xml version=\"1.0\"?>\n" + + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n" + + " <xs:redefine schemaLocation=\"" + schemaLocation + "\">\n" + + " <xs:complexType name=\"Placeholder\"><xs:sequence/></xs:complexType>\n" + + " </xs:redefine>\n" + + " <xs:element name=\"root\" type=\"xs:string\"/>\n" + + "</xs:schema>\n"; + } + + @Test + void schemaFactoryBlocksImport() { + AttackTestSupport.assertSchemaCompilationBlocks(xsdWithImport(IMPORT_NAMESPACE, Payloads.UNREACHABLE_HTTP)); + } + + @Test + void schemaFactoryBlocksInclude() { + AttackTestSupport.assertSchemaCompilationBlocks(xsdWithInclude(Payloads.UNREACHABLE_HTTP)); + } + + @Test + void schemaFactoryBlocksRedefine() { + AttackTestSupport.assertSchemaCompilationBlocks(xsdWithRedefine(Payloads.UNREACHABLE_HTTP)); + } +} diff --git a/src/test/java/org/apache/commons/xml/factory/attacks/ExternalStylesheetTest.java b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalStylesheetTest.java new file mode 100644 index 0000000..f35842e --- /dev/null +++ b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalStylesheetTest.java @@ -0,0 +1,66 @@ +/* + * 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.factory.attacks; + +import org.junit.jupiter.api.Test; + +/** + * Exercises {@link javax.xml.XMLConstants#ACCESS_EXTERNAL_STYLESHEET ACCESS_EXTERNAL_STYLESHEET} via the three XSLT vectors it guards: + * {@code xsl:include} and {@code xsl:import} at compile time, and the {@code document()} function at transform time. + */ +class ExternalStylesheetTest { + + private static final String INPUT = "<root/>"; + + private static String xsltWithDocumentCall(final String uri) { + return "<?xml version=\"1.0\"?>\n" + + "<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n" + + " <xsl:template match=\"/\">\n" + + " <xsl:copy-of select=\"document('" + uri + "')\"/>\n" + + " </xsl:template>\n" + + "</xsl:stylesheet>\n"; + } + + private static String xsltWithImport(final String href) { + return "<?xml version=\"1.0\"?>\n" + + "<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n" + + " <xsl:import href=\"" + href + "\"/>\n" + + "</xsl:stylesheet>\n"; + } + + private static String xsltWithInclude(final String href) { + return "<?xml version=\"1.0\"?>\n" + + "<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n" + + " <xsl:include href=\"" + href + "\"/>\n" + + "</xsl:stylesheet>\n"; + } + + @Test + void transformerBlocksDocument() { + AttackTestSupport.assertTransformerBlocksWithStylesheet(xsltWithDocumentCall(Payloads.UNREACHABLE_HTTP), INPUT); + } + + @Test + void transformerStylesheetBlocksImport() { + AttackTestSupport.assertStylesheetCompilationBlocks(xsltWithImport(Payloads.UNREACHABLE_HTTP)); + } + + @Test + void transformerStylesheetBlocksInclude() { + AttackTestSupport.assertStylesheetCompilationBlocks(xsltWithInclude(Payloads.UNREACHABLE_HTTP)); + } +} diff --git a/src/test/java/org/apache/commons/xml/factory/attacks/Payloads.java b/src/test/java/org/apache/commons/xml/factory/attacks/Payloads.java new file mode 100644 index 0000000..a16a01a --- /dev/null +++ b/src/test/java/org/apache/commons/xml/factory/attacks/Payloads.java @@ -0,0 +1,64 @@ +/* + * 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.factory.attacks; + +/** + * Shared building blocks for attack-test payloads: unreachable-URI constants, benign reference documents, and body-shape helpers for XML, XSLT and XSD. + * + * <p>Each attack test class combines one of the body-shape helpers with an attack-specific DOCTYPE prologue of its own.</p> + */ +final class Payloads { + + /** Trivial W3C XML Schema that validates {@link #xmlBody(String)} output. */ + static final String BENIGN_SCHEMA = + "<?xml version=\"1.0\"?>\n" + + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n" + + " <xs:element name=\"root\">\n" + + " <xs:complexType>\n" + + " <xs:sequence>\n" + + " <xs:element name=\"child\" type=\"xs:string\"/>\n" + + " </xs:sequence>\n" + + " </xs:complexType>\n" + + " </xs:element>\n" + + "</xs:schema>\n"; + + /** A file URI that cannot exist on any reasonable filesystem. */ + static final String UNREACHABLE_FILE = "file:/dev/null/nonexistent/evil.dtd"; + + /** An unreachable HTTP URI: TCP connect to localhost:1 rejects quickly. */ + static final String UNREACHABLE_HTTP = "http://127.0.0.1:1/evil.dtd"; + + private Payloads() { + } + + static String xmlBody(final String text) { + return "<root><child>" + text + "</child></root>"; + } + + static String xsltBody(final String text) { + return "<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n" + + " <xsl:template match=\"/\">" + text + "</xsl:template>\n" + + "</xsl:stylesheet>"; + } + + static String xsdBody(final String text) { + return "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n" + + " <xs:annotation><xs:documentation>" + text + "</xs:documentation></xs:annotation>\n" + + " <xs:element name=\"root\" type=\"xs:string\"/>\n" + + "</xs:schema>"; + } +}
