From d9a6214e5f6772ac9e4bf1a65643459c91a73e9c Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
* The same guarantees apply to {@link javax.xml.validation.Validator} and {@link javax.xml.validation.ValidatorHandler} instances produced from the @@ -58,8 +61,8 @@ public final class SecureSchemaFactory { /** * Capability-driven secure wrapper for any {@link SchemaFactory} on the classpath, the same recipe for every implementation. It is the entry point reached - * by {@link SecureSchemaFactory#newInstance(String)}; there is no per-implementation branching, no {@code FEATURE_SECURE_PROCESSING} and no limit configuration on the - * factory itself. + * by {@link SecureSchemaFactory#newInstance(String)}; there is no per-implementation branching and no limit configuration on the factory itself beyond + * {@code FEATURE_SECURE_PROCESSING}. * *
Three layers cooperate:
** The secure reader supplied by {@link SecureSAXParserFactory#secure(Source, boolean)} already carries {@code FEATURE_SECURE_PROCESSING} and the processing limits, so a - * DOCTYPE, external entity or Billion Laughs payload in the schema or instance document is bounded there rather than on this factory. The JAXP 1.5 - * {@code ACCESS_EXTERNAL_*} properties are deliberately not set: the resolver floor already blocks the same fetches on every implementation, and the JDK 8 - * {@code SchemaFactory} has a bug whereby those properties keep blocking even when a caller's own resolver would grant the access. The floor is a non-removable + * DOCTYPE, external entity or Billion Laughs payload in the schema or instance document is bounded there rather than on this factory. One limit it cannot + * supply is content-model expansion: a large {@code maxOccurs} is expanded by the schema loader when it builds the DFA, after parsing and without the + * reader, so {@code FEATURE_SECURE_PROCESSING} is set on the factory as well, which is what installs that bound on external Xerces (the stock JDK applies + * it unconditionally). The JAXP 1.5 {@code ACCESS_EXTERNAL_*} properties are still not set explicitly: the resolver floor already blocks the same fetches on + * every implementation, and the JDK 8 {@code SchemaFactory} has a bug whereby those properties keep blocking even when a caller's own resolver would grant + * the access. The floor is a non-removable * lower bound: a caller-set {@link LSResourceResolver} is routed through it (opting a specific lookup in by returning a non-{@code null} result) rather than * replacing it, so secure cannot be dropped by swapping the resolver. *
@@ -97,6 +103,8 @@ private static final class Wrapper extends SchemaFactory { */ private Wrapper(final SchemaFactory delegate) { this.delegate = Objects.requireNonNull(delegate, "delegate"); + // Content-model expansion happens in the schema loader, after parsing, so the injected reader's limits cannot reach it. + SecureSchemaFactory.setFeature(delegate, XMLConstants.FEATURE_SECURE_PROCESSING, true); // Compile-time block for xs:import/include/redefine; the wrappers carry the rest (per-product resolver, source rewriting, limits via the reader). delegate.setResourceResolver(floor); } @@ -261,10 +269,10 @@ public static SchemaFactory newInstance(final String schemaLanguage, final Strin /** * Secures a {@link SchemaFactory}. * - *Unlike the other factory types there is no per-implementation branching and no feature or limit configuration on the factory itself: schema compilation - * and validation reach external resources only through the resolver hook, so wrapping the factory with a non-removable ignore-all resolver floor is enough on - * every implementation. The reader used to parse schema and instance documents is secure separately, through - * {@link SecureSAXParserFactory#secure(javax.xml.transform.Source, boolean)}.
+ *Unlike the other factory types there is no per-implementation branching: schema compilation and validation reach external resources only through the + * resolver hook, so wrapping the factory with a non-removable ignore-all resolver floor is enough on every implementation. The reader used to parse schema + * and instance documents is secure separately, through {@link SecureSAXParserFactory#secure(javax.xml.transform.Source, boolean)}; the factory carries + * {@code FEATURE_SECURE_PROCESSING} for the one limit that reader cannot supply, the loader's content-model expansion.
* * @param factory the factory to secure; never {@code null}. * @return a secure factory. @@ -273,6 +281,22 @@ static SchemaFactory secure(final SchemaFactory factory) { return new Wrapper(factory); } + /** + * Sets a feature on the delegate, failing closed: an implementation that cannot accept it yields no factory rather than an unsecured one. + * + * @param factory the factory to configure; never {@code null}. + * @param feature the feature name. + * @param value the value to set. + * @throws SecureException if the implementation rejects the feature. + */ + private static void setFeature(final SchemaFactory factory, final String feature, final boolean value) { + try { + factory.setFeature(feature, value); + } catch (final Exception e) { + throw SecureException.featureFailed(feature, factory, e); + } + } + private SecureSchemaFactory() { // static only } diff --git a/src/test/java/org/apache/commons/xml/secure/SchemaContentModelLimitTest.java b/src/test/java/org/apache/commons/xml/secure/SchemaContentModelLimitTest.java new file mode 100644 index 00000000..93245bf1 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/secure/SchemaContentModelLimitTest.java @@ -0,0 +1,94 @@ +/* + * 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.secure; + +import javax.xml.XMLConstants; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Checks that an untrusted schema's content-model expansion is bounded, the one processing limit no reader can supply. + * + *{@link BillionLaughsTest} covers entity expansion, which the secure reader injected into every {@code Source} bounds before a schema document reaches the + * loader. {@code maxOccurs} is a different mechanism: the loader expands a repeated particle into content-model nodes while building the DFA, which happens + * after parsing and never touches the reader. The bound for it is the schema implementation's own limit ({@code maxOccurLimit}, 3,000 nodes on Xerces), which + * external Xerces installs only when {@code FEATURE_SECURE_PROCESSING} is set on the {@link SchemaFactory}.
+ * + *The expansion is lazy on Xerces: {@code newSchema} returns in milliseconds whatever {@code maxOccurs} says, and the nodes are built on first validation. + * The payload therefore has to be validated, not just compiled, and the assertion accepts a rejection at either step. The repeated particle holds two elements + * so it cannot be collapsed into Xerces' compact repeating-leaf form, and {@link #MAX_OCCURS} clears both limits by little enough that an unbounded run still + * finishes, in seconds, rather than exhausting the heap.
+ */ +@Tag("schema") +class SchemaContentModelLimitTest { + + /** Above both recognized implementations' limits (3,000 nodes on Xerces, 5,000 on the stock JDK); an unbounded run still finishes in seconds. */ + private static final int MAX_OCCURS = 10_000; + + private static String maxOccursPayload() { + return "\n" + + "{@link BillionLaughsTest} covers entity expansion, which the secure reader injected into every {@code Source} bounds before a schema document reaches the * loader. {@code maxOccurs} is a different mechanism: the loader expands a repeated particle into content-model nodes while building the DFA, which happens