This is an automated email from the ASF dual-hosted git repository. ppkarwasz pushed a commit to branch feature/schema-factory-language in repository https://gitbox.apache.org/repos/asf/commons-xml.git
commit 26638d7f8e3f4153a4dfdb2d7690a27402e0df73 Author: Piotr P. Karwasz <[email protected]> AuthorDate: Sun Aug 2 20:30:42 2026 +0200 Add a schemaLanguage parameter to newSchemaFactory Mirror SchemaFactory.newInstance(String): the caller picks the schema language instead of the method hardcoding W3C XML Schema. The hardening is enforced through the LSResourceResolver hook and hardened source rewriting rather than any implementation-specific setting, so it holds for whichever schema language JAXP locates a factory for. Assisted-By: Claude Fable 5 <[email protected]> --- .../apache/commons/xml/HardeningSchemaFactory.java | 2 +- .../java/org/apache/commons/xml/XmlFactories.java | 13 +++++--- src/site/markdown/index.md | 3 +- .../org/apache/commons/xml/AttackTestSupport.java | 24 +++++++------- .../commons/xml/EntityResolverFloorTest.java | 5 +-- .../commons/xml/SchemaFactoryLanguageTest.java | 38 ++++++++++++++++++++++ .../org/apache/commons/xml/XmlFactoriesTest.java | 4 +-- 7 files changed, 67 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java index 537f40e..e76c965 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java @@ -31,7 +31,7 @@ /** * Capability-driven hardening wrapper for any {@link SchemaFactory} on the classpath, the same recipe for every implementation. It is the entry point reached - * by {@link XmlFactories#newSchemaFactory()}; there is no per-implementation branching, no {@code FEATURE_SECURE_PROCESSING} and no limit configuration on the + * by {@link XmlFactories#newSchemaFactory(String)}; there is no per-implementation branching, no {@code FEATURE_SECURE_PROCESSING} and no limit configuration on the * factory itself. * * <p>Three layers cooperate:</p> diff --git a/src/main/java/org/apache/commons/xml/XmlFactories.java b/src/main/java/org/apache/commons/xml/XmlFactories.java index 8d8a62f..31f33cd 100644 --- a/src/main/java/org/apache/commons/xml/XmlFactories.java +++ b/src/main/java/org/apache/commons/xml/XmlFactories.java @@ -145,7 +145,7 @@ public static SAXParserFactory newSAXParserFactory() { } /** - * Returns a fresh, hardened {@link SchemaFactory} configured for W3C XML Schema ({@link XMLConstants#W3C_XML_SCHEMA_NS_URI}). + * Returns a fresh, hardened {@link SchemaFactory} for the given schema language. * * <p>Beyond the three universal guarantees on {@link XmlFactories}:</p> * @@ -155,12 +155,17 @@ public static SAXParserFactory newSAXParserFactory() { * </ul> * * <p>The same guarantees apply to {@link javax.xml.validation.Validator} and {@link javax.xml.validation.ValidatorHandler} instances produced from the - * resulting {@link javax.xml.validation.Schema}.</p> + * resulting {@link javax.xml.validation.Schema}. They are enforced through the factory's {@link org.w3c.dom.ls.LSResourceResolver} hook and hardened + * source rewriting rather than any implementation-specific setting, so they hold for whichever schema language JAXP locates a factory for.</p> * + * @param schemaLanguage The schema language, as accepted by {@link SchemaFactory#newInstance(String)}; use + * {@link XMLConstants#W3C_XML_SCHEMA_NS_URI} for W3C XML Schema. * @return A hardened factory. + * @throws IllegalArgumentException if no implementation of the schema language is available. + * @throws NullPointerException if {@code schemaLanguage} is {@code null}. */ - public static SchemaFactory newSchemaFactory() { - return new HardeningSchemaFactory(SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)); + public static SchemaFactory newSchemaFactory(final String schemaLanguage) { + return new HardeningSchemaFactory(SchemaFactory.newInstance(schemaLanguage)); } /** diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 98cc59e..e018cbe 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -121,10 +121,11 @@ NodeList hits = (NodeList) XmlFactories.newXPathFactory() **W3C XML Schema validation** via `SchemaFactory`: ```java +import javax.xml.XMLConstants; import javax.xml.transform.stream.StreamSource; import org.apache.commons.xml.XmlFactories; -XmlFactories.newSchemaFactory() +XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI) .newSchema(new StreamSource(xsdStream)) .newValidator() .validate(new StreamSource(inputStream)); diff --git a/src/test/java/org/apache/commons/xml/AttackTestSupport.java b/src/test/java/org/apache/commons/xml/AttackTestSupport.java index 4c80d2e..20f5351 100644 --- a/src/test/java/org/apache/commons/xml/AttackTestSupport.java +++ b/src/test/java/org/apache/commons/xml/AttackTestSupport.java @@ -463,30 +463,30 @@ static void assertSaxParses(final String payload) { /** * Asserts a hardened Schema compilation throws. * - * <p>{@link SchemaFactory#newSchema(Source)} via {@link XmlFactories#newSchemaFactory()}; only a thrown exception passes.</p> + * <p>{@link SchemaFactory#newSchema(Source)} via {@link XmlFactories#newSchemaFactory(String)}; only a thrown exception passes.</p> */ static void assertSchemaBlocks(final Source xsd) { - assertParseFails(() -> strictSchema(XmlFactories.newSchemaFactory(), xsd), "Schema compile", SAXException.class, SecurityException.class); + assertParseFails(() -> strictSchema(XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI), xsd), "Schema compile", SAXException.class, SecurityException.class); } /** * Asserts a hardened Schema compilation succeeds. * - * <p>{@link SchemaFactory#newSchema(Source)} via {@link XmlFactories#newSchemaFactory()}; positive control for DOCTYPE-only payloads.</p> + * <p>{@link SchemaFactory#newSchema(Source)} via {@link XmlFactories#newSchemaFactory(String)}; positive control for DOCTYPE-only payloads.</p> */ static void assertSchemaCompiles(final Source xsd) { - assertParseSucceeds(() -> strictSchema(XmlFactories.newSchemaFactory(), xsd), "Schema compile"); + assertParseSucceeds(() -> strictSchema(XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI), xsd), "Schema compile"); } /** * 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 + * <p>{@link SchemaFactory#newSchema(Source)} via {@link XmlFactories#newSchemaFactory(String)}; use this when the hardening contract guarantees the compile * succeeds but never resolves the external resource (for example, {@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"); + assertParseSucceeds(() -> strictSchema(XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI), xsd), "Schema compile"); } /** @@ -594,36 +594,36 @@ static void assertTransformerTransforms(final String payload) { /** * Asserts a hardened Validator validation throws. * - * <p>{@link Validator#validate(Source)} on a validator from {@link #BENIGN_SCHEMA} compiled via {@link XmlFactories#newSchemaFactory()}; only a thrown + * <p>{@link Validator#validate(Source)} on a validator from {@link #BENIGN_SCHEMA} compiled via {@link XmlFactories#newSchemaFactory(String)}; only a thrown * exception passes (the schema is benign; the attack lives in the instance document).</p> */ static void assertValidatorBlocks(final String xml) { assertParseFails( - () -> strictValidator(strictSchema(XmlFactories.newSchemaFactory(), streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)), + () -> strictValidator(strictSchema(XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI), 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 + * <p>{@link Validator#validate(Source)} on a validator from {@link #BENIGN_SCHEMA} compiled via {@link XmlFactories#newSchemaFactory(String)}; 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)), + () -> strictValidator(strictSchema(XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI), streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)), "Validator"); } /** * Asserts a hardened Validator validation succeeds. * - * <p>{@link Validator#validate(Source)} on a validator from {@link #BENIGN_SCHEMA} compiled via {@link XmlFactories#newSchemaFactory()}; positive control + * <p>{@link Validator#validate(Source)} on a validator from {@link #BENIGN_SCHEMA} compiled via {@link XmlFactories#newSchemaFactory(String)}; positive control * for DOCTYPE-only payloads.</p> */ static void assertValidatorValidates(final String xml) { assertParseSucceeds( - () -> strictValidator(strictSchema(XmlFactories.newSchemaFactory(), streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)), + () -> strictValidator(strictSchema(XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI), streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)), "Validator"); } diff --git a/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java b/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java index 42a473f..9ef2d69 100644 --- a/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java +++ b/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java @@ -27,6 +27,7 @@ import java.io.StringReader; import java.net.URL; +import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.SAXParser; @@ -323,7 +324,7 @@ private static LSInput lsInput(final String systemId) { void schemaResolvesAllowListed() { // with-import.xsd references an element defined only in the imported included.xsd, so it compiles only if the import is resolved. assertParseSucceeds(() -> { - final SchemaFactory factory = XmlFactories.newSchemaFactory(); + final SchemaFactory factory = XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI); factory.setResourceResolver(SCHEMA_ALLOW_LIST); factory.newSchema(AttackTestSupport.resourceSource("with-import.xsd")); }, "Schema import via caller resolver"); @@ -333,7 +334,7 @@ void schemaResolvesAllowListed() { @Tag("schema") void schemaDeniesUnlisted() { assertParseFails(() -> { - final SchemaFactory factory = XmlFactories.newSchemaFactory(); + final SchemaFactory factory = XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI); factory.setResourceResolver((type, namespaceURI, publicId, systemId, baseURI) -> null); factory.newSchema(AttackTestSupport.resourceSource("with-import.xsd")); }, "Schema import", SAXException.class, SecurityException.class); diff --git a/src/test/java/org/apache/commons/xml/SchemaFactoryLanguageTest.java b/src/test/java/org/apache/commons/xml/SchemaFactoryLanguageTest.java new file mode 100644 index 0000000..80b50e2 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SchemaFactoryLanguageTest.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Checks that {@link XmlFactories#newSchemaFactory(String)} passes the schema language through to {@link javax.xml.validation.SchemaFactory#newInstance}. + * + * <p>The working W3C XML Schema path is exercised by the whole schema suite; this test covers only the language-selection contract.</p> + */ +@Tag("schema") +class SchemaFactoryLanguageTest { + + @Test + void unknownSchemaLanguageThrows() { + assertThrows(IllegalArgumentException.class, () -> XmlFactories.newSchemaFactory("urn:example:unknown-schema-language"), + "an unsupported schema language should surface SchemaFactory.newInstance's IllegalArgumentException"); + } +} diff --git a/src/test/java/org/apache/commons/xml/XmlFactoriesTest.java b/src/test/java/org/apache/commons/xml/XmlFactoriesTest.java index 8665e1c..e7d133a 100644 --- a/src/test/java/org/apache/commons/xml/XmlFactoriesTest.java +++ b/src/test/java/org/apache/commons/xml/XmlFactoriesTest.java @@ -113,8 +113,8 @@ void newXPathFactoryReturnsFreshInstance() throws Exception { @Test void newSchemaFactoryReturnsFreshInstance() throws Exception { - final SchemaFactory a = XmlFactories.newSchemaFactory(); - final SchemaFactory b = XmlFactories.newSchemaFactory(); + final SchemaFactory a = XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI); + final SchemaFactory b = XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI); assertNotSame(a, b); assertTrue(a.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); }
