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 9d485e3ca3412002219134ee2be5d4ba1b101ebf Author: Piotr P. Karwasz <[email protected]> AuthorDate: Wed Apr 29 21:37:27 2026 +0200 Add Android tests and rationalise test suite (#10) CI gains a `build-android` job that runs the existing instrumented suite on API 33 and 35 emulators alongside the JVM matrix. Getting it green required pinning down what hardening guarantees parsers offer regardless of which JAXP surface a payload reaches them through. After this change a payload behaves the same whether it is parsed as an XML document, an XSD schema or an XSLT stylesheet: - A `DOCTYPE` that names an external subset parses without throwing because the subset is silently skipped. - An undeclared general entity reference is silently dropped per XML 1.0 §4.1. - An external entity reference is rejected through whatever channel the underlying parser exposes, with one exception: on Android Expat the JNI bridge does not enable libexpat's `XML_SetParamEntityParsing`, so external parameter entities are silently skipped without consulting the resolver; the affected test cases are gated on a probe that detects this. Making the contract uniform required wrapping the JDK's `TransformerFactory` and `SchemaFactory` so the SAX reader they use internally is the one `XmlFactories.newSAXParserFactory()` hands out. Their public API does not propagate the features that matter (`load-external-dtd` in particular) down to the SAX layer, so without the wrap the inner reader runs with its own defaults. Routing every `Source` through the same hardened reader also drops the Apache-Xalan and Saxon dependency on `X [...] Assisted-By: Claude Opus 4.7 (1M context) <[email protected]> --- .github/workflows/maven.yml | 103 +++- android-tests/build.gradle.kts | 4 +- .../commons/xml/factory/AndroidProvider.java | 98 ++-- .../commons/xml/factory/DelegatingSAXParser.java | 87 ++++ .../commons/xml/factory/DelegatingXMLReader.java | 111 +++++ .../commons/xml/factory/HardeningSchema.java | 51 ++ .../xml/factory/HardeningSchemaFactory.java | 79 +++ .../commons/xml/factory/HardeningTemplates.java | 52 ++ .../commons/xml/factory/HardeningTransformer.java | 43 ++ .../xml/factory/HardeningTransformerFactory.java | 89 ++++ .../commons/xml/factory/HardeningValidator.java | 97 ++++ .../apache/commons/xml/factory/SaxonProvider.java | 3 + .../commons/xml/factory/StockJdkProvider.java | 23 +- .../apache/commons/xml/factory/XalanProvider.java | 144 +----- .../apache/commons/xml/factory/XercesProvider.java | 93 +--- .../apache/commons/xml/factory/XmlFactories.java | 30 ++ .../commons/xml/factory/AttackTestSupport.java | 542 ++++++++++++++------- .../commons/xml/factory/BillionLaughsTest.java | 154 +++--- .../commons/xml/factory/DoctypeOnlyTest.java | 3 + .../commons/xml/factory/ExternalDtdTest.java | 36 +- .../xml/factory/ExternalGeneralEntityTest.java | 24 +- .../xml/factory/ExternalParameterEntityTest.java | 32 +- .../commons/xml/factory/SchemaImportTest.java | 2 +- .../commons/xml/factory/SchemaIncludeTest.java | 2 +- .../commons/xml/factory/SchemaRedefineTest.java | 2 +- 25 files changed, 1366 insertions(+), 538 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 8115045..f757817 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -82,7 +82,9 @@ jobs: - name: Set up Maven cache uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae #v5.0.5 with: - path: ~/.m2/repository + path: | + ~/.m2/repository + ~/.gradle/wrapper key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-maven- @@ -97,4 +99,103 @@ jobs: name: surefire-reports-${{ matrix.os }}-jdk${{ matrix.java-version }} path: '**/target/surefire-reports/' if-no-files-found: ignore + retention-days: 14 + + build-android: + + # API 33 mirrors the Pixel 6a AVD baseline configured in android-tests/build.gradle.kts; API 35 covers the latest stable platform. + # Linux runners only: GitHub-hosted macOS / Windows runners no longer ship KVM / HAXM acceleration, and arm64 / x86 emulator images + # without acceleration boot too slowly to be useful in CI. + name: build (android, API ${{ matrix.api-level }}) + runs-on: ubuntu-latest + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + api-level: [ 33, 35 ] + + steps: + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up JDK 17 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: temurin + java-version: 17 + + # We only restore, since the JDK build's cache will be more comprehensive + - name: Set up Maven cache + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Build library JAR + run: mvn --errors --show-version --batch-mode --no-transfer-progress -DskipTests package + + - name: AVD cache + id: avd-cache + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + ~/.android/avd + ~/.android/adb* + key: avd-${{ matrix.api-level }} + + # Only runs on a cache miss to create an AVD snapshot and speed up the next builds + - name: Generate AVD snapshot + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0 + with: + api-level: ${{ matrix.api-level }} + target: default + arch: x86_64 + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: false + script: echo "Generated AVD snapshot" + + - name: Gradle cache + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('android-tests/*.gradle.kts', 'android-tests/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Run instrumented tests + uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0 + with: + api-level: ${{ matrix.api-level }} + target: default + arch: x86_64 + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + working-directory: android-tests + script: ./gradlew connectedDebugAndroidTest + + - name: Upload Android test reports + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: android-test-reports-api${{ matrix.api-level }} + path: | + android-tests/build/outputs/androidTest-results/ + android-tests/build/reports/androidTests/ + if-no-files-found: ignore retention-days: 14 \ No newline at end of file diff --git a/android-tests/build.gradle.kts b/android-tests/build.gradle.kts index 18a7c15..94acbbf 100644 --- a/android-tests/build.gradle.kts +++ b/android-tests/build.gradle.kts @@ -18,7 +18,7 @@ import com.android.build.api.dsl.ManagedVirtualDevice plugins { id("com.android.library") version "8.6.1" - id("de.mannodermaus.android-junit") version "2.0.1" + id("de.mannodermaus.android-junit5") version "1.14.0.0" } val libraryVersion = "0.1.0-SNAPSHOT" @@ -66,7 +66,7 @@ android { junitPlatform { filters { // Pass single tag expression - includeTags("dom | sax | schema") + includeTags("dom | sax | schema | trax") } } diff --git a/src/main/java/org/apache/commons/xml/factory/AndroidProvider.java b/src/main/java/org/apache/commons/xml/factory/AndroidProvider.java index f2863ae..dd46fc2 100644 --- a/src/main/java/org/apache/commons/xml/factory/AndroidProvider.java +++ b/src/main/java/org/apache/commons/xml/factory/AndroidProvider.java @@ -22,12 +22,16 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; import org.xml.sax.XMLReader; +import org.xml.sax.ext.DefaultHandler2; import org.xml.sax.ext.LexicalHandler; /** @@ -72,37 +76,22 @@ final class AndroidProvider { * * <p>Merely <em>declaring</em> an external subset does not cause the parse to throw.</p> */ - private static final class DtdAwareDenyResolver implements EntityResolver, LexicalHandler { - - private boolean inDtd; - private String dtdPublicId; - private String dtdSystemId; + private static final class DtdAwareDenyResolver extends DefaultHandler2 { private static String forbiddenMessage(final String publicId, final String systemId) { return String.format("External Entity: failed to read external entity (publicId='%s', systemId='%s'); external entity access is denied.", publicId, systemId); } - @Override - public void comment(final char[] ch, final int start, final int length) { - // no-op - } - - @Override - public void endCDATA() { - // no-op - } + private String dtdPublicId; + private String dtdSystemId; + private boolean inDtd; @Override public void endDTD() { inDtd = false; } - @Override - public void endEntity(final String name) { - // no-op - } - @Override public InputSource resolveEntity(final String publicId, final String systemId) throws SAXException { if (inDtd && Objects.equals(publicId, dtdPublicId) && Objects.equals(systemId, dtdSystemId)) { @@ -111,35 +100,88 @@ public InputSource resolveEntity(final String publicId, final String systemId) t throw new SAXException(forbiddenMessage(publicId, systemId)); } - @Override - public void startCDATA() { - // no-op - } - @Override public void startDTD(final String name, final String publicId, final String systemId) { inDtd = true; dtdPublicId = publicId; dtdSystemId = systemId; } + } + + /** {@link SAXParser} wrapper whose {@link #getXMLReader()} returns a {@link GuardedXMLReader}. */ + private static final class GuardedSAXParser extends DelegatingSAXParser { + + private final XMLReader guardedReader; + + GuardedSAXParser(final SAXParser delegate, final XMLReader guardedReader) { + super(delegate); + this.guardedReader = guardedReader; + } @Override - public void startEntity(final String name) { - // no-op + public XMLReader getXMLReader() { + return guardedReader; } } + /** {@link SAXParserFactory} wrapper that produces {@link GuardedSAXParser}s. */ + private static final class GuardedSAXParserFactory extends DelegatingSAXParserFactory { + + GuardedSAXParserFactory(final SAXParserFactory delegate) { + super(delegate); + } + + @Override + public SAXParser newSAXParser() throws ParserConfigurationException, SAXException { + final SAXParser parser = super.newSAXParser(); + return new GuardedSAXParser(parser, configure(parser.getXMLReader())); + } + } + + /** + * {@link XMLReader} wrapper that surfaces ExpatReader's two-feature conflict at {@code setFeature} time instead of {@code parse} time. + * + * <p>Android's {@code ExpatReader.parse()} throws {@link SAXNotSupportedException} when {@code namespaces} and {@code namespace-prefixes} are both + * enabled; on Android 33+ {@code namespaces} defaults to {@code true}, so any caller (notably Apache Xalan's identity transformer) that flips + * {@code namespace-prefixes} on triggers the throw at parse time. This wrapper relocates the throw to the offending {@code setFeature} call, where + * Apache Xalan's {@code try { ... } catch (SAXException) { /* We don't care. *<!-- -->/ }} swallows it cleanly.</p> + */ + static final class GuardedXMLReader extends DelegatingXMLReader { + + GuardedXMLReader(final XMLReader delegate) { + super(delegate); + } + + @Override + public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { + if (value + && (NAMESPACE_PREFIXES_FEATURE.equals(name) && super.getFeature(NAMESPACES_FEATURE) + || NAMESPACES_FEATURE.equals(name) && super.getFeature(NAMESPACE_PREFIXES_FEATURE))) { + throw new SAXNotSupportedException( + "ExpatReader cannot have both '" + NAMESPACES_FEATURE + + "' and '" + NAMESPACE_PREFIXES_FEATURE + "' enabled simultaneously"); + } + super.setFeature(name, value); + } + } private static final String LEXICAL_HANDLER_PROPERTY = "http://xml.org/sax/properties/lexical-handler"; + private static final String NAMESPACES_FEATURE = "http://xml.org/sax/features/namespaces"; + + private static final String NAMESPACE_PREFIXES_FEATURE = "http://xml.org/sax/features/namespace-prefixes"; + static DocumentBuilderFactory configure(final DocumentBuilderFactory factory) { return factory; } static SAXParserFactory configure(final SAXParserFactory factory) { - return new HardeningSAXParserFactory(factory, AndroidProvider::configure); + return new GuardedSAXParserFactory(factory); } static XMLReader configure(final XMLReader reader) { + if (reader instanceof GuardedXMLReader) { + return reader; + } final DtdAwareDenyResolver resolver = new DtdAwareDenyResolver(); reader.setEntityResolver(resolver); try { @@ -147,7 +189,7 @@ static XMLReader configure(final XMLReader reader) { } catch (final SAXException e) { // ExpatReader recognises the lexical-handler property; if a future replacement does not, fall through and lose subset-vs-entity discrimination. } - return reader; + return new GuardedXMLReader(reader); } private AndroidProvider() { diff --git a/src/main/java/org/apache/commons/xml/factory/DelegatingSAXParser.java b/src/main/java/org/apache/commons/xml/factory/DelegatingSAXParser.java new file mode 100644 index 0000000..c31ecb1 --- /dev/null +++ b/src/main/java/org/apache/commons/xml/factory/DelegatingSAXParser.java @@ -0,0 +1,87 @@ +/* + * 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 javax.xml.parsers.SAXParser; +import javax.xml.validation.Schema; + +import org.xml.sax.Parser; +import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; +import org.xml.sax.XMLReader; + +/** + * {@link SAXParser} that forwards every method to a wrapped delegate. + * + * <p>The non-abstract {@code parse(...)} overloads inherited from {@link SAXParser} call {@code this.getXMLReader()} virtually, so a subclass that only + * overrides {@link #getXMLReader()} can redirect every parse path through a different reader without having to override the overloads.</p> + */ +class DelegatingSAXParser extends SAXParser { + + private final SAXParser delegate; + + DelegatingSAXParser(final SAXParser delegate) { + this.delegate = delegate; + } + + @Override + @SuppressWarnings("deprecation") + public Parser getParser() throws SAXException { + return delegate.getParser(); + } + + @Override + public Object getProperty(final String name) throws SAXNotRecognizedException, SAXNotSupportedException { + return delegate.getProperty(name); + } + + @Override + public Schema getSchema() { + return delegate.getSchema(); + } + + @Override + public XMLReader getXMLReader() throws SAXException { + return delegate.getXMLReader(); + } + + @Override + public boolean isNamespaceAware() { + return delegate.isNamespaceAware(); + } + + @Override + public boolean isValidating() { + return delegate.isValidating(); + } + + @Override + public boolean isXIncludeAware() { + return delegate.isXIncludeAware(); + } + + @Override + public void reset() { + delegate.reset(); + } + + @Override + public void setProperty(final String name, final Object value) throws SAXNotRecognizedException, SAXNotSupportedException { + delegate.setProperty(name, value); + } +} diff --git a/src/main/java/org/apache/commons/xml/factory/DelegatingXMLReader.java b/src/main/java/org/apache/commons/xml/factory/DelegatingXMLReader.java new file mode 100644 index 0000000..5e6e0f2 --- /dev/null +++ b/src/main/java/org/apache/commons/xml/factory/DelegatingXMLReader.java @@ -0,0 +1,111 @@ +/* + * 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 java.io.IOException; + +import org.xml.sax.ContentHandler; +import org.xml.sax.DTDHandler; +import org.xml.sax.EntityResolver; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; +import org.xml.sax.XMLReader; + +/** + * {@link XMLReader} that forwards every method to a wrapped delegate. + */ +class DelegatingXMLReader implements XMLReader { + + private final XMLReader delegate; + + DelegatingXMLReader(final XMLReader delegate) { + this.delegate = delegate; + } + + @Override + public ContentHandler getContentHandler() { + return delegate.getContentHandler(); + } + + @Override + public DTDHandler getDTDHandler() { + return delegate.getDTDHandler(); + } + + @Override + public EntityResolver getEntityResolver() { + return delegate.getEntityResolver(); + } + + @Override + public ErrorHandler getErrorHandler() { + return delegate.getErrorHandler(); + } + + @Override + public boolean getFeature(final String name) throws SAXNotRecognizedException, SAXNotSupportedException { + return delegate.getFeature(name); + } + + @Override + public Object getProperty(final String name) throws SAXNotRecognizedException, SAXNotSupportedException { + return delegate.getProperty(name); + } + + @Override + public void parse(final InputSource input) throws IOException, SAXException { + delegate.parse(input); + } + + @Override + public void parse(final String systemId) throws IOException, SAXException { + delegate.parse(systemId); + } + + @Override + public void setContentHandler(final ContentHandler handler) { + delegate.setContentHandler(handler); + } + + @Override + public void setDTDHandler(final DTDHandler handler) { + delegate.setDTDHandler(handler); + } + + @Override + public void setEntityResolver(final EntityResolver resolver) { + delegate.setEntityResolver(resolver); + } + + @Override + public void setErrorHandler(final ErrorHandler handler) { + delegate.setErrorHandler(handler); + } + + @Override + public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { + delegate.setFeature(name, value); + } + + @Override + public void setProperty(final String name, final Object value) throws SAXNotRecognizedException, SAXNotSupportedException { + delegate.setProperty(name, value); + } +} diff --git a/src/main/java/org/apache/commons/xml/factory/HardeningSchema.java b/src/main/java/org/apache/commons/xml/factory/HardeningSchema.java new file mode 100644 index 0000000..2655d78 --- /dev/null +++ b/src/main/java/org/apache/commons/xml/factory/HardeningSchema.java @@ -0,0 +1,51 @@ +/* + * 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 java.util.function.UnaryOperator; + +import javax.xml.validation.Schema; +import javax.xml.validation.Validator; +import javax.xml.validation.ValidatorHandler; + +/** + * {@link Schema} wrapper that applies provider-specific decoration to every {@link Validator} and {@link ValidatorHandler} the inner Schema produces, then + * wraps each {@link Validator} in {@link HardeningValidator} so {@link Validator#validate(javax.xml.transform.Source)} runs through + * {@link XmlFactories#harden(javax.xml.transform.Source)}. + */ +final class HardeningSchema extends Schema { + + private final Schema delegate; + private final UnaryOperator<Validator> validatorHardener; + private final UnaryOperator<ValidatorHandler> handlerHardener; + + HardeningSchema(final Schema delegate, final UnaryOperator<Validator> validatorHardener, final UnaryOperator<ValidatorHandler> handlerHardener) { + this.delegate = delegate; + this.validatorHardener = validatorHardener; + this.handlerHardener = handlerHardener; + } + + @Override + public Validator newValidator() { + return new HardeningValidator(validatorHardener.apply(delegate.newValidator())); + } + + @Override + public ValidatorHandler newValidatorHandler() { + return handlerHardener.apply(delegate.newValidatorHandler()); + } +} diff --git a/src/main/java/org/apache/commons/xml/factory/HardeningSchemaFactory.java b/src/main/java/org/apache/commons/xml/factory/HardeningSchemaFactory.java new file mode 100644 index 0000000..eeaa15d --- /dev/null +++ b/src/main/java/org/apache/commons/xml/factory/HardeningSchemaFactory.java @@ -0,0 +1,79 @@ +/* + * 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 java.util.function.UnaryOperator; + +import javax.xml.transform.Source; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; +import javax.xml.validation.ValidatorHandler; + +import org.xml.sax.SAXException; + +/** + * {@link SchemaFactory} wrapper that rewrites every Source-taking entry point through {@link XmlFactories#harden(Source)} and applies provider-specific + * decoration to every {@link Validator} and {@link ValidatorHandler} that the produced {@link Schema} hands out. + * + * <p>Three layers cooperate:</p> + * <ol> + * <li>{@link HardeningSchemaFactory} rewrites the Source on every {@code newSchema(Source[])} entry point.</li> + * <li>{@link HardeningSchema} applies the provider-specific hardener to every Validator/ValidatorHandler the inner Schema produces (e.g. Xerces re-installs + * its {@code LSResourceResolver} and {@code SecurityManager} since it does not propagate them through Schema).</li> + * <li>{@link HardeningValidator} rewrites the Source on every {@link Validator#validate(Source)} call.</li> + * </ol> + */ +final class HardeningSchemaFactory extends DelegatingSchemaFactory { + + private final UnaryOperator<Validator> validatorHardener; + private final UnaryOperator<ValidatorHandler> handlerHardener; + + HardeningSchemaFactory(final SchemaFactory delegate) { + this(delegate, UnaryOperator.identity(), UnaryOperator.identity()); + } + + HardeningSchemaFactory(final SchemaFactory delegate, final UnaryOperator<Validator> validatorHardener, + final UnaryOperator<ValidatorHandler> handlerHardener) { + super(delegate); + this.validatorHardener = validatorHardener; + this.handlerHardener = handlerHardener; + } + + @Override + public Schema newSchema() throws SAXException { + return new HardeningSchema(super.newSchema(), validatorHardener, handlerHardener); + } + + @Override + public Schema newSchema(final Source[] schemas) throws SAXException { + return new HardeningSchema(super.newSchema(harden(schemas)), validatorHardener, handlerHardener); + } + + private static Source[] harden(final Source[] schemas) throws SAXException { + final Source[] hardened = new Source[schemas.length]; + try { + for (int i = 0; i < schemas.length; i++) { + hardened[i] = XmlFactories.harden(schemas[i]); + } + } catch (final TransformerConfigurationException e) { + throw new SAXException("Failed to harden schema source", e); + } + return hardened; + } +} diff --git a/src/main/java/org/apache/commons/xml/factory/HardeningTemplates.java b/src/main/java/org/apache/commons/xml/factory/HardeningTemplates.java new file mode 100644 index 0000000..09e65f5 --- /dev/null +++ b/src/main/java/org/apache/commons/xml/factory/HardeningTemplates.java @@ -0,0 +1,52 @@ +/* + * 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 javax.xml.transform.Templates; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.URIResolver; + +/** + * {@link Templates} wrapper whose only purpose is to return a {@link HardeningTransformer} from {@link Templates#newTransformer()}, with the factory's + * compile-time {@link URIResolver} pre-installed. + * + * <p>Both Apache Xalan 2.7 and stock-JDK XSLTC fail to propagate the factory's URIResolver through {@code Templates.newTransformer()}: the produced runtime + * Transformer has a null URIResolver unless the caller sets one, leaving runtime {@code document()} calls unguarded. Snapshotting the resolver at compile time + * and restoring it onto the runtime Transformer matches the JAXP-conformant intuition that the factory's resolver is the default for any Transformer the + * factory ultimately produces.</p> + */ +final class HardeningTemplates extends DelegatingTemplates { + + /** Compile-time URIResolver snapshot; the underlying impl does not propagate the factory's resolver onto Transformers obtained from Templates. */ + private final URIResolver uriResolver; + + HardeningTemplates(final Templates delegate, final URIResolver uriResolver) { + super(delegate); + this.uriResolver = uriResolver; + } + + @Override + public Transformer newTransformer() throws TransformerConfigurationException { + final Transformer transformer = super.newTransformer(); + if (transformer == null) { + return null; + } + transformer.setURIResolver(uriResolver); + return new HardeningTransformer(transformer); + } +} diff --git a/src/main/java/org/apache/commons/xml/factory/HardeningTransformer.java b/src/main/java/org/apache/commons/xml/factory/HardeningTransformer.java new file mode 100644 index 0000000..12a4a67 --- /dev/null +++ b/src/main/java/org/apache/commons/xml/factory/HardeningTransformer.java @@ -0,0 +1,43 @@ +/* + * 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 javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; + +/** + * {@link Transformer} wrapper that rewrites the Source on every {@link Transformer#transform(Source, Result)} call through + * {@link XmlFactories#harden(Source)} before delegating. + */ +final class HardeningTransformer extends DelegatingTransformer { + + HardeningTransformer(final Transformer delegate) { + super(delegate); + } + + @Override + public void transform(final Source xmlSource, final Result outputTarget) throws TransformerException { + try { + super.transform(XmlFactories.harden(xmlSource), outputTarget); + } catch (final TransformerConfigurationException e) { + throw new TransformerException(e); + } + } +} diff --git a/src/main/java/org/apache/commons/xml/factory/HardeningTransformerFactory.java b/src/main/java/org/apache/commons/xml/factory/HardeningTransformerFactory.java new file mode 100644 index 0000000..23e3a57 --- /dev/null +++ b/src/main/java/org/apache/commons/xml/factory/HardeningTransformerFactory.java @@ -0,0 +1,89 @@ +/* + * 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 javax.xml.transform.Source; +import javax.xml.transform.Templates; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.sax.TransformerHandler; + +import org.xml.sax.XMLReader; + +/** + * {@link javax.xml.transform.TransformerFactory} wrapper that rewrites every Source-taking entry point through {@link XmlFactories#harden(Source)} before + * delegating. + * + * <p>Used by providers whose underlying TrAX implementation pulls a fresh {@code SAXParserFactory.newInstance()} for any Source that is not already a + * {@link SAXSource} carrying its own {@link XMLReader}, and only sets {@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING FSP} on the resulting reader. + * Wrapping the factory and rewriting the Source upstream guarantees the parse runs through an {@link XmlFactories}-hardened reader instead.</p> + * + * <p>Three layers cooperate:</p> + * <ol> + * <li>{@link HardeningTransformerFactory} rewrites the Source on every entry point that compiles a stylesheet or transforms a one-shot input.</li> + * <li>{@link HardeningTemplates} returns a {@link HardeningTransformer} from {@link Templates#newTransformer()} so runtime source parsing is also covered, and + * restores the factory's URIResolver onto the produced Transformer (which the underlying impl typically does not propagate through {@code Templates}).</li> + * <li>{@link HardeningTransformer} rewrites the Source on every {@link Transformer#transform(Source, javax.xml.transform.Result)} call.</li> + * </ol> + * + * <h2>Caveats</h2> + * <ul> + * <li>A {@link SAXSource} that carries its own {@link XMLReader} is trusted as-is: the caller is expected to supply a hardened reader (via + * {@link XmlFactories#newSAXParserFactory()} or {@link XmlFactories#harden(XMLReader)}) in that case.</li> + * <li>{@link TransformerHandler} returned from {@code newTransformerHandler} is not wrapped: it processes incoming SAX events instead of reading a Source, so + * it has no inner Source-parsing path. A caller who pulls the inner {@link Transformer} via {@link TransformerHandler#getTransformer()} bypasses the + * runtime source rewrite.</li> + * </ul> + */ +final class HardeningTransformerFactory extends DelegatingTransformerFactory { + + HardeningTransformerFactory(final SAXTransformerFactory delegate) { + super(delegate); + } + + @Override + public Source getAssociatedStylesheet(final Source source, final String media, final String title, final String charset) + throws TransformerConfigurationException { + return super.getAssociatedStylesheet(XmlFactories.harden(source), media, title, charset); + } + + @Override + public Templates newTemplates(final Source source) throws TransformerConfigurationException { + final Templates templates = super.newTemplates(XmlFactories.harden(source)); + return templates == null ? null : new HardeningTemplates(templates, getURIResolver()); + } + + @Override + public Transformer newTransformer() throws TransformerConfigurationException { + // Identity transformer: still parses runtime sources, so wrap it to harden Transformer.transform(Source, Result). + final Transformer transformer = super.newTransformer(); + return transformer == null ? null : new HardeningTransformer(transformer); + } + + @Override + public Transformer newTransformer(final Source source) throws TransformerConfigurationException { + final Transformer transformer = super.newTransformer(XmlFactories.harden(source)); + return transformer == null ? null : new HardeningTransformer(transformer); + } + + @Override + public TransformerHandler newTransformerHandler(final Source source) throws TransformerConfigurationException { + return super.newTransformerHandler(XmlFactories.harden(source)); + } +} diff --git a/src/main/java/org/apache/commons/xml/factory/HardeningValidator.java b/src/main/java/org/apache/commons/xml/factory/HardeningValidator.java new file mode 100644 index 0000000..b970ed9 --- /dev/null +++ b/src/main/java/org/apache/commons/xml/factory/HardeningValidator.java @@ -0,0 +1,97 @@ +/* + * 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 java.io.IOException; + +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.validation.Validator; + +import org.w3c.dom.ls.LSResourceResolver; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; + +/** + * {@link Validator} wrapper that rewrites the Source on every {@link Validator#validate(Source)} and {@link Validator#validate(Source, Result)} call through + * {@link XmlFactories#harden(Source)} before delegating. + */ +final class HardeningValidator extends Validator { + + private final Validator delegate; + + HardeningValidator(final Validator delegate) { + this.delegate = delegate; + } + + @Override + public ErrorHandler getErrorHandler() { + return delegate.getErrorHandler(); + } + + @Override + public boolean getFeature(final String name) throws SAXNotRecognizedException, SAXNotSupportedException { + return delegate.getFeature(name); + } + + @Override + public Object getProperty(final String name) throws SAXNotRecognizedException, SAXNotSupportedException { + return delegate.getProperty(name); + } + + @Override + public LSResourceResolver getResourceResolver() { + return delegate.getResourceResolver(); + } + + @Override + public void reset() { + delegate.reset(); + } + + @Override + public void setErrorHandler(final ErrorHandler errorHandler) { + delegate.setErrorHandler(errorHandler); + } + + @Override + public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { + delegate.setFeature(name, value); + } + + @Override + public void setProperty(final String name, final Object object) throws SAXNotRecognizedException, SAXNotSupportedException { + delegate.setProperty(name, object); + } + + @Override + public void setResourceResolver(final LSResourceResolver resourceResolver) { + delegate.setResourceResolver(resourceResolver); + } + + @Override + public void validate(final Source source, final Result result) throws SAXException, IOException { + try { + delegate.validate(XmlFactories.harden(source), result); + } catch (final TransformerConfigurationException e) { + throw new SAXException("Failed to harden source for validation", e); + } + } +} diff --git a/src/main/java/org/apache/commons/xml/factory/SaxonProvider.java b/src/main/java/org/apache/commons/xml/factory/SaxonProvider.java index da14957..1fdde1c 100644 --- a/src/main/java/org/apache/commons/xml/factory/SaxonProvider.java +++ b/src/main/java/org/apache/commons/xml/factory/SaxonProvider.java @@ -58,6 +58,9 @@ private HardenedConfiguration() { // URI-resolution layer: empty string disallows every URI scheme. Saxon front-ends the existing ResourceResolver with a ProtocolRestrictor; a // later setResourceResolver call would cancel this filter, so this stays last in the constructor. setConfigurationProperty(Feature.ALLOWED_PROTOCOLS, ""); + // Use the parser below for both style and source: + setStyleParserClass("#DEFAULT"); + setSourceParserClass("#DEFAULT"); } /** diff --git a/src/main/java/org/apache/commons/xml/factory/StockJdkProvider.java b/src/main/java/org/apache/commons/xml/factory/StockJdkProvider.java index 6d70f5d..e56c46c 100644 --- a/src/main/java/org/apache/commons/xml/factory/StockJdkProvider.java +++ b/src/main/java/org/apache/commons/xml/factory/StockJdkProvider.java @@ -25,6 +25,7 @@ import javax.xml.parsers.SAXParserFactory; import javax.xml.stream.XMLInputFactory; import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.SAXTransformerFactory; import javax.xml.validation.SchemaFactory; import javax.xml.xpath.XPathFactory; @@ -79,6 +80,8 @@ static DocumentBuilderFactory configure(final DocumentBuilderFactory factory) { static SAXParserFactory configure(final SAXParserFactory factory) { // Required: enables the JDK XMLSecurityManager limits. setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + // Useful: namespaces should be recognized by default + factory.setNamespaceAware(true); // The remaining hardening (limits, ACCESS_EXTERNAL_*) lives in the XMLReader configure() because SAXParserFactory has no property API. return new HardeningSAXParserFactory(factory, StockJdkProvider::configure); } @@ -105,16 +108,16 @@ static XMLInputFactory configure(final XMLInputFactory factory) { } static TransformerFactory configure(final TransformerFactory factory) { - // Defense-in-depth: pin to the JDK's bundled SAX parser; see FEATURE_OVERRIDE_DEFAULT_PARSER. - setFeature(factory, FEATURE_OVERRIDE_DEFAULT_PARSER, false); - // Required: enables the JDK XMLSecurityManager limits used by the internal SAX parser. + // Required: enables XSLTC's runtime evaluator limits (entity expansion, attribute count, element/name depth). setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); // Defense-in-depth: pin to JDK 25 limits so older JDKs do not fall back to looser secure values. Limits.applyToJdkTransformer(factory); - // Defense-in-depth: already FSP-secure defaults, set explicitly so they are not relaxed via system property. + // Required: XSLTC's compile path (Util.getInputSource) propagates the factory's ACCESS_EXTERNAL_DTD onto the SAXSource's reader. setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_DTD, ""); + // Required: Prevents resolution of `xsl:import`, `xsl:include` and `document()`. setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); - return factory; + // Required: XSLTC's source-document parsing path provisions its own SAX reader if the source does not have its own parser. + return new HardeningTransformerFactory((SAXTransformerFactory) factory); } static XPathFactory configure(final XPathFactory factory) { @@ -126,16 +129,16 @@ static XPathFactory configure(final XPathFactory factory) { } static SchemaFactory configure(final SchemaFactory factory) { - // Defense-in-depth: pin to the JDK's bundled SAX parser; see FEATURE_OVERRIDE_DEFAULT_PARSER. - setFeature(factory, FEATURE_OVERRIDE_DEFAULT_PARSER, false); - // Required: enables the JDK XMLSecurityManager limits; propagates to Validator and ValidatorHandler. + // Required: enables the JDK XMLSecurityManager limits. setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); // Defense-in-depth: pin to JDK 25 limits so older JDKs do not fall back to looser secure values. Limits.applyToJdkSchema(factory); - // Defense-in-depth: already FSP-secure defaults, set explicitly so they are not relaxed via system property. + // Required: XMLSchemaLoader propagates this onto its inner SAX reader, otherwise it is overrideable by system properties setProperty(factory, XMLConstants.ACCESS_EXTERNAL_DTD, ""); + // Required: gates xs:import/include/redefine fetches and xsi:schemaLocation. setProperty(factory, XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); - return factory; + // Required: routes every newSchema(Source[]) parse through an XmlFactories-hardened reader. + return new HardeningSchemaFactory(factory); } private StockJdkProvider() { diff --git a/src/main/java/org/apache/commons/xml/factory/XalanProvider.java b/src/main/java/org/apache/commons/xml/factory/XalanProvider.java index d666960..0e16034 100644 --- a/src/main/java/org/apache/commons/xml/factory/XalanProvider.java +++ b/src/main/java/org/apache/commons/xml/factory/XalanProvider.java @@ -19,24 +19,10 @@ import static org.apache.commons.xml.factory.JaxpSetters.setFeature; import javax.xml.XMLConstants; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.Result; -import javax.xml.transform.Source; -import javax.xml.transform.Templates; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.sax.SAXSource; import javax.xml.transform.sax.SAXTransformerFactory; -import javax.xml.transform.sax.TransformerHandler; import javax.xml.xpath.XPathFactory; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.XMLReader; - /** * Hardening recipes for the external Apache Xalan distribution (the {@code xalan:xalan} artifact). * @@ -50,142 +36,20 @@ * <li><strong>{@link DenyAllResolver#URI}</strong>: required. Xalan does not implement the JAXP 1.5 {@code ACCESS_EXTERNAL_*} attributes; a deny-all * {@link javax.xml.transform.URIResolver} blocks {@code xsl:include}, {@code xsl:import}, {@code xsl:source-document} during stylesheet compilation * and {@code document()}, {@code unparsed-text()}, {@code collection()} at runtime.</li> - * <li> - * <p><strong>{@code Hardening*} wrappers + source rewriting</strong>: required. Xalan's source-document parsing path falls back to - * {@code SAXParserFactory.newInstance()} whenever the input is not a {@link DOMSource} or a {@link SAXSource} carrying its own {@link XMLReader}; it - * only sets FSP on the resulting reader. Three wrapper layers are needed:</p> - * <ol> - * <li>{@link HardeningTransformerFactory} rewrites the Source on every entry point that compiles a stylesheet.</li> - * <li>{@link HardeningTemplates} returns a hardened Transformer from {@link Templates#newTransformer()} so runtime source parsing is also - * covered.</li> - * <li>{@link HardeningTransformer} rewrites the Source on {@link Transformer#transform(Source, Result)}.</li> - * </ol> - * </li> + * <li><strong>{@link HardeningTransformerFactory} + {@link HardeningTemplates} + {@link HardeningTransformer}</strong>: required. Xalan's source-document + * parsing path falls back to {@code SAXParserFactory.newInstance()} whenever the input is not a {@link javax.xml.transform.dom.DOMSource} or a + * {@link javax.xml.transform.sax.SAXSource} carrying its own {@link org.xml.sax.XMLReader}; it only sets FSP on the resulting reader. The wrappers + * rewrite every Source through an {@link XmlFactories}-hardened reader so the Xalan internals never get to provision their own.</li> * </ul> * * <h2>Caveats</h2> * <ul> * <li>Replacing the {@code URIResolver} on the factory or transformer cancels the deny-all hardening for XSLT URI fetches; Xalan exposes no * tamper-resistant equivalent of JAXP 1.5 {@code ACCESS_EXTERNAL_STYLESHEET}.</li> - * <li>The wrappers trust a {@link SAXSource} that carries its own {@link XMLReader}: the caller is expected to supply a hardened reader (via - * {@link XmlFactories#newSAXParserFactory()} or {@link XmlFactories#harden(XMLReader)}) in that case.</li> - * <li>{@link TransformerHandler} returned from {@code newTransformerHandler} is not wrapped: it processes incoming SAX events instead of reading a Source, - * so it has no inner Source-parsing path. A caller who pulls the inner {@link Transformer} via {@link TransformerHandler#getTransformer()} bypasses - * the runtime source rewrite.</li> * </ul> */ final class XalanProvider { - /** - * {@link TransformerFactory} wrapper that rewrites every Source-taking entry point through {@link #hardenSource(Source)} before delegating. - */ - private static final class HardeningTransformerFactory extends DelegatingTransformerFactory { - - HardeningTransformerFactory(final SAXTransformerFactory delegate) { - super(delegate); - } - - @Override - public Source getAssociatedStylesheet(final Source source, final String media, final String title, final String charset) - throws TransformerConfigurationException { - return super.getAssociatedStylesheet(hardenSource(source), media, title, charset); - } - - @Override - public Templates newTemplates(final Source source) throws TransformerConfigurationException { - final Templates templates = super.newTemplates(hardenSource(source)); - return templates == null ? null : new HardeningTemplates(templates); - } - - @Override - public Transformer newTransformer() throws TransformerConfigurationException { - // Identity transformer: still parses runtime sources, so wrap it to harden Transformer.transform(Source, Result). - final Transformer transformer = super.newTransformer(); - if (transformer == null) { - return null; - } - return new HardeningTransformer(transformer); - } - - @Override - public Transformer newTransformer(final Source source) throws TransformerConfigurationException { - final Transformer transformer = super.newTransformer(hardenSource(source)); - if (transformer == null) { - return null; - } - return new HardeningTransformer(transformer); - } - - @Override - public TransformerHandler newTransformerHandler(final Source source) throws TransformerConfigurationException { - return super.newTransformerHandler(hardenSource(source)); - } - } - - /** - * {@link Templates} wrapper whose only purpose is to return a {@link HardeningTransformer} from {@link Templates#newTransformer()}. - */ - private static final class HardeningTemplates extends DelegatingTemplates { - - HardeningTemplates(final Templates delegate) { - super(delegate); - } - - @Override - public Transformer newTransformer() throws TransformerConfigurationException { - final Transformer transformer = super.newTransformer(); - if (transformer == null) { - return null; - } - return new HardeningTransformer(transformer); - } - } - - /** - * {@link Transformer} wrapper that rewrites the Source on every {@link Transformer#transform(Source, Result)} call. - */ - private static final class HardeningTransformer extends DelegatingTransformer { - - HardeningTransformer(final Transformer delegate) { - super(delegate); - } - - @Override - public void transform(final Source xmlSource, final Result outputTarget) throws TransformerException { - super.transform(hardenSource(xmlSource), outputTarget); - } - } - - /** - * Rewrites a Source to one whose XML reader is hardened by {@link XmlFactories}. - * - * <p>Pass-through cases (the caller is trusted to have arranged equivalent hardening already):</p> - * <ul> - * <li>{@link DOMSource}: the tree is already built; no parser is invoked.</li> - * <li>{@link SAXSource} carrying its own {@link XMLReader}: the caller's reader is reused, matching Xalan's own contract.</li> - * </ul> - * - * <p>Otherwise the source is converted via {@link SAXSource#sourceToInputSource} and re-wrapped in a {@link SAXSource} backed by an - * {@link XmlFactories}-hardened reader.</p> - */ - private static Source hardenSource(final Source source) throws TransformerConfigurationException { - if (source instanceof DOMSource || source instanceof SAXSource && ((SAXSource) source).getXMLReader() != null) { - return source; - } - final InputSource inputSource = SAXSource.sourceToInputSource(source); - if (inputSource == null) { - return source; - } - try { - final XMLReader reader = XmlFactories.newSAXParserFactory().newSAXParser().getXMLReader(); - final SAXSource hardened = new SAXSource(reader, inputSource); - hardened.setSystemId(source.getSystemId()); - return hardened; - } catch (final ParserConfigurationException | SAXException e) { - throw new TransformerConfigurationException("Failed to obtain a hardened XMLReader for Xalan source parsing", e); - } - } - static TransformerFactory configure(final TransformerFactory factory) { // Required: enables Xalan's secure-processing mode (extension-function block) setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); diff --git a/src/main/java/org/apache/commons/xml/factory/XercesProvider.java b/src/main/java/org/apache/commons/xml/factory/XercesProvider.java index 3c58fa4..8052f48 100644 --- a/src/main/java/org/apache/commons/xml/factory/XercesProvider.java +++ b/src/main/java/org/apache/commons/xml/factory/XercesProvider.java @@ -24,15 +24,12 @@ import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; -import javax.xml.transform.Source; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; import javax.xml.validation.ValidatorHandler; -import org.w3c.dom.ls.LSResourceResolver; import org.xml.sax.EntityResolver; -import org.xml.sax.SAXException; import org.xml.sax.SAXNotRecognizedException; import org.xml.sax.SAXNotSupportedException; import org.xml.sax.XMLReader; @@ -88,73 +85,24 @@ public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException } } - /** - * Hardened Xerces {@link Schema} wrapper. - * - * <p>Required because Xerces' {@link Schema} does not propagate the {@link SchemaFactory}'s resolver or security manager to - * {@link Validator} / {@link ValidatorHandler} products, so this wrapper re-installs the deny-all resolver (closes the DOCTYPE SYSTEM vector - * inside the instance document) and pins the JDK 25 limits on every product.</p> - */ - private static final class HardeningSchema extends Schema { - - private final Schema delegate; - private final LSResourceResolver resolver; - - HardeningSchema(final Schema delegate, final LSResourceResolver resolver) { - this.delegate = delegate; - this.resolver = resolver; - } - - @Override - public Validator newValidator() { - final Validator validator = delegate.newValidator(); - try { - // Defense-in-depth: pin the validator's security manager to JDK 25 limits. - Limits.applyToXerces(validator.getProperty(XERCES_SECURITY_MANAGER_PROPERTY)); - } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { - throw new HardeningException("Failed to read Xerces security manager from Validator", e); - } - // Required: re-install the deny-all resolver since SchemaFactory-level hardening does not propagate to Validators. - validator.setResourceResolver(resolver); - return validator; - } - - @Override - public ValidatorHandler newValidatorHandler() { - final ValidatorHandler handler = delegate.newValidatorHandler(); - try { - // Defense-in-depth: pin the handler's security manager to JDK 25 limits. - Limits.applyToXerces(handler.getProperty(XERCES_SECURITY_MANAGER_PROPERTY)); - } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { - throw new HardeningException("Failed to read Xerces security manager from ValidatorHandler", e); - } - // Required: re-install the deny-all resolver since SchemaFactory-level hardening does not propagate to ValidatorHandlers. - handler.setResourceResolver(resolver); - return handler; + private static Validator hardenValidator(final Validator validator) { + try { + Limits.applyToXerces(validator.getProperty(XERCES_SECURITY_MANAGER_PROPERTY)); + } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { + throw new HardeningException("Failed to read Xerces security manager from Validator", e); } + validator.setResourceResolver(DenyAllResolver.LS_RESOURCE); + return validator; } - /** - * Hardened Xerces {@link SchemaFactory} wrapper. - * - * <p>Wraps every produced {@link Schema} in {@link HardeningSchema} so the SchemaFactory's resolver and security manager are re-installed on each - * {@link Validator} / {@link ValidatorHandler}.</p> - */ - private static final class HardeningSchemaFactory extends DelegatingSchemaFactory { - - HardeningSchemaFactory(final SchemaFactory delegate) { - super(delegate); - } - - @Override - public Schema newSchema() throws SAXException { - return new HardeningSchema(super.newSchema(), super.getResourceResolver()); - } - - @Override - public Schema newSchema(final Source[] schemas) throws SAXException { - return new HardeningSchema(super.newSchema(schemas), super.getResourceResolver()); + private static ValidatorHandler hardenValidatorHandler(final ValidatorHandler handler) { + try { + Limits.applyToXerces(handler.getProperty(XERCES_SECURITY_MANAGER_PROPERTY)); + } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { + throw new HardeningException("Failed to read Xerces security manager from ValidatorHandler", e); } + handler.setResourceResolver(DenyAllResolver.LS_RESOURCE); + return handler; } /** @@ -189,6 +137,8 @@ static DocumentBuilderFactory configure(final DocumentBuilderFactory factory) { static SAXParserFactory configure(final SAXParserFactory factory) { // Required: enables Xerces' built-in SecurityManager (which is what carries the limits). setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + // Useful: namespaces should be recognized by default + factory.setNamespaceAware(true); // The remaining hardening (limits, entity resolver) lives in the XMLReader configure() because SAXParserFactory has no property API. return new HardeningSAXParserFactory(factory, XercesProvider::configure); } @@ -210,18 +160,19 @@ static XMLReader configure(final XMLReader reader) { } static SchemaFactory configure(final SchemaFactory factory) { - // Required: enables Xerces' built-in SecurityManager (which is what carries the limits). + // Required: enables Xerces' built-in SecurityManager. setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); try { - // Defense-in-depth: pin the factory's security manager to JDK 25 limits; Xerces' own defaults are looser than even JDK 8. + // Required: pins limits to JDK 25 secure values, otherwise Xerces' own caps are looser than JDK 8. Limits.applyToXerces(factory.getProperty(XERCES_SECURITY_MANAGER_PROPERTY)); } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { throw new HardeningException("Failed to read Xerces security manager from SchemaFactory", e); } - // Required: Xerces does not honour JAXP 1.5 ACCESS_EXTERNAL_*; install the deny-all resolver so xs:import/xs:include/xs:redefine cannot fetch. + // Required: Xerces ignores ACCESS_EXTERNAL_*; the deny-all resolver blocks xs:import/include/redefine fetches. factory.setResourceResolver(DenyAllResolver.LS_RESOURCE); - // Required: Xerces' Schema does not propagate the SchemaFactory's resolver or security manager to Validator/ValidatorHandler; the wrapper re-installs. - return new HardeningSchemaFactory(factory); + // Required: routes every newSchema(Source[]) parse through an XmlFactories-hardened reader, and re-installs limits + resolver on each Validator and + // ValidatorHandler since Xerces' Schema does not propagate factory state through. + return new HardeningSchemaFactory(factory, XercesProvider::hardenValidator, XercesProvider::hardenValidatorHandler); } private XercesProvider() { diff --git a/src/main/java/org/apache/commons/xml/factory/XmlFactories.java b/src/main/java/org/apache/commons/xml/factory/XmlFactories.java index 00dd1b0..a16fa33 100644 --- a/src/main/java/org/apache/commons/xml/factory/XmlFactories.java +++ b/src/main/java/org/apache/commons/xml/factory/XmlFactories.java @@ -18,12 +18,19 @@ import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import javax.xml.stream.XMLInputFactory; +import javax.xml.transform.Source; +import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; import javax.xml.validation.SchemaFactory; import javax.xml.xpath.XPathFactory; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; import org.xml.sax.XMLReader; /** @@ -142,6 +149,28 @@ private static SchemaFactory dispatch(final SchemaFactory factory) { } } + /** + * Rewrites a {@link Source} so that any SAX parsing it triggers runs through an {@link XmlFactories}-hardened {@link XMLReader}. + * + * <p>Only {@link StreamSource} and {@link SAXSource} without a reader are enriched with a hardened reader. Other kinds of sources are returned as-is.</p> + * + * @param source the source to harden; never {@code null}. + * @return a hardened source. + * @throws TransformerConfigurationException if a hardened reader cannot be obtained. + */ + public static Source harden(final Source source) throws TransformerConfigurationException { + if (source instanceof StreamSource || source instanceof SAXSource && ((SAXSource) source).getXMLReader() == null) { + try { + final XMLReader reader = newSAXParserFactory().newSAXParser().getXMLReader(); + final InputSource inputSource = SAXSource.sourceToInputSource(source); + return inputSource == null ? source : new SAXSource(reader, inputSource); + } catch (final ParserConfigurationException | SAXException e) { + throw new TransformerConfigurationException("Failed to obtain a hardened XMLReader for source parsing", e); + } + } + return source; + } + /** * Hardens an existing {@link XMLReader}. * @@ -155,6 +184,7 @@ public static XMLReader harden(final XMLReader reader) { case "com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser": return StockJdkProvider.configure(reader); case "org.apache.harmony.xml.ExpatReader": + case "org.apache.commons.xml.factory.AndroidProvider$GuardedXMLReader": return AndroidProvider.configure(reader); case "org.apache.xerces.jaxp.SAXParserImpl$JAXPSAXParser": return XercesProvider.configure(reader); diff --git a/src/test/java/org/apache/commons/xml/factory/AttackTestSupport.java b/src/test/java/org/apache/commons/xml/factory/AttackTestSupport.java index 0ce93c6..b130bf0 100644 --- a/src/test/java/org/apache/commons/xml/factory/AttackTestSupport.java +++ b/src/test/java/org/apache/commons/xml/factory/AttackTestSupport.java @@ -36,14 +36,17 @@ import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.ErrorListener; import javax.xml.transform.Source; import javax.xml.transform.Templates; import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; @@ -51,32 +54,38 @@ import org.junit.jupiter.api.function.ThrowingSupplier; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; /** * Shared fixtures for attack tests. * - * <p>The hardened-side helpers come in two flavours, distinguished by their suffix:</p> + * <p>The hardened-side helpers come in three flavours, distinguished by their suffix:</p> * * <ul> * <li>{@code assert*Blocks(...)} runs the payload through a hardened factory from {@link XmlFactories} and asserts the parse throws. Used when the hardening * layer is expected to reject the attack outright.</li> - * <li>{@code assert*DoesNotLeak(...)} runs the payload through a hardened factory and asserts the {@link #LEAKED_MARKER} string does not appear in the - * output. The parse may either throw or succeed silently. Used where the JAXP API does not give a clean throw hook (SAX content stream, transformer - * output) or where the XML spec lets the parser silently skip undeclared entities once the external DTD subset has been refused (DOM with an external - * DTD subset).</li> + * <li>{@code assert*DoesNotLeak(...)} runs the payload through a hardened factory and asserts the parse completes without throwing and without producing the + * {@link #LEAKED_MARKER} string. Used when the hardening contract guarantees the parse succeeds but never resolves the external resource (e.g. + * {@code XERCES_LOAD_EXTERNAL_DTD=false} silently skipping the external subset, with the body's undeclared entity reference dropped per XML 1.0 + * §4.1).</li> + * <li>{@code assert*BlocksOrDoesNotLeak(...)} accepts either of the previous two outcomes. Used where the same hardening contract surfaces differently across + * providers (e.g. stock-JDK XSLTC throws via {@code ACCESS_EXTERNAL_DTD} while Apache Xalan silently skips because its source-rewrite routes parsing + * through a {@code XERCES_LOAD_EXTERNAL_DTD=false} reader).</li> * </ul> * * <p>DOM tests that depend on user-defined entity machinery should gate themselves with {@link org.junit.jupiter.api.Assumptions#assumeTrue} on * {@link #DOM_RESOLVES_INTERNAL_ENTITIES} so they skip on platforms (such as Android with KXmlParser) whose DOM parser does not surface the entity events that * the strict {@link #assertDomBlocks} assertion expects.</p> * - * <p>The unconfigured-side positive controls keep verbs that match the JAXP type they exercise: {@code assert*Resolves(payload)} for direct parsing, - * {@code assert*Compiles(...)} for {@link SchemaFactory} / {@link TransformerFactory} compilation, {@code assertTransformerSucceeds(payload)} for - * {@code Transformer.transform}, {@code assertValidatorAccepts(payload)} for {@code Validator.validate}.</p> + * <p>The permissive-side positive controls mirror the hardened-side verbs with an {@code assertPermissive*} prefix: {@code assertPermissive*Parses} for direct + * parsing, {@code assertPermissive*Compiles} for {@link SchemaFactory} / {@link TransformerFactory} compilation, {@code assertPermissiveTransformerTransforms} + * for {@code Transformer.transform}, {@code assertPermissiveValidatorValidates} for {@code Validator.validate}. Both sides perform the same operation; the + * prefix marks which factory hardening level the assertion is set against.</p> * * <p>Schema and Templates assertions take a {@link Source} so the same helper covers both inline-string payloads and resource-backed wrappers; build the * source via {@link #streamSource(String)} for a string payload or {@link #resourceSource(String)} for a file under {@code src/test/resources/leaked/}. The @@ -88,6 +97,49 @@ */ final class AttackTestSupport { + /** + * Strict reporter installed on every hardened factory, parser, validator and transformer in the helpers below. + * + * <p>The hardening layer signals every blocked external fetch and every SAX-fatal it could not silently skip via the standard JAXP error channels: + * {@link ErrorListener#error(TransformerException)} / {@link ErrorListener#fatalError(TransformerException) fatalError} on the TrAX side and + * {@link ErrorHandler#error(SAXParseException) error} / {@link ErrorHandler#fatalError(SAXParseException) fatalError} on the SAX side. Both Apache Xalan's + * {@code DefaultErrorHandler(false)} and Saxon's {@code StandardErrorListener} are pathologically lenient defaults that swallow these events; SAX's + * {@link DefaultHandler} treats {@code error} as a no-op. The test fixture replaces those defaults with a strict reporter that re-throws on every reported + * error or fatalError so the helpers can observe the block via the same mechanism the spec uses to surface it. Warnings stay silent: they are not security + * signals.</p> + */ + private static final class StrictReporter implements ErrorListener, ErrorHandler { + + @Override + public void error(final SAXParseException exception) throws SAXException { + throw exception; + } + + @Override + public void error(final TransformerException exception) throws TransformerException { + throw exception; + } + + @Override + public void fatalError(final SAXParseException exception) throws SAXException { + throw exception; + } + + @Override + public void fatalError(final TransformerException exception) throws TransformerException { + throw exception; + } + + @Override + public void warning(final SAXParseException exception) { + // not a security signal + } + + @Override + public void warning(final TransformerException exception) { + // not a security signal + } + } /** * Trivial W3C XML Schema that validates {@link #xmlBody(String)} output. */ @@ -102,7 +154,6 @@ final class AttackTestSupport { + " </xs:complexType>\n" + " </xs:element>\n" + "</xs:schema>\n"; - /** * Benign payload used to probe whether the DOM parser inlines a user-defined internal general entity into the tree. */ @@ -110,17 +161,14 @@ final class AttackTestSupport { "<?xml version=\"1.0\"?>\n" + "<!DOCTYPE root [<!ENTITY foo \"bar\">]>\n" + "<root>&foo;</root>"; - /** * Set to {@code true} when the platform's DOM parser supports user-defined internal entities. * * <p>Android's {@code KXmlParser} currently fails this test.</p> */ static final boolean DOM_RESOLVES_INTERNAL_ENTITIES = probeDomResolvesInternalEntities(); - /** {@code true} when running on Android (Dalvik / ART), {@code false} on any standard JVM. Probed once via {@code Class.forName} on {@code android.os.Build}. */ static final boolean IS_ANDROID = probeAndroid(); - /** * URL form of the JDK's entity-expansion limit property. */ @@ -132,6 +180,7 @@ final class AttackTestSupport { * presence is the leak signal.</p> */ static final String LEAKED_MARKER = "All your base are belong to us"; + private static final StrictReporter STRICT_REPORTER = new StrictReporter(); /** * Asserts a hardened DOM parse of the payload throws. @@ -139,25 +188,17 @@ final class AttackTestSupport { * <p>{@link DocumentBuilder#parse(InputSource)} via {@link XmlFactories#newDocumentBuilderFactory()}; only a thrown exception passes.</p> */ static void assertDomBlocks(final String payload) { - assertParseFails(() -> XmlFactories.newDocumentBuilderFactory().newDocumentBuilder().parse(inputSource(payload)), "DOM", SAXException.class); + assertParseFails(() -> strictDocumentBuilder(XmlFactories.newDocumentBuilderFactory()).parse(inputSource(payload)), "DOM", SAXException.class); } /** - * Asserts a hardened DOM parse either throws or completes without leaked content. + * Asserts a hardened DOM parse completes without throwing and without leaked content. * - * <p>{@link DocumentBuilder#parse(InputSource)} via {@link XmlFactories#newDocumentBuilderFactory()}; XML 1.0 §4.1 permits silent skipping of undeclared - * entities when an external subset is declared but unread.</p> + * <p>{@link DocumentBuilder#parse(InputSource)} via {@link XmlFactories#newDocumentBuilderFactory()}; use this when the hardening guarantee is "the parse + * succeeds but never resolves the external resource", e.g. when {@code XERCES_LOAD_EXTERNAL_DTD=false} silently skips the external subset.</p> */ static void assertDomDoesNotLeak(final String payload) { - assertNoLeak(() -> { - final Document doc = XmlFactories.newDocumentBuilderFactory().newDocumentBuilder().parse(inputSource(payload)); - if (doc.getDocumentElement() == null) { - return ""; - } - // Harmony's DOM returns null from getTextContent() on an element whose only children are unresolved EntityReference nodes. - final String text = doc.getDocumentElement().getTextContent(); - return text == null ? "" : text; - }, "DOM", SAXException.class); + assertNoLeakStrict(() -> domParseAndCaptureText(payload), "DOM"); } /** @@ -166,28 +207,11 @@ static void assertDomDoesNotLeak(final String payload) { * <p>{@link DocumentBuilder#parse(InputSource)} via {@link XmlFactories#newDocumentBuilderFactory()}; positive control for DOCTYPE-only payloads.</p> */ static void assertDomParses(final String payload) { - assertParseSucceeds(() -> XmlFactories.newDocumentBuilderFactory().newDocumentBuilder().parse(inputSource(payload)), "DOM"); - } - - /** - * Asserts a permissive DOM parse succeeds. - * - * <p>{@link DocumentBuilder#parse(InputSource)} via {@link DocumentBuilderFactory#newInstance()} with FSP off; positive control proving the payload is - * well-formed.</p> - */ - static void assertDomResolves(final String payload) { - assertParseSucceeds(() -> { - final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - if (!IS_ANDROID) { - factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); - } - suppressException(() -> factory.setAttribute(JDK_ENTITY_EXPANSION_LIMIT, "0")); - factory.newDocumentBuilder().parse(inputSource(payload)); - }, "DOM"); + assertParseSucceeds(() -> strictDocumentBuilder(XmlFactories.newDocumentBuilderFactory()).parse(inputSource(payload)), "DOM"); } /** - * Skeleton for every {@code assert*DoesNotLeak} helper. + * Skeleton for every {@code assert*BlocksOrDoesNotLeak} helper. * * <p>Treats a thrown exception of one of the {@code expected} types as "hardening blocked at parse" (acceptable); otherwise asserts the captured output * omits {@link #LEAKED_MARKER}. A throw whose type does not match {@code expected} fails the test, so unrelated failures (e.g. a {@link HardeningException} @@ -198,7 +222,7 @@ static void assertDomResolves(final String payload) { * @param expected the exception types any of which the hardening layer may surface as a clean rejection. */ @SafeVarargs - private static void assertNoLeak(final ThrowingSupplier<String> action, final String description, final Class<? extends Throwable>... expected) { + private static void assertNoLeakOrThrows(final ThrowingSupplier<String> action, final String description, final Class<? extends Throwable>... expected) { final String output; try { output = action.get(); @@ -212,6 +236,22 @@ private static void assertNoLeak(final ThrowingSupplier<String> action, final St "Hardening did not block " + description + "; output contained marker '" + LEAKED_MARKER + "'.\nFull output:\n" + output); } + /** + * Skeleton for every strict {@code assert*DoesNotLeak} helper. + * + * <p>Runs the action, lets any thrown exception fail the assertion, and asserts that the captured output omits {@link #LEAKED_MARKER}. Use this when the + * hardening contract guarantees "parses successfully without resolving the external resource"; use {@link #assertNoLeakOrThrows} when the contract is + * "either blocks at parse or completes without leaked content".</p> + * + * @param action the parse to execute, returning the captured output text checked for {@link #LEAKED_MARKER}. + * @param description short label naming the JAXP surface under test. + */ + private static void assertNoLeakStrict(final ThrowingSupplier<String> action, final String description) { + final String output = assertDoesNotThrow(action, "Hardened " + description + " parse must not throw"); + assertFalse(output.contains(LEAKED_MARKER), + "Hardening did not block " + description + "; output contained marker '" + LEAKED_MARKER + "'.\nFull output:\n" + output); + } + /** * Asserts the supplied parsing action throws an exception of the {@code expected} type. * @@ -241,8 +281,9 @@ static void assertParseFails(final Executable action, final String description, /** * Asserts the supplied action does not throw. * - * <p>Generic primitive underlying every {@code assert*Parses(...)} / {@code assert*Compiles(...)} / {@code assert*Resolves(...)} / - * {@code assert*Succeeds(...)} / {@code assertValidatorAccepts(...)} helper; exposed for tests that compose a non-standard call.</p> + * <p>Generic primitive underlying every {@code assert*Parses(...)} / {@code assert*Compiles(...)} / {@code assert*Transforms(...)} / + * {@code assert*Validates(...)} helper (and their permissive {@code assertPermissive*} counterparts); exposed for tests that compose a non-standard + * call.</p> * * @param action the parse to execute. * @param description short label included in the failure message. @@ -252,31 +293,20 @@ static void assertParseSucceeds(final Executable action, final String descriptio } /** - * Asserts a hardened SAX parse of the payload throws. - * - * <p>{@link XMLReader#parse(InputSource)} on a parser from {@link XmlFactories#newSAXParserFactory()}; only a thrown exception passes.</p> - */ - static void assertSaxBlocks(final String payload) { - assertParseFails(() -> parseQuietly(XmlFactories.newSAXParserFactory().newSAXParser().getXMLReader(), payload), "SAX", SAXException.class); - } - - /** - * Asserts a hardened SAX parse either throws or completes without leaked content. - * - * <p>{@link XMLReader#parse(InputSource)} on a parser from {@link XmlFactories#newSAXParserFactory()}; XML 1.0 §4.1 permits silent skipping of undeclared - * entities when an external subset is declared but unread.</p> - */ - static void assertSaxDoesNotLeak(final String payload) { - assertNoLeak(() -> captureCharacters(XmlFactories.newSAXParserFactory().newSAXParser().getXMLReader(), payload), "SAX", SAXException.class); - } - - /** - * Asserts a hardened SAX parse succeeds. + * Asserts a permissive DOM parse succeeds. * - * <p>{@link XMLReader#parse(InputSource)} on a parser from {@link XmlFactories#newSAXParserFactory()}; positive control for DOCTYPE-only payloads.</p> + * <p>{@link DocumentBuilder#parse(InputSource)} via {@link DocumentBuilderFactory#newInstance()} with FSP off; positive control proving the payload is + * well-formed.</p> */ - static void assertSaxParses(final String payload) { - assertParseSucceeds(() -> parseQuietly(XmlFactories.newSAXParserFactory().newSAXParser().getXMLReader(), payload), "SAX"); + static void assertPermissiveDomParses(final String payload) { + assertParseSucceeds(() -> { + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + if (!IS_ANDROID) { + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); + } + suppressException(() -> factory.setAttribute(JDK_ENTITY_EXPANSION_LIMIT, "0")); + strictDocumentBuilder(factory).parse(inputSource(payload)); + }, "DOM"); } /** @@ -285,77 +315,150 @@ static void assertSaxParses(final String payload) { * <p>{@link XMLReader#parse(InputSource)} on a parser from {@link SAXParserFactory#newInstance()} with FSP off; positive control proving the payload is * well-formed.</p> */ - static void assertSaxResolves(final String payload) { + static void assertPermissiveSaxParses(final String payload) { assertParseSucceeds(() -> { final SAXParserFactory factory = SAXParserFactory.newInstance(); if (!IS_ANDROID) { factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); } - final XMLReader reader = factory.newSAXParser().getXMLReader(); + final XMLReader reader = strictXMLReader(factory); suppressException(() -> reader.setProperty(JDK_ENTITY_EXPANSION_LIMIT, "0")); - parseQuietly(reader, payload); + consumeXmlReader(reader, payload); }, "SAX"); } - /** - * Asserts a hardened Schema compilation throws. - * - * <p>{@link SchemaFactory#newSchema(Source)} via {@link XmlFactories#newSchemaFactory()}; only a thrown exception passes.</p> - */ - static void assertSchemaBlocks(final Source xsd) { - assertParseFails(() -> XmlFactories.newSchemaFactory().newSchema(xsd), "Schema compile", SAXException.class, SecurityException.class); - } - /** * Asserts a permissive Schema compilation succeeds. * * <p>{@link SchemaFactory#newSchema(Source)} via {@link SchemaFactory#newInstance(String)} with FSP off; positive control proving the wrapper is * well-formed.</p> */ - static void assertSchemaCompiles(final Source xsd) { + static void assertPermissiveSchemaCompiles(final Source xsd) { assertParseSucceeds(() -> { final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); if (!IS_ANDROID) { factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); } suppressException(() -> factory.setProperty(JDK_ENTITY_EXPANSION_LIMIT, "0")); - factory.newSchema(xsd); + strictSchema(factory, xsd); }, "Schema compile"); } /** - * Asserts a hardened StAX parse of the payload throws. + * Asserts a permissive StAX parse succeeds. * - * <p>{@link XMLStreamReader} and {@link XMLEventReader} from {@link XmlFactories#newXMLInputFactory()}; both flavours are exercised and either must - * throw.</p> + * <p>{@link XMLStreamReader} from {@link XMLInputFactory#newInstance()} with FSP off; positive control proving the payload is well-formed.</p> */ - static void assertStaxBlocks(final String payload) { - assertParseFails(() -> consumeStreamReader(XmlFactories.newXMLInputFactory(), payload), "StAX stream", XMLStreamException.class); - assertParseFails(() -> consumeEventReader(XmlFactories.newXMLInputFactory(), payload), "StAX event", XMLStreamException.class); + static void assertPermissiveStaxParses(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); + }, "StAX"); + } + + /** + * Asserts a permissive Templates compilation succeeds. + * + * <p>{@link TransformerFactory#newTransformer(Source)} via {@link TransformerFactory#newInstance()} with FSP off; positive control proving the stylesheet + * is well-formed.</p> + */ + static void assertPermissiveTemplatesCompiles(final String xslt) { + assertParseSucceeds(() -> { + final TransformerFactory factory = TransformerFactory.newInstance(); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); + strictTemplates(factory, permissiveSaxSource(xslt)); + }, "Templates compile"); } /** - * Asserts a hardened StAX parse succeeds. + * Asserts a permissive identity Transformer succeeds. * - * <p>{@link XMLStreamReader} from {@link XmlFactories#newXMLInputFactory()}; positive control for DOCTYPE-only payloads.</p> + * <p>{@link Transformer#transform(Source, javax.xml.transform.Result)} via {@link TransformerFactory#newInstance()} with FSP off; positive control proving + * the payload is well-formed.</p> */ - static void assertStaxParses(final String payload) { - assertParseSucceeds(() -> consumeStreamReader(XmlFactories.newXMLInputFactory(), payload), "StAX"); + static void assertPermissiveTransformerTransforms(final String payload) { + assertParseSucceeds(() -> { + final TransformerFactory factory = TransformerFactory.newInstance(); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); + strictTransformer(factory).transform(permissiveSaxSource(payload), new StreamResult(new StringWriter())); + }, "Transformer"); } /** - * Asserts a permissive StAX parse succeeds. + * Asserts a permissive Validator validation succeeds. * - * <p>{@link XMLStreamReader} from {@link XMLInputFactory#newInstance()} with FSP off; positive control proving the payload is well-formed.</p> + * <p>{@link Validator#validate(Source)} on a validator from {@link #BENIGN_SCHEMA} compiled via {@link SchemaFactory#newInstance(String)} with FSP off; + * positive control proving the instance is well-formed.</p> */ - static void assertStaxResolves(final String payload) { + static void assertPermissiveValidatorValidates(final String xml) { 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. + 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")); - consumeStreamReader(factory, payload); - }, "StAX"); + strictValidator(strictSchema(factory, streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)); + }, "Validator"); + } + + /** + * Asserts a hardened SAX parse of the payload throws. + * + * <p>{@link XMLReader#parse(InputSource)} on a parser from {@link XmlFactories#newSAXParserFactory()}; only a thrown exception passes.</p> + */ + static void assertSaxBlocks(final String payload) { + assertParseFails(() -> consumeXmlReader(strictXMLReader(XmlFactories.newSAXParserFactory()), payload), "SAX", SAXException.class); + } + + /** + * Asserts a hardened SAX parse completes without throwing and without leaked content. + * + * <p>{@link XMLReader#parse(InputSource)} on a parser from {@link XmlFactories#newSAXParserFactory()}; use this when the hardening guarantee is "the parse + * succeeds but never resolves the external resource", e.g. when {@code XERCES_LOAD_EXTERNAL_DTD=false} silently skips the external subset.</p> + */ + static void assertSaxDoesNotLeak(final String payload) { + assertNoLeakStrict(() -> captureCharacters(strictXMLReader(XmlFactories.newSAXParserFactory()), payload), "SAX"); + } + + /** + * Asserts a hardened SAX parse succeeds. + * + * <p>{@link XMLReader#parse(InputSource)} on a parser from {@link XmlFactories#newSAXParserFactory()}; positive control for DOCTYPE-only payloads.</p> + */ + static void assertSaxParses(final String payload) { + assertParseSucceeds(() -> consumeXmlReader(strictXMLReader(XmlFactories.newSAXParserFactory()), payload), "SAX"); + } + + /** + * Asserts a hardened Schema compilation throws. + * + * <p>{@link SchemaFactory#newSchema(Source)} via {@link XmlFactories#newSchemaFactory()}; only a thrown exception passes.</p> + */ + static void assertSchemaBlocks(final Source xsd) { + assertParseFails(() -> strictSchema(XmlFactories.newSchemaFactory(), xsd), "Schema compile", SAXException.class, SecurityException.class); + } + + /** + * Asserts a hardened Schema compilation completes without throwing. + * + * <p>{@link SchemaFactory#newSchema(Source)} via {@link XmlFactories#newSchemaFactory()}; use this when the hardening contract guarantees the compile + * succeeds but never resolves the external resource (e.g. {@code XERCES_LOAD_EXTERNAL_DTD=false} silently skipping the external subset, with the body's + * undeclared entity reference dropped per XML 1.0 §4.1).</p> + */ + static void assertSchemaDoesNotLeak(final Source xsd) { + assertParseSucceeds(() -> strictSchema(XmlFactories.newSchemaFactory(), xsd), "Schema compile"); + } + + /** + * Asserts a hardened StAX parse of the payload throws. + * + * <p>{@link XMLStreamReader} and {@link XMLEventReader} from {@link XmlFactories#newXMLInputFactory()}; both flavours are exercised and either must + * throw.</p> + */ + static void assertStaxBlocks(final String payload) { + assertParseFails(() -> consumeStreamReader(XmlFactories.newXMLInputFactory(), payload), "StAX stream", XMLStreamException.class); + assertParseFails(() -> consumeEventReader(XmlFactories.newXMLInputFactory(), payload), "StAX event", XMLStreamException.class); } /** @@ -366,45 +469,33 @@ static void assertStaxResolves(final String payload) { */ static void assertTemplatesBlocks(final Source xslt) { assertParseFails(() -> { - final Templates templates = XmlFactories.newTransformerFactory().newTemplates(xslt); - // Xalan return `null` if the template fails + final Templates templates = strictTemplates(XmlFactories.newTransformerFactory(), xslt); + // Xalan returns `null` if the template fails if (templates == null) { throw new TransformerException("Transformer factory returned null"); } - templates.newTransformer().transform(streamSource("<root/>"), new StreamResult(new StringWriter())); + strictTransformer(templates).transform(streamSource("<root/>"), new StreamResult(new StringWriter())); }, "Templates", TransformerException.class); } /** - * Asserts a permissive Templates compilation succeeds. + * Asserts a hardened Templates compile-and-transform either throws or completes without leaked content. * - * <p>{@link TransformerFactory#newTransformer(Source)} via {@link TransformerFactory#newInstance()} with FSP off; positive control proving the stylesheet - * is well-formed.</p> + * <p>{@link TransformerFactory#newTemplates(Source)} via {@link XmlFactories#newTransformerFactory()} followed by transform; Saxon's TrAX path may swallow + * the entity-expansion error and produce empty output.</p> */ - static void assertTemplatesCompiles(final String xslt) { - assertParseSucceeds(() -> { - final TransformerFactory factory = TransformerFactory.newInstance(); - factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); - factory.newTransformer(permissiveSaxSource(xslt)); - }, "Templates compile"); + static void assertTemplatesBlocksOrDoesNotLeak(final Source xslt) { + assertNoLeakOrThrows(() -> templatesCompileAndTransform(xslt), "Templates", TransformerException.class); } /** - * Asserts a hardened Templates compile-and-transform either throws or completes without leaked content. + * Asserts a hardened Templates compile-and-transform completes without throwing and without leaked content. * - * <p>{@link TransformerFactory#newTemplates(Source)} via {@link XmlFactories#newTransformerFactory()} followed by transform; Saxon's TrAX path may swallow - * the entity-expansion error and produce empty output.</p> + * <p>{@link TransformerFactory#newTemplates(Source)} via {@link XmlFactories#newTransformerFactory()} followed by transform; use this when the hardening + * contract guarantees the compile and transform succeed but never resolve the external resource.</p> */ static void assertTemplatesDoesNotLeak(final Source xslt) { - assertNoLeak(() -> { - final StringWriter sink = new StringWriter(); - final Templates templates = XmlFactories.newTransformerFactory().newTemplates(xslt); - // Xalan return `null` if the template fails - if (templates != null) { - templates.newTransformer().transform(streamSource("<root/>"), new StreamResult(sink)); - } - return sink.toString(); - }, "Templates", TransformerException.class); + assertNoLeakStrict(() -> templatesCompileAndTransform(xslt), "Templates"); } /** @@ -415,7 +506,7 @@ static void assertTemplatesDoesNotLeak(final Source xslt) { */ static void assertTransformerBlocks(final String payload) { assertParseFails( - () -> XmlFactories.newTransformerFactory().newTransformer().transform(streamSource(payload), new StreamResult(new StringWriter())), + () -> strictTransformer(XmlFactories.newTransformerFactory()).transform(streamSource(payload), new StreamResult(new StringWriter())), "Transformer", TransformerException.class); } @@ -425,41 +516,18 @@ static void assertTransformerBlocks(final String payload) { * <p>{@link Transformer#transform(Source, javax.xml.transform.Result)} via {@link XmlFactories#newTransformerFactory()}; Saxon's TrAX path may swallow * internal errors and produce empty output.</p> */ - static void assertTransformerDoesNotLeak(final String payload) { - assertNoLeak(() -> { - final StringWriter sink = new StringWriter(); - XmlFactories.newTransformerFactory().newTransformer().transform(streamSource(payload), new StreamResult(sink)); - return sink.toString(); - }, "Transformer", TransformerException.class); + static void assertTransformerBlocksOrDoesNotLeak(final String payload) { + assertNoLeakOrThrows(() -> identityTransformAndCapture(payload), "Transformer", TransformerException.class); } /** - * Asserts a permissive identity Transformer succeeds. + * Asserts a hardened identity Transformer completes without throwing and without leaked content. * - * <p>{@link Transformer#transform(Source, javax.xml.transform.Result)} via {@link TransformerFactory#newInstance()} with FSP off; positive control proving - * the payload is well-formed.</p> + * <p>{@link Transformer#transform(Source, javax.xml.transform.Result)} via {@link XmlFactories#newTransformerFactory()}; use this when the hardening + * contract guarantees the transform succeeds but never resolves the external resource.</p> */ - 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 a permissive Validator validation succeeds. - * - * <p>{@link Validator#validate(Source)} on a validator from {@link #BENIGN_SCHEMA} compiled via {@link SchemaFactory#newInstance(String)} with FSP off; - * positive control proving the instance is well-formed.</p> - */ - 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(BENIGN_SCHEMA)).newValidator().validate(streamSource(xml)); - }, "Validator"); + static void assertTransformerDoesNotLeak(final String payload) { + assertNoLeakStrict(() -> identityTransformAndCapture(payload), "Transformer"); } /** @@ -470,27 +538,39 @@ static void assertValidatorAccepts(final String xml) { */ static void assertValidatorBlocks(final String xml) { assertParseFails( - () -> XmlFactories.newSchemaFactory().newSchema(streamSource(BENIGN_SCHEMA)).newValidator().validate(streamSource(xml)), + () -> strictValidator(strictSchema(XmlFactories.newSchemaFactory(), streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)), "Validator", SAXException.class, SecurityException.class); } + /** + * Asserts a hardened Validator validation completes without throwing. + * + * <p>{@link Validator#validate(Source)} on a validator from {@link #BENIGN_SCHEMA} compiled via {@link XmlFactories#newSchemaFactory()}; use this when the + * hardening contract guarantees the validate succeeds but never resolves the external resource.</p> + */ + static void assertValidatorDoesNotLeak(final String xml) { + assertParseSucceeds( + () -> strictValidator(strictSchema(XmlFactories.newSchemaFactory(), streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)), + "Validator"); + } + /** * Asserts a hardened-in-place XMLReader parse of the payload throws. * * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via {@link XmlFactories#harden(XMLReader)}; only a thrown exception passes.</p> */ static void assertXmlReaderBlocks(final String payload) { - assertParseFails(() -> parseQuietly(rawHardenedReader(), payload), "XMLReader", SAXException.class); + assertParseFails(() -> consumeXmlReader(rawHardenedReader(), payload), "XMLReader", SAXException.class); } /** - * Asserts a hardened-in-place XMLReader parse either throws or completes without leaked content. + * Asserts a hardened-in-place XMLReader parse completes without throwing and without leaked content. * - * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via {@link XmlFactories#harden(XMLReader)}; XML 1.0 §4.1 permits silent skipping of - * undeclared entities when an external subset is declared but unread.</p> + * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via {@link XmlFactories#harden(XMLReader)}; use this when the hardening contract + * guarantees the parse succeeds but never resolves the external resource.</p> */ static void assertXmlReaderDoesNotLeak(final String payload) { - assertNoLeak(() -> captureCharacters(rawHardenedReader(), payload), "XMLReader", SAXException.class); + assertNoLeakStrict(() -> captureCharacters(rawHardenedReader(), payload), "XMLReader"); } /** @@ -500,7 +580,7 @@ static void assertXmlReaderDoesNotLeak(final String payload) { * payloads.</p> */ static void assertXmlReaderParses(final String payload) { - assertParseSucceeds(() -> parseQuietly(rawHardenedReader(), payload), "XMLReader"); + assertParseSucceeds(() -> consumeXmlReader(rawHardenedReader(), payload), "XMLReader"); } /** @@ -522,8 +602,7 @@ public void characters(final char[] ch, final int start, final int length) { text.append(ch, start, length); } }); - reader.setErrorHandler(new DefaultHandler()); - reader.parse(inputSource(payload)); + strictXMLReader(reader).parse(inputSource(payload)); return text.toString(); } @@ -551,27 +630,42 @@ private static void consumeStreamReader(final XMLInputFactory factory, final Str } } - /** Builds an {@link InputSource} backed by a {@link StringReader} over the payload. */ - static InputSource inputSource(final String xml) { - return new InputSource(new StringReader(xml)); - } - /** Parses the payload through the supplied reader, discarding events; used by the SAX-based {@code Parses} / {@code Blocks} helpers. */ - private static void parseQuietly(final XMLReader reader, final String payload) throws Exception { + private static void consumeXmlReader(final XMLReader reader, final String payload) throws Exception { reader.setContentHandler(new DefaultHandler()); - reader.setErrorHandler(new DefaultHandler()); - reader.parse(inputSource(payload)); + strictXMLReader(reader).parse(inputSource(payload)); } - /** Builds a {@link SAXSource} wrapping the payload, parsed by a deliberately permissive SAX parser; used by the unconfigured-side TrAX controls. */ - private static SAXSource permissiveSaxSource(final String xml) throws ParserConfigurationException, org.xml.sax.SAXException { - final SAXParserFactory parserFactory = SAXParserFactory.newInstance(); - if (!IS_ANDROID) { - parserFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); + private static String domParseAndCaptureText(final String payload) throws Exception { + final Document doc = strictDocumentBuilder(XmlFactories.newDocumentBuilderFactory()).parse(inputSource(payload)); + if (doc.getDocumentElement() == null) { + return ""; } - final XMLReader reader = parserFactory.newSAXParser().getXMLReader(); + // Harmony's DOM returns null from getTextContent() on an element whose only children are unresolved EntityReference nodes. + final String text = doc.getDocumentElement().getTextContent(); + return text == null ? "" : text; + } + + private static String identityTransformAndCapture(final String payload) throws TransformerException { + final StringWriter sink = new StringWriter(); + strictTransformer(XmlFactories.newTransformerFactory()).transform(streamSource(payload), new StreamResult(sink)); + return sink.toString(); + } + + /** Builds an {@link InputSource} backed by a {@link StringReader} over the payload. */ + static InputSource inputSource(final String xml) { + return new InputSource(new StringReader(xml)); + } + + /** + * Builds a {@link SAXSource} wrapping the payload, without an explicit parser; used by the unconfigured-side TrAX controls. + */ + private static SAXSource permissiveSaxSource(final String xml) { + final SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + final XMLReader reader = strictXMLReader(factory); suppressException(() -> reader.setProperty(JDK_ENTITY_EXPANSION_LIMIT, "0")); - return new SAXSource(reader, new InputSource(new StringReader(xml))); + return new SAXSource(IS_ANDROID ? new AndroidProvider.GuardedXMLReader(reader) : reader, new InputSource(new StringReader(xml))); } private static boolean probeAndroid() { @@ -621,7 +715,81 @@ static URL resourceUrl(final String name) { /** Builds a {@link StreamSource} backed by a {@link StringReader} over the payload. */ static StreamSource streamSource(final String xml) { - return new StreamSource(new StringReader(xml)); + StreamSource streamSource = new StreamSource(new StringReader(xml)); + streamSource.setSystemId("test:fixture"); + return streamSource; + } + + /** + * Builds a {@link DocumentBuilder} from {@code factory} with {@link #STRICT_REPORTER} installed as its error handler. + */ + private static DocumentBuilder strictDocumentBuilder(final DocumentBuilderFactory factory) throws ParserConfigurationException { + final DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setErrorHandler(STRICT_REPORTER); + return builder; + } + + /** + * Compiles {@code xsds} into a {@link Schema} using {@code factory}, with {@link #STRICT_REPORTER} installed on the factory before compile. + */ + private static Schema strictSchema(final SchemaFactory factory, final Source... xsds) throws SAXException { + factory.setErrorHandler(STRICT_REPORTER); + return factory.newSchema(xsds); + } + + /** + * Compiles {@code xslt} into a {@link Templates} using {@code factory}, with {@link #STRICT_REPORTER} installed on the factory before compile. + */ + private static Templates strictTemplates(final TransformerFactory factory, final Source xslt) throws TransformerConfigurationException { + factory.setErrorListener(STRICT_REPORTER); + return factory.newTemplates(xslt); + } + + /** + * Builds an identity {@link Transformer} from {@code factory} with {@link #STRICT_REPORTER} installed on both the factory and the resulting transformer. + */ + private static Transformer strictTransformer(final TransformerFactory factory) throws TransformerConfigurationException { + factory.setErrorListener(STRICT_REPORTER); + final Transformer transformer = factory.newTransformer(); + transformer.setErrorListener(STRICT_REPORTER); + return transformer; + } + + /** + * Builds a {@link Transformer} from {@code templates} with {@link #STRICT_REPORTER} installed as its error listener. + */ + private static Transformer strictTransformer(final Templates templates) throws TransformerConfigurationException { + final Transformer transformer = templates.newTransformer(); + transformer.setErrorListener(STRICT_REPORTER); + return transformer; + } + + /** + * Builds a {@link Validator} from {@code schema} with {@link #STRICT_REPORTER} installed as its error handler. + */ + private static Validator strictValidator(final Schema schema) { + final Validator validator = schema.newValidator(); + validator.setErrorHandler(STRICT_REPORTER); + return validator; + } + + /** + * Builds an {@link XMLReader} from {@code factory} with {@link #STRICT_REPORTER} installed as its error handler. + */ + private static XMLReader strictXMLReader(final SAXParserFactory factory) { + try { + return strictXMLReader(factory.newSAXParser().getXMLReader()); + } catch (ParserConfigurationException | SAXException e) { + throw new AssertionError("Failed to create permissive XMLReader", e); + } + } + + /** + * Installs {@link #STRICT_REPORTER} as the error handler on {@code reader} and returns it; for raw-reader paths. + */ + private static XMLReader strictXMLReader(final XMLReader reader) { + reader.setErrorHandler(STRICT_REPORTER); + return reader; } /** Runs the action and silently swallows any thrown exception; used to apply best-effort permissive-side flags that may not be supported. */ @@ -633,6 +801,16 @@ private static void suppressException(final Executable action) { } } + private static String templatesCompileAndTransform(final Source xslt) throws TransformerException { + final StringWriter sink = new StringWriter(); + final Templates templates = strictTemplates(XmlFactories.newTransformerFactory(), xslt); + // Xalan returns `null` if the template fails + if (templates != null) { + strictTransformer(templates).transform(streamSource("<root/>"), new StreamResult(sink)); + } + return sink.toString(); + } + /** XML body wrapping the supplied text in a benign {@code <root>/<child>} element pair, validated by {@link #BENIGN_SCHEMA}. */ static String xmlBody(final String text) { return "<root><child>" + text + "</child></root>"; diff --git a/src/test/java/org/apache/commons/xml/factory/BillionLaughsTest.java b/src/test/java/org/apache/commons/xml/factory/BillionLaughsTest.java index accae35..23ae725 100644 --- a/src/test/java/org/apache/commons/xml/factory/BillionLaughsTest.java +++ b/src/test/java/org/apache/commons/xml/factory/BillionLaughsTest.java @@ -33,8 +33,7 @@ * property name, so it parses the ~4 KB of expanded {@code "A"} text and finishes immediately.</li> * <li>The <strong>large</strong> fixture nests seven levels of 10x expansion ({@code lol1} through {@code lol7}), so resolving {@code &lol7;} produces * {@code 10^7 = 10 000 000} leaf expansions, ~10 MB of {@code "A"} text, and an amplification factor in the tens of thousands. Sized to trip - * libexpat >= 2.4's built-in billion-laughs check (8 MiB activation threshold and 100x amplification factor); used by the hardened DOM/SAX/XmlReader - * tests on Android, where the JDK count limit is unavailable and only libexpat's native check stands between the parser and the expansion.</li> + * libexpat >= 2.4's built-in billion-laughs check (8 MiB activation threshold and 100x amplification factor).</li> * </ul> * * <p>Why a single character {@code "A"}: XSLTC compiles a stylesheet's expanded text into a JVM string constant, which is capped at 65535 bytes. A larger @@ -45,30 +44,16 @@ * <p>Which fixture each test uses:</p> * * <ul> - * <li>{@code hardenedSaxBlocks}, {@code hardenedXmlReaderBlocks}: large fixture on Android (libexpat is the only defence), medium fixture on JDK - * (entity-expansion count limit is sufficient). Selected by {@link AttackTestSupport#IS_ANDROID}.</li> - * <li>{@code hardenedDomBlocks} and {@code unconfiguredDomResolves}: gated on {@link AttackTestSupport#DOM_RESOLVES_INTERNAL_ENTITIES}. They run with the - * medium fixture on platforms whose DOM parser resolves user-defined entities (every JDK), and skip on platforms where it does not (Android with - * KXmlParser, where custom internal entities become unresolved {@code EntityReference} nodes and amplification cannot grow). When the probe lights up - * on a future Android the assertions will start running again without further changes.</li> - * <li>Every other test: medium fixture. {@code unconfigured*} positive controls cannot use the large fixture because libexpat's protection cannot be - * disabled from Java. {@code Templates} and {@code Transformer} tests cannot use it because of the XSLTC 60 KB cap. {@code Schema}, {@code StAX} and - * {@code Validator} hardened tests do not run on Android (no harmony {@code SchemaFactory} or {@code XMLInputFactory}), so the JDK count limit is the - * only relevant defence for them and the medium fixture is enough.</li> - * </ul> - * - * <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> + * <li>{@code hardened*} tests pull from {@code hardened*Payload} helpers: large fixture on Android (libexpat is the only defence), medium fixture on JDK + * (entity-expansion count limit is sufficient).</li> + * <li>{@code unconfigured*} positive controls pull from {@code medium*Payload} helpers regardless of platform: libexpat's billion-laughs check cannot be + * disabled from Java, so a permissive parse of the large fixture would still trip on Android.</li> * </ul> */ class BillionLaughsTest { + private static final String LARGE_CONTENT = "&lol7;"; + private static final String LARGE_DTD = " <!ENTITY lol \"A\">\n" + " <!ENTITY lol1 \"&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;\">\n" @@ -79,7 +64,7 @@ class BillionLaughsTest { + " <!ENTITY lol6 \"&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;\">\n" + " <!ENTITY lol7 \"&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;\">\n"; - private static final String LARGE_CONTENT = "&lol7;"; + private static final String MEDIUM_CONTENT = "&lol3;"; private static final String MEDIUM_DTD = " <!ENTITY lol \"A\">\n" @@ -87,30 +72,89 @@ class BillionLaughsTest { + " <!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"; - private static final String MEDIUM_CONTENT = "&lol3;"; - /** - * Hardened-side payload for DOM/SAX/XmlReader: large on Android (libexpat amplification check), medium on JDK (entity-expansion count limit). + * Hardened-side payload for DOM/SAX/XmlReader + * + * <ul> + * <li>~10 MiB on Android to trip libexpats 8 MiB limit, and</li> + * <li>~4 KiB on JDK to trip JDK 25's {@code entityExpansionLimit = 2500}.</li> + * </ul> */ private static String hardenedXmlPayload() { - return AttackTestSupport.IS_ANDROID ? largeXmlPayload() : mediumXmlPayload(); + return AttackTestSupport.IS_ANDROID + ? withDoctype("root", LARGE_DTD, AttackTestSupport.xmlBody(LARGE_CONTENT)) + : mediumXmlPayload(); } /** - * ~10 MB of expanded {@code "A"}; trips libexpat >= 2.4's billion-laughs check. Not usable for Templates/Transformer (XSLTC 60 KB cap) or for any - * unconfigured positive control. + * Hardened-side XSD payload + * + * <ul> + * <li>~10 MiB on Android to trip libexpats 8 MiB limit, and</li> + * <li>~4 KiB on JDK to trip JDK 25's {@code entityExpansionLimit = 2500}.</li> + * </ul> */ - private static String largeXmlPayload() { - return withDoctype("root", LARGE_DTD, AttackTestSupport.xmlBody(LARGE_CONTENT)); + private static String hardenedXsdPayload() { + return AttackTestSupport.IS_ANDROID + ? withDoctype("xs:schema", LARGE_DTD, AttackTestSupport.xsdBody(LARGE_CONTENT)) + : mediumXsdPayload(); } /** - * ~4 KB of expanded {@code "A"}; trips JDK 25's {@code entityExpansionLimit = 2500}. Universal payload that stays under XSLTC's 60 KB constant-pool cap. + * Hardened-side XSLT payload + * + * <ul> + * <li>~10 MiB on Android to trip libexpats 8 MiB limit, and</li> + * <li>~4 KiB on JDK to trip JDK 25's {@code entityExpansionLimit = 2500}, and</li> + * <li>stay under XSLTC's 60 KB constant-pool cap, and</li> + * </ul> + */ + private static String hardenedXsltPayload() { + return AttackTestSupport.IS_ANDROID + ? withDoctype("xsl:stylesheet", LARGE_DTD, AttackTestSupport.xsltBody(LARGE_CONTENT)) + : mediumXsltPayload(); + } + + /** + * Unconfigured-side payload for DOM/SAX/XmlReader + * + * <p>~4 KB of expanded {@code "A"}, which:</p> + * <ul> + * <li>trips JDK 25's {@code entityExpansionLimit = 2500}, but</li> + * <li>does not trip libexpat's immutable 8 MiB limit.</li> + * </ul> */ private static String mediumXmlPayload() { return withDoctype("root", MEDIUM_DTD, AttackTestSupport.xmlBody(MEDIUM_CONTENT)); } + /** + * Unconfigured-side XSD payload + * + * <p>~4 KB of expanded {@code "A"}, which:</p> + * <ul> + * <li>trips JDK 25's {@code entityExpansionLimit = 2500}, but</li> + * <li>does not trip libexpat's immutable 8 MiB limit.</li> + * </ul> + */ + private static String mediumXsdPayload() { + return withDoctype("xs:schema", MEDIUM_DTD, AttackTestSupport.xsdBody(MEDIUM_CONTENT)); + } + + /** + * Unconfigured-side XSLT payload + * + * <p>~4 KB of expanded {@code "A"}, which:</p> + * <ul> + * <li>trips JDK 25's {@code entityExpansionLimit = 2500}, but</li> + * <li>stays under XSLTC's 60 KB constant-pool cap, and</li> + * <li>does not trip libexpat's immutable 8 MiB limit.</li> + * </ul> + */ + private static String mediumXsltPayload() { + return withDoctype("xsl:stylesheet", MEDIUM_DTD, AttackTestSupport.xsltBody(MEDIUM_CONTENT)); + } + private static String withDoctype(final String rootQName, final String dtd, final String body) { return "<?xml version=\"1.0\"?>\n" + "<!DOCTYPE " + rootQName + " [\n" @@ -119,14 +163,6 @@ private static String withDoctype(final String rootQName, final String dtd, fina + body + "\n"; } - private static String xsdPayload() { - return withDoctype("xs:schema", MEDIUM_DTD, AttackTestSupport.xsdBody(MEDIUM_CONTENT)); - } - - private static String xsltPayload() { - return withDoctype("xsl:stylesheet", MEDIUM_DTD, AttackTestSupport.xsltBody(MEDIUM_CONTENT)); - } - @Test @Tag("dom") void hardenedDomBlocks() { @@ -144,31 +180,31 @@ void hardenedSaxBlocks() { @Test @Tag("schema") void hardenedSchemaBlocks() { - AttackTestSupport.assertSchemaBlocks(AttackTestSupport.streamSource(xsdPayload())); + AttackTestSupport.assertSchemaBlocks(AttackTestSupport.streamSource(hardenedXsdPayload())); } @Test @Tag("stax") void hardenedStaxBlocks() { - AttackTestSupport.assertStaxBlocks(mediumXmlPayload()); + AttackTestSupport.assertStaxBlocks(hardenedXmlPayload()); } @Test @Tag("trax") - void hardenedTemplatesDoesNotLeak() { - AttackTestSupport.assertTemplatesDoesNotLeak(AttackTestSupport.streamSource(xsltPayload())); + void hardenedTemplatesBlocks() { + AttackTestSupport.assertTemplatesBlocks(AttackTestSupport.streamSource(hardenedXsltPayload())); } @Test @Tag("trax") - void hardenedTransformerDoesNotLeak() { - AttackTestSupport.assertTransformerDoesNotLeak(mediumXmlPayload()); + void hardenedTransformerBlocks() { + AttackTestSupport.assertTransformerBlocks(hardenedXmlPayload()); } @Test @Tag("schema") void hardenedValidatorBlocks() { - AttackTestSupport.assertValidatorBlocks(mediumXmlPayload()); + AttackTestSupport.assertValidatorBlocks(hardenedXmlPayload()); } @Test @@ -179,45 +215,45 @@ void hardenedXmlReaderBlocks() { @Test @Tag("dom") - void unconfiguredDomResolves() { + void unconfiguredDomParses() { Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES, "Skipped: platform DOM does not resolve user-defined entities"); - AttackTestSupport.assertDomResolves(mediumXmlPayload()); + AttackTestSupport.assertPermissiveDomParses(mediumXmlPayload()); } @Test @Tag("sax") - void unconfiguredSaxResolves() { - AttackTestSupport.assertSaxResolves(mediumXmlPayload()); + void unconfiguredSaxParses() { + AttackTestSupport.assertPermissiveSaxParses(mediumXmlPayload()); } @Test @Tag("schema") void unconfiguredSchemaCompiles() { - AttackTestSupport.assertSchemaCompiles(AttackTestSupport.streamSource(xsdPayload())); + AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.streamSource(mediumXsdPayload())); } @Test @Tag("stax") - void unconfiguredStaxResolves() { - AttackTestSupport.assertStaxResolves(mediumXmlPayload()); + void unconfiguredStaxParses() { + AttackTestSupport.assertPermissiveStaxParses(mediumXmlPayload()); } @Test @Tag("trax") void unconfiguredTemplatesCompiles() { - AttackTestSupport.assertTemplatesCompiles(xsltPayload()); + AttackTestSupport.assertPermissiveTemplatesCompiles(mediumXsltPayload()); } @Test @Tag("trax") - void unconfiguredTransformerSucceeds() { - AttackTestSupport.assertTransformerSucceeds(mediumXmlPayload()); + void unconfiguredTransformerTransforms() { + AttackTestSupport.assertPermissiveTransformerTransforms(mediumXmlPayload()); } @Test @Tag("schema") - void unconfiguredValidatorAccepts() { - AttackTestSupport.assertValidatorAccepts(mediumXmlPayload()); + void unconfiguredValidatorValidates() { + AttackTestSupport.assertPermissiveValidatorValidates(mediumXmlPayload()); } } diff --git a/src/test/java/org/apache/commons/xml/factory/DoctypeOnlyTest.java b/src/test/java/org/apache/commons/xml/factory/DoctypeOnlyTest.java index 451a2c8..6df4cf9 100644 --- a/src/test/java/org/apache/commons/xml/factory/DoctypeOnlyTest.java +++ b/src/test/java/org/apache/commons/xml/factory/DoctypeOnlyTest.java @@ -16,6 +16,7 @@ */ package org.apache.commons.xml.factory; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -45,6 +46,8 @@ private static String payload() { @Test @Tag("dom") void hardenedDomParses() { + Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES, + "Skipped: platform DOM does not resolve user-defined entities"); AttackTestSupport.assertDomParses(payload()); } diff --git a/src/test/java/org/apache/commons/xml/factory/ExternalDtdTest.java b/src/test/java/org/apache/commons/xml/factory/ExternalDtdTest.java index fd5e3cb..c164c20 100644 --- a/src/test/java/org/apache/commons/xml/factory/ExternalDtdTest.java +++ b/src/test/java/org/apache/commons/xml/factory/ExternalDtdTest.java @@ -75,8 +75,8 @@ void hardenedSaxDoesNotLeak() { @Test @Tag("schema") - void hardenedSchemaBlocks() { - AttackTestSupport.assertSchemaBlocks(AttackTestSupport.streamSource(xsdPayload())); + void hardenedSchemaDoesNotLeak() { + AttackTestSupport.assertSchemaDoesNotLeak(AttackTestSupport.streamSource(xsdPayload())); } @Test @@ -87,8 +87,8 @@ void hardenedStaxBlocks() { @Test @Tag("trax") - void hardenedTemplatesBlocks() { - AttackTestSupport.assertTemplatesBlocks(AttackTestSupport.streamSource(xsltPayload())); + void hardenedTemplatesDoesNotLeak() { + AttackTestSupport.assertTemplatesDoesNotLeak(AttackTestSupport.streamSource(xsltPayload())); } @Test @@ -99,8 +99,8 @@ void hardenedTransformerDoesNotLeak() { @Test @Tag("schema") - void hardenedValidatorBlocks() { - AttackTestSupport.assertValidatorBlocks(xmlPayload()); + void hardenedValidatorDoesNotLeak() { + AttackTestSupport.assertValidatorDoesNotLeak(xmlPayload()); } @Test @@ -111,45 +111,45 @@ void hardenedXmlReaderDoesNotLeak() { @Test @Tag("dom") - void unconfiguredDomResolves() { + void unconfiguredDomParses() { Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES, "Skipped: platform DOM does not resolve user-defined entities"); - AttackTestSupport.assertDomResolves(xmlPayload()); + AttackTestSupport.assertPermissiveDomParses(xmlPayload()); } @Test @Tag("sax") - void unconfiguredSaxResolves() { - AttackTestSupport.assertSaxResolves(xmlPayload()); + void unconfiguredSaxParses() { + AttackTestSupport.assertPermissiveSaxParses(xmlPayload()); } @Test @Tag("schema") void unconfiguredSchemaCompiles() { - AttackTestSupport.assertSchemaCompiles(AttackTestSupport.streamSource(xsdPayload())); + AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.streamSource(xsdPayload())); } @Test @Tag("stax") - void unconfiguredStaxResolves() { - AttackTestSupport.assertStaxResolves(xmlPayload()); + void unconfiguredStaxParses() { + AttackTestSupport.assertPermissiveStaxParses(xmlPayload()); } @Test @Tag("trax") void unconfiguredTemplatesCompiles() { - AttackTestSupport.assertTemplatesCompiles(xsltPayload()); + AttackTestSupport.assertPermissiveTemplatesCompiles(xsltPayload()); } @Test @Tag("trax") - void unconfiguredTransformerSucceeds() { - AttackTestSupport.assertTransformerSucceeds(xmlPayload()); + void unconfiguredTransformerTransforms() { + AttackTestSupport.assertPermissiveTransformerTransforms(xmlPayload()); } @Test @Tag("schema") - void unconfiguredValidatorAccepts() { - AttackTestSupport.assertValidatorAccepts(xmlPayload()); + void unconfiguredValidatorValidates() { + AttackTestSupport.assertPermissiveValidatorValidates(xmlPayload()); } } diff --git a/src/test/java/org/apache/commons/xml/factory/ExternalGeneralEntityTest.java b/src/test/java/org/apache/commons/xml/factory/ExternalGeneralEntityTest.java index 08c13f5..b1e7896 100644 --- a/src/test/java/org/apache/commons/xml/factory/ExternalGeneralEntityTest.java +++ b/src/test/java/org/apache/commons/xml/factory/ExternalGeneralEntityTest.java @@ -113,45 +113,45 @@ void hardenedXmlReaderBlocks() { @Test @Tag("dom") - void unconfiguredDomResolves() { + void unconfiguredDomParses() { Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES, "Skipped: platform DOM does not resolve user-defined entities"); - AttackTestSupport.assertDomResolves(xmlPayload()); + AttackTestSupport.assertPermissiveDomParses(xmlPayload()); } @Test @Tag("sax") - void unconfiguredSaxResolves() { - AttackTestSupport.assertSaxResolves(xmlPayload()); + void unconfiguredSaxParses() { + AttackTestSupport.assertPermissiveSaxParses(xmlPayload()); } @Test @Tag("schema") void unconfiguredSchemaCompiles() { - AttackTestSupport.assertSchemaCompiles(AttackTestSupport.streamSource(xsdPayload())); + AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.streamSource(xsdPayload())); } @Test @Tag("stax") - void unconfiguredStaxResolves() { - AttackTestSupport.assertStaxResolves(xmlPayload()); + void unconfiguredStaxParses() { + AttackTestSupport.assertPermissiveStaxParses(xmlPayload()); } @Test @Tag("trax") void unconfiguredTemplatesCompiles() { - AttackTestSupport.assertTemplatesCompiles(xsltPayload()); + AttackTestSupport.assertPermissiveTemplatesCompiles(xsltPayload()); } @Test @Tag("trax") - void unconfiguredTransformerSucceeds() { - AttackTestSupport.assertTransformerSucceeds(xmlPayload()); + void unconfiguredTransformerTransforms() { + AttackTestSupport.assertPermissiveTransformerTransforms(xmlPayload()); } @Test @Tag("schema") - void unconfiguredValidatorAccepts() { - AttackTestSupport.assertValidatorAccepts(xmlPayload()); + void unconfiguredValidatorValidates() { + AttackTestSupport.assertPermissiveValidatorValidates(xmlPayload()); } } diff --git a/src/test/java/org/apache/commons/xml/factory/ExternalParameterEntityTest.java b/src/test/java/org/apache/commons/xml/factory/ExternalParameterEntityTest.java index 353011f..3f7571b 100644 --- a/src/test/java/org/apache/commons/xml/factory/ExternalParameterEntityTest.java +++ b/src/test/java/org/apache/commons/xml/factory/ExternalParameterEntityTest.java @@ -157,6 +157,8 @@ void hardenedSaxBlocks() { @Test @Tag("schema") void hardenedSchemaBlocks() { + Assumptions.assumeTrue(SAX_RESOLVES_PARAMETER_ENTITIES, + "Skipped: platform SAX parser does not invoke the entity resolver for parameter entities"); AttackTestSupport.assertSchemaBlocks(AttackTestSupport.streamSource(xsdPayload())); } @@ -169,18 +171,24 @@ void hardenedStaxBlocks() { @Test @Tag("trax") void hardenedTemplatesBlocks() { + Assumptions.assumeTrue(SAX_RESOLVES_PARAMETER_ENTITIES, + "Skipped: platform SAX parser does not invoke the entity resolver for parameter entities"); AttackTestSupport.assertTemplatesBlocks(AttackTestSupport.streamSource(xsltPayload())); } @Test @Tag("trax") void hardenedTransformerBlocks() { + Assumptions.assumeTrue(SAX_RESOLVES_PARAMETER_ENTITIES, + "Skipped: platform SAX parser does not invoke the entity resolver for parameter entities"); AttackTestSupport.assertTransformerBlocks(xmlPayload()); } @Test @Tag("schema") void hardenedValidatorBlocks() { + Assumptions.assumeTrue(SAX_RESOLVES_PARAMETER_ENTITIES, + "Skipped: platform SAX parser does not invoke the entity resolver for parameter entities"); AttackTestSupport.assertValidatorBlocks(xmlPayload()); } @@ -194,45 +202,45 @@ void hardenedXmlReaderBlocks() { @Test @Tag("dom") - void unconfiguredDomResolves() { + void unconfiguredDomParses() { Assumptions.assumeTrue(DOM_ACCEPTS_PARAMETER_ENTITIES, "Skipped: platform DOM does not accept parameter entities"); - AttackTestSupport.assertDomResolves(xmlPayload()); + AttackTestSupport.assertPermissiveDomParses(xmlPayload()); } @Test @Tag("sax") - void unconfiguredSaxResolves() { - AttackTestSupport.assertSaxResolves(xmlPayload()); + void unconfiguredSaxParses() { + AttackTestSupport.assertPermissiveSaxParses(xmlPayload()); } @Test @Tag("schema") void unconfiguredSchemaCompiles() { - AttackTestSupport.assertSchemaCompiles(AttackTestSupport.streamSource(xsdPayload())); + AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.streamSource(xsdPayload())); } @Test @Tag("stax") - void unconfiguredStaxResolves() { - AttackTestSupport.assertStaxResolves(xmlPayload()); + void unconfiguredStaxParses() { + AttackTestSupport.assertPermissiveStaxParses(xmlPayload()); } @Test @Tag("trax") void unconfiguredTemplatesCompiles() { - AttackTestSupport.assertTemplatesCompiles(xsltPayload()); + AttackTestSupport.assertPermissiveTemplatesCompiles(xsltPayload()); } @Test @Tag("trax") - void unconfiguredTransformerSucceeds() { - AttackTestSupport.assertTransformerSucceeds(xmlPayload()); + void unconfiguredTransformerTransforms() { + AttackTestSupport.assertPermissiveTransformerTransforms(xmlPayload()); } @Test @Tag("schema") - void unconfiguredValidatorAccepts() { - AttackTestSupport.assertValidatorAccepts(xmlPayload()); + void unconfiguredValidatorValidates() { + AttackTestSupport.assertPermissiveValidatorValidates(xmlPayload()); } } diff --git a/src/test/java/org/apache/commons/xml/factory/SchemaImportTest.java b/src/test/java/org/apache/commons/xml/factory/SchemaImportTest.java index aec609a..f9888db 100644 --- a/src/test/java/org/apache/commons/xml/factory/SchemaImportTest.java +++ b/src/test/java/org/apache/commons/xml/factory/SchemaImportTest.java @@ -35,6 +35,6 @@ void hardenedSchemaBlocks() { @Test void unconfiguredSchemaCompiles() { - AttackTestSupport.assertSchemaCompiles(AttackTestSupport.resourceSource(RESOURCE)); + AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.resourceSource(RESOURCE)); } } diff --git a/src/test/java/org/apache/commons/xml/factory/SchemaIncludeTest.java b/src/test/java/org/apache/commons/xml/factory/SchemaIncludeTest.java index f02dbf1..229f988 100644 --- a/src/test/java/org/apache/commons/xml/factory/SchemaIncludeTest.java +++ b/src/test/java/org/apache/commons/xml/factory/SchemaIncludeTest.java @@ -35,6 +35,6 @@ void hardenedSchemaBlocks() { @Test void unconfiguredSchemaCompiles() { - AttackTestSupport.assertSchemaCompiles(AttackTestSupport.resourceSource(RESOURCE)); + AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.resourceSource(RESOURCE)); } } diff --git a/src/test/java/org/apache/commons/xml/factory/SchemaRedefineTest.java b/src/test/java/org/apache/commons/xml/factory/SchemaRedefineTest.java index 734775a..5d10821 100644 --- a/src/test/java/org/apache/commons/xml/factory/SchemaRedefineTest.java +++ b/src/test/java/org/apache/commons/xml/factory/SchemaRedefineTest.java @@ -35,6 +35,6 @@ void hardenedSchemaBlocks() { @Test void unconfiguredSchemaCompiles() { - AttackTestSupport.assertSchemaCompiles(AttackTestSupport.resourceSource(RESOURCE)); + AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.resourceSource(RESOURCE)); } }
