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 f847a03936fa73ab010512ab6c21d7866ea162f8 Author: Piotr P. Karwasz <[email protected]> AuthorDate: Sat Apr 25 19:25:27 2026 +0200 fix: make DTD-based attack tests more reliable The previous ExternalDtdTest, ExternalGeneralEntityTest and ExternalParameterEntityTest relied on a 2-second timing budget plus a keyword filter to distinguish "hardened-blocked" from "TCP refused late". On macOS and Windows runners that combination is fragile. This change moves the tests to a sibling DTD on the test classpath and rewrites each wrapper so the parse cannot succeed unless the external reference actually resolves. BillionLaughsTest gets the same paired structure once the payload is sized to fit between the JDK's entity-expansion limit and XSLTC's per-class string-constant ceiling. This change also aligns Xerces's entity-expansion and max-occur limits with the JDK's. Xerces's own defaults are higher (100000 vs 64000), so without this a Validator from a hardened SchemaFactory would let DTD-borne attacks expand further than on the pure-JDK path; the alignment reads the same jdk.xml.* system properties the JDK consults, with the JDK's own defaults as fallback. Assisted-By: Claude Opus 4.7 (1M context) <[email protected]> --- .../xml/factory/internal/AbstractXmlProvider.java | 17 ++ .../xml/factory/internal/XercesProvider.java | 49 +++- .../xml/factory/attacks/AttackTestSupport.java | 292 ++++++++++++++------- .../xml/factory/attacks/BillionLaughsTest.java | 111 ++++++-- .../xml/factory/attacks/ExternalDtdTest.java | 96 +++++-- .../factory/attacks/ExternalGeneralEntityTest.java | 94 +++++-- .../attacks/ExternalParameterEntityTest.java | 97 +++++-- .../commons/xml/factory/attacks/Payloads.java | 8 +- src/test/resources/leaked/referenced.dtd | 2 + 9 files changed, 587 insertions(+), 179 deletions(-) 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 index 9010b51..992896f 100644 --- a/src/main/java/org/apache/commons/xml/factory/internal/AbstractXmlProvider.java +++ b/src/main/java/org/apache/commons/xml/factory/internal/AbstractXmlProvider.java @@ -141,6 +141,23 @@ static void setProperty(final SchemaFactory factory, final String property, fina throw fail(factory, "property", property, e); } } + + static void setProperty(final Validator validator, final String property, final Object value) { + try { + validator.setProperty(property, value); + } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { + throw fail(validator, "property", property, e); + } + } + + static void setProperty(final ValidatorHandler handler, final String property, final Object value) { + try { + handler.setProperty(property, value); + } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { + throw fail(handler, "property", property, e); + } + } + private final Set<String> supported; protected AbstractXmlProvider(final String... supportedClassNames) { diff --git a/src/main/java/org/apache/commons/xml/factory/internal/XercesProvider.java b/src/main/java/org/apache/commons/xml/factory/internal/XercesProvider.java index bcb8e97..81c88a5 100644 --- a/src/main/java/org/apache/commons/xml/factory/internal/XercesProvider.java +++ b/src/main/java/org/apache/commons/xml/factory/internal/XercesProvider.java @@ -80,16 +80,19 @@ private static final class HardeningSchema extends Schema { private final Schema delegate; private final LSResourceResolver resolver; + private final Object securityManager; - HardeningSchema(final Schema delegate, final LSResourceResolver resolver) { + HardeningSchema(final Schema delegate, final LSResourceResolver resolver, final Object securityManager) { this.delegate = delegate; this.resolver = resolver; + this.securityManager = securityManager; } @Override public Validator newValidator() { final Validator validator = delegate.newValidator(); setFeature(validator, XMLConstants.FEATURE_SECURE_PROCESSING, true); + setProperty(validator, XERCES_SECURITY_MANAGER_PROPERTY, securityManager); validator.setResourceResolver(resolver); return validator; } @@ -98,6 +101,7 @@ public Validator newValidator() { public ValidatorHandler newValidatorHandler() { final ValidatorHandler handler = delegate.newValidatorHandler(); setFeature(handler, XMLConstants.FEATURE_SECURE_PROCESSING, true); + setProperty(handler, XERCES_SECURITY_MANAGER_PROPERTY, securityManager); handler.setResourceResolver(resolver); return handler; } @@ -111,9 +115,11 @@ public ValidatorHandler newValidatorHandler() { private static final class HardeningSchemaFactory extends SchemaFactory { private final SchemaFactory delegate; + private final Object securityManager; - HardeningSchemaFactory(final SchemaFactory delegate) { + HardeningSchemaFactory(final SchemaFactory delegate, final Object securityManager) { this.delegate = delegate; + this.securityManager = securityManager; } @Override @@ -143,12 +149,12 @@ public boolean isSchemaLanguageSupported(final String schemaLanguage) { @Override public Schema newSchema() throws SAXException { - return new HardeningSchema(delegate.newSchema(), delegate.getResourceResolver()); + return new HardeningSchema(delegate.newSchema(), delegate.getResourceResolver(), securityManager); } @Override public Schema newSchema(final Source[] schemas) throws SAXException { - return new HardeningSchema(delegate.newSchema(schemas), delegate.getResourceResolver()); + return new HardeningSchema(delegate.newSchema(schemas), delegate.getResourceResolver(), securityManager); } @Override @@ -174,6 +180,29 @@ public void setResourceResolver(final LSResourceResolver resourceResolver) { private static final String FEATURE_DISALLOW_DOCTYPE = "http://apache.org/xml/features/disallow-doctype-decl"; + /** + * Xerces-specific factory/validator property whose value is an {@code org.apache.xerces.util.SecurityManager} instance carrying processing-limit + * thresholds (entity-expansion limit, max occur limit, and so on). + */ + private static final String XERCES_SECURITY_MANAGER_PROPERTY = "http://apache.org/xml/properties/security-manager"; + + /** + * Reads an integer JDK XML limit from the named system property, falling back to {@code defaultValue} if unset, blank, or unparsable. + */ + private static int jdkLimit(final String systemPropertyName, final int defaultValue) { + final String raw = System.getProperty(systemPropertyName); + if (raw == null || raw.isEmpty()) { + return defaultValue; + } + try { + return Integer.parseInt(raw.trim()); + } catch (final NumberFormatException e) { + return defaultValue; + } + } + + private final Object securityManager; + /** * Default constructor; invoked by {@link java.util.ServiceLoader} and the registry. */ @@ -181,6 +210,16 @@ public XercesProvider() { super("org.apache.xerces.jaxp.DocumentBuilderFactoryImpl", "org.apache.xerces.jaxp.SAXParserFactoryImpl", "org.apache.xerces.jaxp.validation.XMLSchemaFactory"); + Object sm; + try { + final Class<?> clazz = Class.forName("org.apache.xerces.util.SecurityManager"); + sm = clazz.getDeclaredConstructor().newInstance(); + clazz.getMethod("setEntityExpansionLimit", int.class).invoke(sm, jdkLimit("jdk.xml.entityExpansionLimit", 64000)); + clazz.getMethod("setMaxOccurNodeLimit", int.class).invoke(sm, jdkLimit("jdk.xml.maxOccurLimit", 5000)); + } catch (final ReflectiveOperationException e) { + sm = null; + } + this.securityManager = sm; } @Override @@ -209,6 +248,6 @@ public SchemaFactory configure(final SchemaFactory factory) { // Blocks xs:import/xs:include/xs:redefine resolution during schema compilation. The same resolver is captured at schema-creation time and re-installed // on every Validator/ValidatorHandler produced from the resulting Schema (see HardeningSchema), because Xerces does not propagate it automatically. factory.setResourceResolver(DenyAllResourceResolver.INSTANCE); - return new HardeningSchemaFactory(factory); + return new HardeningSchemaFactory(factory, securityManager); } } 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 fc5d3d9..a6f8410 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 @@ -16,26 +16,27 @@ */ package org.apache.commons.xml.factory.attacks; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; import java.io.StringReader; import java.io.StringWriter; import java.net.URL; -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.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.SAXParserFactory; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.SchemaFactory; import org.apache.commons.xml.factory.XmlFactories; import org.xml.sax.InputSource; @@ -45,88 +46,118 @@ /** * 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> + * <p>For every JAXP factory type the class exposes a paired set of helpers:</p> + * + * <ul> + * <li>{@code assert*Blocks(payload)} runs the payload through a hardened factory from {@link XmlFactories} and asserts the parse throws. Used by the + * {@code hardened*} side of test pairs to check that the hardening layer rejected the attack.</li> + * <li>{@code assert*Resolves(payload)} / {@code assert*Compiles(payload)} / similar run the payload through a default factory from + * {@link DocumentBuilderFactory#newInstance()} and friends, asserting the parse succeeds. Used by the {@code unconfigured*} side of test pairs as a + * positive control: it proves the payload is well-formed and the external resolution would happen without hardening.</li> + * </ul> + * + * <p>The two generic primitives {@link #assertParseFails(ThrowingAction, String)} and {@link #assertParseSucceeds(ThrowingAction, String)} are exposed for + * tests that need to compose a non-standard factory call.</p> */ final class AttackTestSupport { - /** Functional interface so test bodies can throw checked JAXP exceptions. */ + /** + * 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. + * URL form of the JDK's entity-expansion limit property. */ - 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"))); + private static final String JDK_ENTITY_EXPANSION_LIMIT = "http://www.oracle.com/xml/jaxp/properties/entityExpansionLimit"; + + static void assertDomBlocks(final String payload) { + assertParseFails(() -> XmlFactories.newDocumentBuilderFactory().newDocumentBuilder().parse(inputSource(payload)), "DOM"); + } + + static void assertDomResolves(final String payload) { + assertParseSucceeds(() -> { + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); + suppressException(() -> factory.setAttribute(JDK_ENTITY_EXPANSION_LIMIT, "0")); + factory.newDocumentBuilder().parse(inputSource(payload)); + }, "DOM"); + } /** - * Upper bound on elapsed time for any payload to be processed. + * Generic positive assertion for the file-based attack tests: runs the supplied parse action and asserts it throws. Used by the {@code hardened*} side of + * test pairs, where the wrapper deliberately requires the external resolution to succeed, so a throw means hardening prevented the resolution. * - * <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> + * @param action the parse to execute. + * @param description short label included in the failure message, for example {@code "DOM"} or {@code "validator"}. */ - private static final long MAX_MILLIS = 2_000L; + static void assertParseFails(final ThrowingAction action, final String description) { + assertThrows(Exception.class, action::run, "Hardening did not block " + description + "; parse completed successfully."); + } /** - * Asserts that the supplied action threw an exception quickly and the exception chain indicates the hardening layer (not a network/file attempt). + * Generic positive control for the file-based attack tests: runs the supplied parse action and asserts it does not throw. Used by the + * {@code unconfigured*} side of test pairs to prove the wrapper is well-formed and the external resolution would succeed without hardening. + * + * @param action the parse to execute. + * @param description short label included in the failure message, for example {@code "DOM"} or {@code "validator"}. */ - 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 assertParseSucceeds(final ThrowingAction action, final String description) { + assertDoesNotThrow(action::run, "Unconfigured factory should parse " + description + "; the wrapper or its external reference is broken."); } static void assertSaxBlocks(final String payload) { - assertAttackBlocked(() -> { + assertParseFails(() -> { final XMLReader reader = XmlFactories.newSAXParserFactory().newSAXParser().getXMLReader(); reader.setContentHandler(new DefaultHandler()); reader.setErrorHandler(new DefaultHandler()); reader.parse(inputSource(payload)); - }); + }, "SAX"); + } + + static void assertSaxResolves(final String payload) { + assertParseSucceeds(() -> { + final SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); + final XMLReader reader = factory.newSAXParser().getXMLReader(); + suppressException(() -> reader.setProperty(JDK_ENTITY_EXPANSION_LIMIT, "0")); + reader.setContentHandler(new DefaultHandler()); + reader.setErrorHandler(new DefaultHandler()); + reader.parse(inputSource(payload)); + }, "SAX"); } /** - * Asserts that compiling the given schema via {@code SchemaFactory.newSchema(Source)} is blocked by the hardening layer. + * Asserts that compiling the given schema via {@code SchemaFactory.newSchema(Source)} from {@link XmlFactories} throws. */ static void assertSchemaCompilationBlocks(final String xsd) { - assertAttackBlocked(() -> XmlFactories.newSchemaFactory().newSchema(source(xsd))); + assertParseFails(() -> XmlFactories.newSchemaFactory().newSchema(streamSource(xsd)), "Schema compile"); + } + + /** + * Asserts that compiling the given schema via a default {@link SchemaFactory#newInstance(String)} succeeds. Positive control for + * {@link #assertSchemaCompilationBlocks(String)}. + */ + static void assertSchemaCompilationSucceeds(final String xsd) { + assertParseSucceeds(() -> { + final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); + suppressException(() -> factory.setProperty(JDK_ENTITY_EXPANSION_LIMIT, "0")); + factory.newSchema(streamSource(xsd)); + }, "Schema compile"); } /** - * Asserts that a StAX parse of the supplied payload blocks DTD resolution. + * Asserts that a StAX parse of the supplied payload through a hardened factory from {@link XmlFactories} 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> + * <p>StAX with {@code SUPPORT_DTD=false} either throws on entity references in content 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(); @@ -134,58 +165,130 @@ static void assertStaxBlocks(final String payload) { 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(); - } + consumeStreamReader(factory, payload); + consumeEventReader(factory, payload); } 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)); + 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. + * Positive control for {@link #assertStaxBlocks(String)}: parses the payload through the default {@link XMLInputFactory#newInstance()} and asserts the + * parse does not throw. The default factory has {@code SUPPORT_DTD=true}, so the external DTD reference gets fetched and any entity references resolve. + */ + static void assertStaxResolves(final String payload) { + assertParseSucceeds(() -> { + final XMLInputFactory factory = XMLInputFactory.newInstance(); + suppressException(() -> factory.setProperty(XMLConstants.FEATURE_SECURE_PROCESSING, false)); + // URL form of the JDK property; JDK 8's XMLSecurityManager.getIndex only matches this form, JDK 11+ accepts both. + suppressException(() -> factory.setProperty(JDK_ENTITY_EXPANSION_LIMIT, "0")); + consumeStreamReader(factory, payload); + //consumeEventReader(factory, payload); + }, "StAX"); + } + + /** + * Asserts that compiling the given stylesheet via {@code TransformerFactory.newTransformer(Source)} from {@link XmlFactories} throws. */ static void assertStylesheetCompilationBlocks(final String xslt) { - assertAttackBlocked(() -> XmlFactories.newTransformerFactory().newTransformer(source(xslt))); + assertParseFails(() -> XmlFactories.newTransformerFactory().newTransformer(streamSource(xslt)), "stylesheet compile"); + } + + /** + * Asserts that compiling the given stylesheet via a default {@link TransformerFactory#newInstance()} succeeds. Positive control for + * {@link #assertStylesheetCompilationBlocks(String)}. + */ + static void assertStylesheetCompilationSucceeds(final String xslt) { + assertParseSucceeds(() -> { + final TransformerFactory factory = TransformerFactory.newInstance(); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); + factory.newTransformer(permissiveSaxSource(xslt)); + }, "stylesheet compile"); } static void assertTransformerBlocks(final String payload) { - assertAttackBlocked(() -> XmlFactories.newTransformerFactory().newTransformer().transform(source(payload), new StreamResult(new StringWriter()))); + assertParseFails(() -> XmlFactories.newTransformerFactory().newTransformer().transform(streamSource(payload), new StreamResult(new StringWriter())), + "transformer"); + } + + static void assertTransformerSucceeds(final String payload) { + assertParseSucceeds(() -> { + final TransformerFactory factory = TransformerFactory.newInstance(); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); + factory.newTransformer().transform(permissiveSaxSource(payload), new StreamResult(new StringWriter())); + }, "transformer"); } /** - * 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. + * Positive control for {@link #assertValidatorBlocks(String)}: validates the same payload through a Validator obtained from a default + * {@link SchemaFactory#newInstance(String)}, asserting it accepts the input. + */ + static void assertValidatorAccepts(final String xml) { + assertParseSucceeds(() -> { + final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); + suppressException(() -> factory.setProperty(JDK_ENTITY_EXPANSION_LIMIT, "0")); + factory.newSchema(streamSource(Payloads.BENIGN_SCHEMA)).newValidator().validate(streamSource(xml)); + }, "validator"); + } + + /** + * Asserts that validating the given XML instance through a {@link javax.xml.validation.Validator} obtained from {@link Payloads#BENIGN_SCHEMA} via the + * hardened factory throws. 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))); + assertParseFails(() -> XmlFactories.newSchemaFactory().newSchema(streamSource(Payloads.BENIGN_SCHEMA)).newValidator().validate(streamSource(xml)), + "validator"); + } + + private static void consumeEventReader(final XMLInputFactory factory, final String payload) throws Exception { + final XMLEventReader events = factory.createXMLEventReader(new StringReader(payload)); + try { + while (events.hasNext()) { + events.nextEvent(); + } + } finally { + events.close(); + } } - private static InputSource inputSource(final String xml) { + private static void consumeStreamReader(final XMLInputFactory factory, final String payload) throws Exception { + final XMLStreamReader stream = factory.createXMLStreamReader(new StringReader(payload)); + try { + while (stream.hasNext()) { + stream.next(); + } + } finally { + stream.close(); + } + } + + static InputSource inputSource(final String xml) { return new InputSource(new StringReader(xml)); } + /** + * Builds a {@link javax.xml.transform.sax.SAXSource} backed by a permissively configured {@link XMLReader}. Used by the unconfigured + * {@link #assertStylesheetCompilationSucceeds(String)} and {@link #assertTransformerSucceeds(String)} helpers because some {@link TransformerFactory} + * implementations (notably Saxon) do not propagate FSP-off or entity-limit overrides set on the factory through to the underlying SAX parser they create. + * Wrapping the source in a SAXSource forces the supplied reader to be used and bypasses that gap. + * + * <p>The reader's entity-expansion and size limits are set to {@code 0} (no limit) via the JDK's {@code jdk.xml.*} property names, which are tolerated if + * unrecognised by the underlying parser. Implementations that don't recognise them fall back to their own defaults; for the {@link BillionLaughsTest} + * payload, only the JDK's 64000 default actually fires below the 65536 expansion events the test produces, and removing it via these properties is what + * lets the unconfigured side complete.</p> + */ + private static javax.xml.transform.sax.SAXSource permissiveSaxSource(final String xml) + throws javax.xml.parsers.ParserConfigurationException, org.xml.sax.SAXException { + final SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + parserFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); + final XMLReader reader = parserFactory.newSAXParser().getXMLReader(); + suppressException(() -> reader.setProperty(JDK_ENTITY_EXPANSION_LIMIT, "0")); + return new javax.xml.transform.sax.SAXSource(reader, new InputSource(new StringReader(xml))); + } + /** * Returns the URL of a fixture under {@code src/test/resources/leaked/} on the test classpath. Fails the test if the resource is not present. * @@ -198,23 +301,16 @@ static URL resourceUrl(final String name) { return url; } - 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; + static StreamSource streamSource(final String xml) { + return new StreamSource(new StringReader(xml)); } - private static StreamSource source(final String xml) { - return new StreamSource(new StringReader(xml)); + private static void suppressException(ThrowingAction action) { + try { + action.run(); + } catch (final Exception e) { + // Ignore + } } 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 index 76e0bb1..15cde3f 100644 --- a/src/test/java/org/apache/commons/xml/factory/attacks/BillionLaughsTest.java +++ b/src/test/java/org/apache/commons/xml/factory/attacks/BillionLaughsTest.java @@ -18,56 +18,123 @@ import org.junit.jupiter.api.Test; +/** + * Checks whether parsers reject a Billion Laughs payload (nested entity expansion in the internal DTD subset). + * + * <p>The payload nests four levels of 16/15x expansion ({@code lol1} through {@code lol4}), so resolving {@code &lol4;} produces + * {@code 15 * 16 * 16 * 16 = 61440} leaf {@code &lol;} expansions and a total of {@code 1 + 15 + 240 + 3840 + 61440 = 65536} entity-expansion events. That is + * just over the JDK's default {@code jdk.xml.entityExpansionLimit = 64000}, so a parser with the limit enforced rejects the payload, and a parser with the + * limit disabled materialises only ~60 KB of {@code "A"} text and finishes immediately.</p> + * + * <p>Why a single character {@code "A"} and only 15x at the outer level: XSLTC compiles a stylesheet's expanded text into a JVM string constant, which is + * capped at 65535 bytes. A larger expansion makes {@code newTransformer(stylesheet)} fail with a misleading "GregorSamsa" stub-class error even when entity + * limits are disabled, so the payload is sized to stay just under that ceiling.</p> + * + * <p>Each parser type is exercised twice as a pair (unconfigured factory, expected to parse; hardened factory, expected to throw):</p> + * + * <ul> + * <li>The {@code hardened*} side runs the payload through {@link org.apache.commons.xml.factory.XmlFactories} and asserts the parse throws. Hardening blocks + * at whichever layer fires first (DOCTYPE-disallow on DOM/SAX, FSP plus propagated entity-expansion limits elsewhere).</li> + * <li>The {@code unconfigured*} side runs the payload through a default factory with {@code FEATURE_SECURE_PROCESSING=false} and asserts the parse succeeds. + * Disabling FSP turns off the JDK's entity-expansion limit, which is the only safety net stopping a default factory from materialising the expansion. + * The {@code unconfigured*} name is slightly euphemistic here: the factory is actively configured to be permissive, not "left alone".</li> + * </ul> + */ class BillionLaughsTest { - private static final String INSERTION = "&lol6;"; + private static final String INSERTION = "&lol4;"; 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" + + " <!ENTITY lol \"A\">\n" + + " <!ENTITY lol1 \"&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;\">\n" + + " <!ENTITY lol2 \"&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;\">\n" + + " <!ENTITY lol3 \"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;\">\n" + + " <!ENTITY lol4 \"&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;\">\n" + "]>\n" + body + "\n"; } + private static String xmlPayload() { + return withDoctype("root", Payloads.xmlBody(INSERTION)); + } + + private static String xsdPayload() { + return withDoctype("xs:schema", Payloads.xsdBody(INSERTION)); + } + + private static String xsltPayload() { + return withDoctype("xsl:stylesheet", Payloads.xsltBody(INSERTION)); + } + + @Test + void hardenedDomBlocksBillionLaughs() { + AttackTestSupport.assertDomBlocks(xmlPayload()); + } + + @Test + void hardenedSaxBlocksBillionLaughs() { + AttackTestSupport.assertSaxBlocks(xmlPayload()); + } + + @Test + void hardenedSchemaBlocksBillionLaughs() { + AttackTestSupport.assertSchemaCompilationBlocks(xsdPayload()); + } + + @Test + void hardenedStaxBlocksBillionLaughs() { + AttackTestSupport.assertStaxBlocks(xmlPayload()); + } + + @Test + void hardenedStylesheetBlocksBillionLaughs() { + AttackTestSupport.assertStylesheetCompilationBlocks(xsltPayload()); + } + + @Test + void hardenedTransformerBlocksBillionLaughs() { + AttackTestSupport.assertTransformerBlocks(xmlPayload()); + } + + @Test + void hardenedValidatorBlocksBillionLaughs() { + AttackTestSupport.assertValidatorBlocks(xmlPayload()); + } + @Test - void domBlocksBillionLaughs() { - AttackTestSupport.assertDomBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredDomResolvesBillionLaughs() { + AttackTestSupport.assertDomResolves(xmlPayload()); } @Test - void saxBlocksBillionLaughs() { - AttackTestSupport.assertSaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredSaxResolvesBillionLaughs() { + AttackTestSupport.assertSaxResolves(xmlPayload()); } @Test - void schemaFactoryBlocksBillionLaughs() { - AttackTestSupport.assertSchemaCompilationBlocks(withDoctype("xs:schema", Payloads.xsdBody(INSERTION))); + void unconfiguredSchemaCompilesBillionLaughs() { + AttackTestSupport.assertSchemaCompilationSucceeds(xsdPayload()); } @Test - void staxBlocksBillionLaughs() { - AttackTestSupport.assertStaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredStaxResolvesBillionLaughs() { + AttackTestSupport.assertStaxResolves(xmlPayload()); } @Test - void transformerBlocksBillionLaughs() { - AttackTestSupport.assertTransformerBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredStylesheetCompilesBillionLaughs() { + AttackTestSupport.assertStylesheetCompilationSucceeds(xsltPayload()); } @Test - void transformerStylesheetBlocksBillionLaughs() { - AttackTestSupport.assertStylesheetCompilationBlocks(withDoctype("xsl:stylesheet", Payloads.xsltBody(INSERTION))); + void unconfiguredTransformerSucceedsBillionLaughs() { + AttackTestSupport.assertTransformerSucceeds(xmlPayload()); } @Test - void validatorBlocksBillionLaughs() { - AttackTestSupport.assertValidatorBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredValidatorAcceptsBillionLaughs() { + AttackTestSupport.assertValidatorAccepts(xmlPayload()); } } 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 index db9965b..c817aea 100644 --- a/src/test/java/org/apache/commons/xml/factory/attacks/ExternalDtdTest.java +++ b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalDtdTest.java @@ -18,48 +18,112 @@ import org.junit.jupiter.api.Test; +/** + * Checks whether parsers can pull in an external DTD declared via {@code <!DOCTYPE root SYSTEM "...">}. + * + * <p>The wrapper points at {@code src/test/resources/leaked/referenced.dtd}, which declares a {@code leaked} entity. Each wrapper body references + * {@code &leaked;}, so the parse cannot succeed unless the DTD is actually fetched: a hardened parser refuses the fetch and {@code &leaked;} is undefined, + * which throws; an unconfigured parser fetches the DTD, the entity resolves, and the parse succeeds.</p> + * + * <p>Each parser type is exercised twice as a pair (unconfigured factory, expected to parse; hardened factory, expected to throw):</p> + * + * <ul> + * <li>DOM, SAX and StAX direct XML parsing.</li> + * <li>{@code SchemaFactory.newSchema(Source)} compilation of an XSD whose source has the DOCTYPE.</li> + * <li>{@link javax.xml.validation.Validator#validate(javax.xml.transform.Source)} of an instance whose source has the DOCTYPE.</li> + * <li>Identity {@code Transformer} reading the input XML.</li> + * <li>{@code TransformerFactory.newTransformer(Source)} compilation of a stylesheet whose source has the DOCTYPE.</li> + * </ul> + */ class ExternalDtdTest { - private static final String INSERTION = ""; + private static final String INSERTION = "&leaked;"; private static String withDoctype(final String rootQName, final String body) { return "<?xml version=\"1.0\"?>\n" - + "<!DOCTYPE " + rootQName + " SYSTEM \"" + Payloads.UNREACHABLE_HTTP + "\">\n" + + "<!DOCTYPE " + rootQName + " SYSTEM \"" + AttackTestSupport.resourceUrl("referenced.dtd") + "\">\n" + body + "\n"; } + private static String xmlPayload() { + return withDoctype("root", Payloads.xmlBody(INSERTION)); + } + + private static String xsdPayload() { + return withDoctype("xs:schema", Payloads.xsdBody(INSERTION)); + } + + private static String xsltPayload() { + return withDoctype("xsl:stylesheet", Payloads.xsltBody(INSERTION)); + } + + @Test + void hardenedDomBlocks() { + AttackTestSupport.assertDomBlocks(xmlPayload()); + } + + @Test + void hardenedSaxBlocks() { + AttackTestSupport.assertSaxBlocks(xmlPayload()); + } + + @Test + void hardenedSchemaBlocks() { + AttackTestSupport.assertSchemaCompilationBlocks(xsdPayload()); + } + + @Test + void hardenedStaxBlocks() { + AttackTestSupport.assertStaxBlocks(xmlPayload()); + } + + @Test + void hardenedStylesheetBlocks() { + AttackTestSupport.assertStylesheetCompilationBlocks(xsltPayload()); + } + + @Test + void hardenedTransformerBlocks() { + AttackTestSupport.assertTransformerBlocks(xmlPayload()); + } + + @Test + void hardenedValidatorBlocks() { + AttackTestSupport.assertValidatorBlocks(xmlPayload()); + } + @Test - void domBlocks() { - AttackTestSupport.assertDomBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredDomResolves() { + AttackTestSupport.assertDomResolves(xmlPayload()); } @Test - void saxBlocks() { - AttackTestSupport.assertSaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredSaxResolves() { + AttackTestSupport.assertSaxResolves(xmlPayload()); } @Test - void schemaFactoryBlocks() { - AttackTestSupport.assertSchemaCompilationBlocks(withDoctype("xs:schema", Payloads.xsdBody(INSERTION))); + void unconfiguredSchemaCompiles() { + AttackTestSupport.assertSchemaCompilationSucceeds(xsdPayload()); } @Test - void staxBlocks() { - AttackTestSupport.assertStaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredStaxResolves() { + AttackTestSupport.assertStaxResolves(xmlPayload()); } @Test - void transformerBlocks() { - AttackTestSupport.assertTransformerBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredStylesheetCompiles() { + AttackTestSupport.assertStylesheetCompilationSucceeds(xsltPayload()); } @Test - void transformerStylesheetBlocks() { - AttackTestSupport.assertStylesheetCompilationBlocks(withDoctype("xsl:stylesheet", Payloads.xsltBody(INSERTION))); + void unconfiguredTransformerSucceeds() { + AttackTestSupport.assertTransformerSucceeds(xmlPayload()); } @Test - void validatorBlocks() { - AttackTestSupport.assertValidatorBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredValidatorAccepts() { + AttackTestSupport.assertValidatorAccepts(xmlPayload()); } } 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 index d0cacf9..8047bba 100644 --- a/src/test/java/org/apache/commons/xml/factory/attacks/ExternalGeneralEntityTest.java +++ b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalGeneralEntityTest.java @@ -18,6 +18,23 @@ import org.junit.jupiter.api.Test; +/** + * Checks whether parsers can pull in an external general entity declared inline in the internal subset. + * + * <p>The wrapper declares {@code <!ENTITY xxe SYSTEM "file:.../referenced.txt">} and uses {@code &xxe;} in the body. The general entity expands to the + * content of {@code src/test/resources/leaked/referenced.txt} when the external reference is resolved. A hardened parser refuses the fetch, {@code &xxe;} is + * undefined, and the parse throws; an unconfigured parser fetches the file, the entity resolves, and the parse succeeds.</p> + * + * <p>Each parser type is exercised twice as a pair (unconfigured factory, expected to parse; hardened factory, expected to throw):</p> + * + * <ul> + * <li>DOM, SAX and StAX direct XML parsing.</li> + * <li>{@code SchemaFactory.newSchema(Source)} compilation of an XSD whose source has the entity-bearing DOCTYPE.</li> + * <li>{@link javax.xml.validation.Validator#validate(javax.xml.transform.Source)} of an instance whose source has the entity-bearing DOCTYPE.</li> + * <li>Identity {@code Transformer} reading the input XML.</li> + * <li>{@code TransformerFactory.newTransformer(Source)} compilation of a stylesheet whose source has the entity-bearing DOCTYPE.</li> + * </ul> + */ class ExternalGeneralEntityTest { private static final String INSERTION = "&xxe;"; @@ -25,43 +42,90 @@ class ExternalGeneralEntityTest { 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" + + " <!ENTITY xxe SYSTEM \"" + AttackTestSupport.resourceUrl("referenced.txt") + "\">\n" + "]>\n" + body + "\n"; } + private static String xmlPayload() { + return withDoctype("root", Payloads.xmlBody(INSERTION)); + } + + private static String xsdPayload() { + return withDoctype("xs:schema", Payloads.xsdBody(INSERTION)); + } + + private static String xsltPayload() { + return withDoctype("xsl:stylesheet", Payloads.xsltBody(INSERTION)); + } + + @Test + void hardenedDomBlocks() { + AttackTestSupport.assertDomBlocks(xmlPayload()); + } + + @Test + void hardenedSaxBlocks() { + AttackTestSupport.assertSaxBlocks(xmlPayload()); + } + + @Test + void hardenedSchemaBlocks() { + AttackTestSupport.assertSchemaCompilationBlocks(xsdPayload()); + } + + @Test + void hardenedStaxBlocks() { + AttackTestSupport.assertStaxBlocks(xmlPayload()); + } + + @Test + void hardenedStylesheetBlocks() { + AttackTestSupport.assertStylesheetCompilationBlocks(xsltPayload()); + } + + @Test + void hardenedTransformerBlocks() { + AttackTestSupport.assertTransformerBlocks(xmlPayload()); + } + + @Test + void hardenedValidatorBlocks() { + AttackTestSupport.assertValidatorBlocks(xmlPayload()); + } + @Test - void domBlocks() { - AttackTestSupport.assertDomBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredDomResolves() { + AttackTestSupport.assertDomResolves(xmlPayload()); } @Test - void saxBlocks() { - AttackTestSupport.assertSaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredSaxResolves() { + AttackTestSupport.assertSaxResolves(xmlPayload()); } @Test - void schemaFactoryBlocks() { - AttackTestSupport.assertSchemaCompilationBlocks(withDoctype("xs:schema", Payloads.xsdBody(INSERTION))); + void unconfiguredSchemaCompiles() { + AttackTestSupport.assertSchemaCompilationSucceeds(xsdPayload()); } @Test - void staxBlocks() { - AttackTestSupport.assertStaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredStaxResolves() { + AttackTestSupport.assertStaxResolves(xmlPayload()); } @Test - void transformerBlocks() { - AttackTestSupport.assertTransformerBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredStylesheetCompiles() { + AttackTestSupport.assertStylesheetCompilationSucceeds(xsltPayload()); } @Test - void transformerStylesheetBlocks() { - AttackTestSupport.assertStylesheetCompilationBlocks(withDoctype("xsl:stylesheet", Payloads.xsltBody(INSERTION))); + void unconfiguredTransformerSucceeds() { + AttackTestSupport.assertTransformerSucceeds(xmlPayload()); } @Test - void validatorBlocks() { - AttackTestSupport.assertValidatorBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredValidatorAccepts() { + AttackTestSupport.assertValidatorAccepts(xmlPayload()); } } 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 index b1f853c..d1ae0f1 100644 --- a/src/test/java/org/apache/commons/xml/factory/attacks/ExternalParameterEntityTest.java +++ b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalParameterEntityTest.java @@ -18,51 +18,116 @@ import org.junit.jupiter.api.Test; +/** + * Checks whether parsers can pull in an external DTD via a parameter-entity reference inside the internal subset. + * + * <p>The wrapper declares a parameter entity {@code %xxe;} pointing at {@code src/test/resources/leaked/referenced.dtd} and immediately references it in the + * internal subset; once expanded, the entity declarations from {@code referenced.dtd} (in particular {@code <!ENTITY leaked "...">}) become part of the + * document's DTD. Each wrapper body then references {@code &leaked;}, so the parse cannot succeed unless the parameter-entity expansion is allowed: a hardened + * parser refuses, {@code &leaked;} is undefined, and the parse throws; an unconfigured parser fetches and resolves, and the parse succeeds.</p> + * + * <p>Each parser type is exercised twice as a pair (unconfigured factory, expected to parse; hardened factory, expected to throw):</p> + * + * <ul> + * <li>DOM, SAX and StAX direct XML parsing.</li> + * <li>{@code SchemaFactory.newSchema(Source)} compilation of an XSD whose source has the parameter-entity DOCTYPE.</li> + * <li>{@link javax.xml.validation.Validator#validate(javax.xml.transform.Source)} of an instance whose source has the parameter-entity DOCTYPE.</li> + * <li>Identity {@code Transformer} reading the input XML.</li> + * <li>{@code TransformerFactory.newTransformer(Source)} compilation of a stylesheet whose source has the parameter-entity DOCTYPE.</li> + * </ul> + */ class ExternalParameterEntityTest { - private static final String INSERTION = ""; + private static final String INSERTION = "&leaked;"; 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" + + " <!ENTITY % xxe SYSTEM \"" + AttackTestSupport.resourceUrl("referenced.dtd") + "\">\n" + " %xxe;\n" + "]>\n" + body + "\n"; } + private static String xmlPayload() { + return withDoctype("root", Payloads.xmlBody(INSERTION)); + } + + private static String xsdPayload() { + return withDoctype("xs:schema", Payloads.xsdBody(INSERTION)); + } + + private static String xsltPayload() { + return withDoctype("xsl:stylesheet", Payloads.xsltBody(INSERTION)); + } + + @Test + void hardenedDomBlocks() { + AttackTestSupport.assertDomBlocks(xmlPayload()); + } + + @Test + void hardenedSaxBlocks() { + AttackTestSupport.assertSaxBlocks(xmlPayload()); + } + + @Test + void hardenedSchemaBlocks() { + AttackTestSupport.assertSchemaCompilationBlocks(xsdPayload()); + } + + @Test + void hardenedStaxBlocks() { + AttackTestSupport.assertStaxBlocks(xmlPayload()); + } + + @Test + void hardenedStylesheetBlocks() { + AttackTestSupport.assertStylesheetCompilationBlocks(xsltPayload()); + } + + @Test + void hardenedTransformerBlocks() { + AttackTestSupport.assertTransformerBlocks(xmlPayload()); + } + + @Test + void hardenedValidatorBlocks() { + AttackTestSupport.assertValidatorBlocks(xmlPayload()); + } + @Test - void domBlocks() { - AttackTestSupport.assertDomBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredDomResolves() { + AttackTestSupport.assertDomResolves(xmlPayload()); } @Test - void saxBlocks() { - AttackTestSupport.assertSaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredSaxResolves() { + AttackTestSupport.assertSaxResolves(xmlPayload()); } @Test - void schemaFactoryBlocks() { - AttackTestSupport.assertSchemaCompilationBlocks(withDoctype("xs:schema", Payloads.xsdBody(INSERTION))); + void unconfiguredSchemaCompiles() { + AttackTestSupport.assertSchemaCompilationSucceeds(xsdPayload()); } @Test - void staxBlocks() { - AttackTestSupport.assertStaxBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredStaxResolves() { + AttackTestSupport.assertStaxResolves(xmlPayload()); } @Test - void transformerBlocks() { - AttackTestSupport.assertTransformerBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredStylesheetCompiles() { + AttackTestSupport.assertStylesheetCompilationSucceeds(xsltPayload()); } @Test - void transformerStylesheetBlocks() { - AttackTestSupport.assertStylesheetCompilationBlocks(withDoctype("xsl:stylesheet", Payloads.xsltBody(INSERTION))); + void unconfiguredTransformerSucceeds() { + AttackTestSupport.assertTransformerSucceeds(xmlPayload()); } @Test - void validatorBlocks() { - AttackTestSupport.assertValidatorBlocks(withDoctype("root", Payloads.xmlBody(INSERTION))); + void unconfiguredValidatorAccepts() { + AttackTestSupport.assertValidatorAccepts(xmlPayload()); } } 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 index a16a01a..5e6db70 100644 --- a/src/test/java/org/apache/commons/xml/factory/attacks/Payloads.java +++ b/src/test/java/org/apache/commons/xml/factory/attacks/Payloads.java @@ -17,7 +17,7 @@ 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. + * Shared building blocks for attack-test payloads: a benign reference schema 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> */ @@ -36,12 +36,6 @@ final class Payloads { + " </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() { } diff --git a/src/test/resources/leaked/referenced.dtd b/src/test/resources/leaked/referenced.dtd new file mode 100644 index 0000000..d464887 --- /dev/null +++ b/src/test/resources/leaked/referenced.dtd @@ -0,0 +1,2 @@ +<!-- SPDX-License-Identifier: Apache-2.0 --> +<!ENTITY leaked "All your base are belong to us">
