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 fca53edaf97ddb98a55fcd25fce9ac86858d1b2b Author: Piotr P. Karwasz <[email protected]> AuthorDate: Sat Apr 25 16:20:13 2026 +0200 fix: make ExternalSchemaTest more reliable (#1) The previous version relied on Payloads.UNREACHABLE_HTTP and a 2-second timing budget combined with a keyword filter to tell "hardened-blocked" apart from "TCP refused late". On macOS and Windows runners that combination is fragile. This change adds four schema fixtures under src/test/resources/leaked/: a base included.xsd that declares a leaked element and a LeakedType, plus three wrappers that reference it through xs:include, xs:import (in a different namespace) and xs:redefine. Each wrapper cannot compile unless the schemaLocation actually resolves, so compile-success vs compile-throw is itself the hardening signal, with no timing budget and no keyword filter. This change also pairs each case with an unconfiguredCompiles* positive control that runs the same fixture through SchemaFactory.newInstance() and asserts compilation succeeds. That proves the fixtures are well-formed and the resolution would happen without hardening, so the hardenedBlocks* side is genuinely exercising hardening. Finally, this change removes the now-unused AttackTestSupport.assertXPathBlocks helper. Assisted-By: Claude Opus 4.7 (1M context) <[email protected]> --- .../xml/factory/attacks/AttackTestSupport.java | 9 --- .../xml/factory/attacks/ExternalSchemaTest.java | 92 +++++++++++++++------- src/test/resources/leaked/included.xsd | 11 +++ src/test/resources/leaked/with-import.xsd | 15 ++++ src/test/resources/leaked/with-include.xsd | 15 ++++ src/test/resources/leaked/with-redefine.xsd | 19 +++++ 6 files changed, 123 insertions(+), 38 deletions(-) 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 index 74f442e..fc5d3d9 100644 --- a/src/test/java/org/apache/commons/xml/factory/attacks/AttackTestSupport.java +++ b/src/test/java/org/apache/commons/xml/factory/attacks/AttackTestSupport.java @@ -182,15 +182,6 @@ static void assertValidatorBlocks(final String xml) { assertAttackBlocked(() -> XmlFactories.newSchemaFactory().newSchema(source(Payloads.BENIGN_SCHEMA)).newValidator().validate(source(xml))); } - /** - * Asserts that evaluating the given XPath expression on a fresh {@link javax.xml.xpath.XPath} from {@code factory} is blocked by the hardening layer. The - * expression is evaluated against an empty DOM document as context; this is enough for URI-fetching functions such as {@code doc()}, {@code collection()}, - * {@code unparsed-text()} and {@code json-doc()}, which do not consult the context node beyond requiring it to be non-null. - */ - static void assertXPathBlocks(final javax.xml.xpath.XPathFactory factory, final String expression) { - assertAttackBlocked(() -> factory.newXPath().evaluate(expression, XmlFactories.newDocumentBuilderFactory().newDocumentBuilder().newDocument())); - } - private static InputSource inputSource(final String xml) { return new InputSource(new StringReader(xml)); } 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 index b394a4c..632335e 100644 --- a/src/test/java/org/apache/commons/xml/factory/attacks/ExternalSchemaTest.java +++ b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalSchemaTest.java @@ -16,54 +16,88 @@ */ package org.apache.commons.xml.factory.attacks; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.net.URL; + +import javax.xml.XMLConstants; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.SchemaFactory; + +import org.apache.commons.xml.factory.XmlFactories; 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}. + * Checks whether schemas can pull in external schemas at compile time. + * + * <p>Each wrapper under {@code src/test/resources/leaked/} references an element or type defined only in {@code included.xsd}, so the wrapper cannot compile + * unless the external schemaLocation is actually resolved.</p> + * + * <p>Each case is exercised twice as a pair:</p> + * + * <ul> + * <li>An {@code unconfigured*} test compiles the wrapper through a default {@link SchemaFactory#newInstance(String)} and asserts compilation succeeds. This + * is the positive control: it proves the wrapper itself is well-formed and that the resolution would succeed if hardening were not in the way, so the + * paired {@code hardened*} test is genuinely exercising hardening rather than passing for an unrelated reason.</li> + * <li>A {@code hardened*} test compiles the wrapper through {@link XmlFactories#newSchemaFactory()} and asserts compilation throws. If hardening blocks the + * schemaLocation, compilation throws (either with an access-restriction message or with an unresolved-reference message); if hardening fails to block, + * compilation succeeds and the test fails.</li> + * </ul> + * + * <p>Cases covered:</p> + * + * <ul> + * <li>{@code xs:include schemaLocation="included.xsd"} of a sibling schema sharing the wrapper's target namespace.</li> + * <li>{@code xs:import namespace="..." schemaLocation="included.xsd"} of a sibling schema in a different target namespace.</li> + * <li>{@code xs:redefine schemaLocation="included.xsd"} that extends a complex type from the sibling schema.</li> + * </ul> */ class ExternalSchemaTest { - private static final String IMPORT_NAMESPACE = "http://example.org/external"; + private static void assertSchemaCompileFails(final String resource) { + final URL url = AttackTestSupport.resourceUrl(resource); + assertThrows(Exception.class, () -> { + final StreamSource src = new StreamSource(url.openStream(), url.toString()); + XmlFactories.newSchemaFactory().newSchema(src); + }, "Hardening did not block the external schemaLocation; schema compiled successfully from " + resource); + } - 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 void assertSchemaCompiles(final String resource) { + final URL url = AttackTestSupport.resourceUrl(resource); + assertDoesNotThrow(() -> { + final StreamSource src = new StreamSource(url.openStream(), url.toString()); + SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(src); + }, "Unconfigured SchemaFactory should compile " + resource + "; the wrapper or its sibling include/import/redefine target is broken."); } - 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"; + @Test + void hardenedBlocksImport() { + assertSchemaCompileFails("with-import.xsd"); } - 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 hardenedBlocksInclude() { + assertSchemaCompileFails("with-include.xsd"); + } + + @Test + void hardenedBlocksRedefine() { + assertSchemaCompileFails("with-redefine.xsd"); } @Test - void schemaFactoryBlocksImport() { - AttackTestSupport.assertSchemaCompilationBlocks(xsdWithImport(IMPORT_NAMESPACE, Payloads.UNREACHABLE_HTTP)); + void unconfiguredCompilesImport() { + assertSchemaCompiles("with-import.xsd"); } @Test - void schemaFactoryBlocksInclude() { - AttackTestSupport.assertSchemaCompilationBlocks(xsdWithInclude(Payloads.UNREACHABLE_HTTP)); + void unconfiguredCompilesInclude() { + assertSchemaCompiles("with-include.xsd"); } @Test - void schemaFactoryBlocksRedefine() { - AttackTestSupport.assertSchemaCompilationBlocks(xsdWithRedefine(Payloads.UNREACHABLE_HTTP)); + void unconfiguredCompilesRedefine() { + assertSchemaCompiles("with-redefine.xsd"); } } diff --git a/src/test/resources/leaked/included.xsd b/src/test/resources/leaked/included.xsd new file mode 100644 index 0000000..8bba9e1 --- /dev/null +++ b/src/test/resources/leaked/included.xsd @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- SPDX-License-Identifier: Apache-2.0 --> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns="http://example.org/leaked" + targetNamespace="http://example.org/leaked" + elementFormDefault="qualified"> + <xs:element name="leaked" type="xs:string"/> + <xs:complexType name="LeakedType"> + <xs:sequence/> + </xs:complexType> +</xs:schema> diff --git a/src/test/resources/leaked/with-import.xsd b/src/test/resources/leaked/with-import.xsd new file mode 100644 index 0000000..8c0205f --- /dev/null +++ b/src/test/resources/leaked/with-import.xsd @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- SPDX-License-Identifier: Apache-2.0 --> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:l="http://example.org/leaked" + targetNamespace="http://example.org/wrapper" + elementFormDefault="qualified"> + <xs:import namespace="http://example.org/leaked" schemaLocation="included.xsd"/> + <xs:element name="root"> + <xs:complexType> + <xs:sequence> + <xs:element ref="l:leaked"/> + </xs:sequence> + </xs:complexType> + </xs:element> +</xs:schema> diff --git a/src/test/resources/leaked/with-include.xsd b/src/test/resources/leaked/with-include.xsd new file mode 100644 index 0000000..f0886a5 --- /dev/null +++ b/src/test/resources/leaked/with-include.xsd @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- SPDX-License-Identifier: Apache-2.0 --> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns="http://example.org/leaked" + targetNamespace="http://example.org/leaked" + elementFormDefault="qualified"> + <xs:include schemaLocation="included.xsd"/> + <xs:element name="root"> + <xs:complexType> + <xs:sequence> + <xs:element ref="leaked"/> + </xs:sequence> + </xs:complexType> + </xs:element> +</xs:schema> diff --git a/src/test/resources/leaked/with-redefine.xsd b/src/test/resources/leaked/with-redefine.xsd new file mode 100644 index 0000000..63710e9 --- /dev/null +++ b/src/test/resources/leaked/with-redefine.xsd @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- SPDX-License-Identifier: Apache-2.0 --> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns="http://example.org/leaked" + targetNamespace="http://example.org/leaked" + elementFormDefault="qualified"> + <xs:redefine schemaLocation="included.xsd"> + <xs:complexType name="LeakedType"> + <xs:complexContent> + <xs:extension base="LeakedType"> + <xs:sequence> + <xs:element name="extra" type="xs:string"/> + </xs:sequence> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:redefine> + <xs:element name="root" type="LeakedType"/> +</xs:schema>
