Limits.java
/*
* 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.apache.commons.xml.JaxpSetters.setAttribute;
import static org.apache.commons.xml.JaxpSetters.setOptionalAttribute;
import static org.apache.commons.xml.JaxpSetters.setProperty;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.IntSupplier;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.stream.XMLInputFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.validation.SchemaFactory;
import org.xml.sax.XMLReader;
/**
* Processing limits applied to the XML parsers configured by this library.
*
* <p>The numeric defaults below are mirrored from the {@code secureValue} column of {@code jdk.xml.internal.XMLSecurityManager.Limit} in
* <strong>JDK 25</strong>.</p>
*
* <p>Each limit can be overridden at runtime via the same JDK system property the JDK itself honours, so applications already tuning the JDK parser get the
* same value applied to the bundled Xerces and Woodstox parsers without further configuration.</p>
*/
final class Limits {
/**
* Maximum number of attributes on a single XML element; JDK 25 secure value.
*
* <p>Override with system property {@value #SP_ELEMENT_ATTRIBUTE_LIMIT}.</p>
*
* <p>JDK 8 through 21 used a less restrictive value of {@code 10000}.</p>
*
* <p>Applied to: stock JDK and Woodstox (via {@value #WSTX_MAX_ATTRIBUTES_PER_ELEMENT}). External Xerces' {@code SecurityManager} does not expose a setter
* for this limit.</p>
*/
private static final int DEFAULT_ELEMENT_ATTRIBUTE_LIMIT = 200;
/**
* Maximum number of entity expansions in a single parse; JDK 25 secure value.
*
* <p>Override with system property {@value #SP_ENTITY_EXPANSION_LIMIT}.</p>
*
* <p>JDK 8 through 21 used a less restrictive value of {@code 64000}.</p>
*
* <p>Applied to: stock JDK, external Xerces (via {@code SecurityManager.setEntityExpansionLimit}) and Woodstox (via {@value #WSTX_MAX_ENTITY_COUNT}).</p>
*/
private static final int DEFAULT_ENTITY_EXPANSION_LIMIT = 2500;
/**
* Cumulative number of replacement characters generated by entity expansions in a single parse; JDK 25 secure value.
*
* <p>Override with system property {@value #SP_ENTITY_REPLACEMENT_LIMIT}.</p>
*
* <p>JDK 8 through 21 used a less restrictive value of {@code 3000000}.</p>
*
* <p>Applied to: stock JDK only. External Xerces' {@code SecurityManager} and Woodstox have no equivalent.</p>
*/
private static final int DEFAULT_ENTITY_REPLACEMENT_LIMIT = 100000;
/**
* Maximum size, in characters, of a single general entity replacement; JDK 25 secure value.
*
* <p>Override with system property {@value #SP_GENERAL_ENTITY_SIZE_LIMIT}.</p>
*
* <p>JDK 8 through 21 imposed no limit ({@code 0}).</p>
*
* <p>Applied to: stock JDK only. External Xerces' {@code SecurityManager} and Woodstox have no equivalent.</p>
*/
private static final int DEFAULT_GENERAL_ENTITY_SIZE_LIMIT = 100000;
/**
* Maximum nesting depth of XML elements; JDK 25 secure value.
*
* <p>Override with system property {@value #SP_MAX_ELEMENT_DEPTH}.</p>
*
* <p>JDK 8 through 21 imposed no limit ({@code 0}).</p>
*
* <p>Applied to: stock JDK and Woodstox (via {@value #WSTX_MAX_ELEMENT_DEPTH}). External Xerces' {@code SecurityManager} does not expose a setter for this
* limit.</p>
*/
private static final int DEFAULT_MAX_ELEMENT_DEPTH = 100;
/**
* Maximum length, in characters, of an XML name (element name, attribute name, namespace prefix, etc); JDK 25 secure value.
*
* <p>Override with system property {@value #SP_MAX_NAME_LIMIT}.</p>
*
* <p>Applied to: stock JDK only. External Xerces' {@code SecurityManager} and Woodstox have no equivalent.</p>
*/
private static final int DEFAULT_MAX_NAME_LIMIT = 1000;
/**
* Maximum number of XSD particles a {@code maxOccurs} attribute may be expanded to; JDK 25 secure value.
*
* <p>Override with system property {@value #SP_MAX_OCCUR_LIMIT}.</p>
*
* <p>Applied to: external Xerces (via {@code SecurityManager.setMaxOccurNodeLimit}). The stock JDK enforces this XSD limit internally via its own
* {@code XMLSecurityManager}; Woodstox is StAX-only and has no schema engine.</p>
*/
private static final int DEFAULT_MAX_OCCUR_LIMIT = 5000;
/**
* Maximum size, in characters, of a single parameter entity replacement (DTD only); JDK 25 secure value.
*
* <p>Override with system property {@value #SP_PARAMETER_ENTITY_SIZE_LIMIT}.</p>
*
* <p>JDK 8 through 21 used a less restrictive value of {@code 1000000}.</p>
*
* <p>Applied to: stock JDK only. External Xerces' {@code SecurityManager} and Woodstox have no equivalent.</p>
*/
private static final int DEFAULT_PARAMETER_ENTITY_SIZE_LIMIT = 15000;
/**
* Cumulative size, in characters, of all entity replacements in a single parse; JDK 25 secure value.
*
* <p>Override with system property {@value #SP_TOTAL_ENTITY_SIZE_LIMIT}.</p>
*
* <p>JDK 8 through 21 used a less restrictive value of {@code 50000000}.</p>
*
* <p>Applied to: stock JDK only. External Xerces' {@code SecurityManager} and Woodstox have no equivalent.</p>
*/
private static final int DEFAULT_TOTAL_ENTITY_SIZE_LIMIT = 100000;
/**
* URL-form property name to current value, in a stable order, for every limit that the stock JDK's parsers accept. Iterated by every {@code applyToJdk*}
* method so each JDK factory or parser instance ends up with the same set of limits applied.
*/
private static final Map<String, IntSupplier> JDK_LIMITS;
/**
* URL form of the JDK's per-element attribute limit; recognized across JDK 8 through 25.
*/
private static final String JDK_URL_ELEMENT_ATTRIBUTE_LIMIT = "http://www.oracle.com/xml/jaxp/properties/elementAttributeLimit";
/**
* URL form of the JDK's entity-expansion limit; recognized across JDK 8 through 25. The short form {@code jdk.xml.entityExpansionLimit} is JDK 11+ only.
*/
private static final String JDK_URL_ENTITY_EXPANSION_LIMIT = "http://www.oracle.com/xml/jaxp/properties/entityExpansionLimit";
/**
* URL form of the JDK's cumulative entity-replacement limit; recognized across JDK 8 through 25.
*/
private static final String JDK_URL_ENTITY_REPLACEMENT_LIMIT = "http://www.oracle.com/xml/jaxp/properties/entityReplacementLimit";
/**
* URL form of the JDK's per-general-entity size limit; recognized across JDK 8 through 25.
*/
private static final String JDK_URL_GENERAL_ENTITY_SIZE_LIMIT = "http://www.oracle.com/xml/jaxp/properties/maxGeneralEntitySizeLimit";
/**
* URL form of the JDK's maximum element depth; recognized across JDK 8 through 25.
*/
private static final String JDK_URL_MAX_ELEMENT_DEPTH = "http://www.oracle.com/xml/jaxp/properties/maxElementDepth";
/**
* URL form of the JDK's maximum XML name length; recognized across JDK 8 through 25.
*/
private static final String JDK_URL_MAX_NAME_LIMIT = "http://www.oracle.com/xml/jaxp/properties/maxXMLNameLimit";
/**
* URL form of the JDK's per-parameter-entity size limit; recognized across JDK 8 through 25.
*/
private static final String JDK_URL_PARAMETER_ENTITY_SIZE_LIMIT = "http://www.oracle.com/xml/jaxp/properties/maxParameterEntitySizeLimit";
/**
* URL form of the JDK's total entity-size limit; recognized across JDK 8 through 25.
*/
private static final String JDK_URL_TOTAL_ENTITY_SIZE_LIMIT = "http://www.oracle.com/xml/jaxp/properties/totalEntitySizeLimit";
/**
* JDK system property name for the per-element attribute limit.
*/
private static final String SP_ELEMENT_ATTRIBUTE_LIMIT = "jdk.xml.elementAttributeLimit";
/**
* JDK system property name for the entity-expansion limit; same name the JDK Zephyr/Xerces stack honours.
*/
private static final String SP_ENTITY_EXPANSION_LIMIT = "jdk.xml.entityExpansionLimit";
/**
* JDK system property name for the cumulative entity-replacement limit.
*/
private static final String SP_ENTITY_REPLACEMENT_LIMIT = "jdk.xml.entityReplacementLimit";
/**
* JDK system property name for the per-general-entity size limit.
*/
private static final String SP_GENERAL_ENTITY_SIZE_LIMIT = "jdk.xml.maxGeneralEntitySizeLimit";
/**
* JDK system property name for the maximum element depth.
*/
private static final String SP_MAX_ELEMENT_DEPTH = "jdk.xml.maxElementDepth";
/**
* JDK system property name for the maximum XML name length.
*/
private static final String SP_MAX_NAME_LIMIT = "jdk.xml.maxXMLNameLimit";
/**
* JDK system property name for the {@code maxOccurs} expansion limit.
*/
private static final String SP_MAX_OCCUR_LIMIT = "jdk.xml.maxOccurLimit";
/**
* JDK system property name for the per-parameter-entity size limit.
*/
private static final String SP_PARAMETER_ENTITY_SIZE_LIMIT = "jdk.xml.maxParameterEntitySizeLimit";
/**
* JDK system property name for the cumulative entity size limit.
*/
private static final String SP_TOTAL_ENTITY_SIZE_LIMIT = "jdk.xml.totalEntitySizeLimit";
/**
* Woodstox property: maximum number of attributes on a single XML element.
*/
private static final String WSTX_MAX_ATTRIBUTES_PER_ELEMENT = "com.ctc.wstx.maxAttributesPerElement";
/**
* Woodstox property: maximum nesting depth of XML elements.
*/
private static final String WSTX_MAX_ELEMENT_DEPTH = "com.ctc.wstx.maxElementDepth";
/**
* Woodstox property: maximum number of entity expansions in a single parse.
*/
private static final String WSTX_MAX_ENTITY_COUNT = "com.ctc.wstx.maxEntityCount";
/**
* Class name of the external Apache Xerces {@link DocumentBuilderFactory}, whose limits live on a {@code SecurityManager} rather than JDK attributes.
*/
private static final String EXTERNAL_XERCES_DOCUMENT_BUILDER_FACTORY = "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl";
static {
final Map<String, IntSupplier> map = new LinkedHashMap<>();
map.put(JDK_URL_ENTITY_EXPANSION_LIMIT, Limits::getEntityExpansionLimit);
map.put(JDK_URL_ELEMENT_ATTRIBUTE_LIMIT, Limits::getElementAttributeLimit);
map.put(JDK_URL_MAX_ELEMENT_DEPTH, Limits::getMaxElementDepth);
map.put(JDK_URL_TOTAL_ENTITY_SIZE_LIMIT, Limits::getTotalEntitySizeLimit);
map.put(JDK_URL_GENERAL_ENTITY_SIZE_LIMIT, Limits::getGeneralEntitySizeLimit);
map.put(JDK_URL_PARAMETER_ENTITY_SIZE_LIMIT, Limits::getParameterEntitySizeLimit);
map.put(JDK_URL_ENTITY_REPLACEMENT_LIMIT, Limits::getEntityReplacementLimit);
map.put(JDK_URL_MAX_NAME_LIMIT, Limits::getMaxNameLimit);
JDK_LIMITS = Collections.unmodifiableMap(map);
}
/**
* Best-effort application of the processing limits to a {@link DocumentBuilderFactory}, dispatched on the implementation.
*
* <p>External Xerces carries its limits on an {@code org.apache.xerces.util.SecurityManager} instance. Every other implementation (the stock JDK and any
* future attribute-based parser) takes the JDK limit attributes. Neither path throws if the implementation declines a limit.</p>
*
* @param factory The target factory to modify.
*/
static void tryApply(final DocumentBuilderFactory factory) {
if (EXTERNAL_XERCES_DOCUMENT_BUILDER_FACTORY.equals(factory.getClass().getName())) {
// Install a fresh SecurityManager pinned to JDK 25 limits, replacing Xerces' built-in caps which are looser than even JDK 8.
final Object securityManager = newSecurityManager();
applyToXerces(securityManager);
setAttribute(factory, XercesProvider.XERCES_SECURITY_MANAGER_PROPERTY, securityManager);
return;
}
// Pin the JDK attribute limits to JDK 25 secure values; skip silently any attribute the implementation does not recognize.
JDK_LIMITS.forEach((name, supplier) -> setOptionalAttribute(factory, name, Integer.toString(supplier.getAsInt())));
}
/**
* Sets every JDK-supported limit on a stock JDK {@link SchemaFactory}.
*
* @param factory The target factory to modify.
*/
static void applyToJdkSchema(final SchemaFactory factory) {
JDK_LIMITS.forEach((name, supplier) -> setProperty(factory, name, Integer.toString(supplier.getAsInt())));
}
/**
* Sets every JDK-supported limit on the stock JDK's {@link XMLInputFactory}.
*
* @param factory The target factory to modify.
*/
static void applyToJdkStax(final XMLInputFactory factory) {
JDK_LIMITS.forEach((name, supplier) -> setProperty(factory, name, Integer.toString(supplier.getAsInt())));
}
/**
* Sets every JDK-supported limit on a stock JDK {@link TransformerFactory}.
*
* @param factory The target factory to modify.
*/
static void applyToJdkTransformer(final TransformerFactory factory) {
JDK_LIMITS.forEach((name, supplier) -> setAttribute(factory, name, Integer.toString(supplier.getAsInt())));
}
/**
* Sets every JDK-supported limit on a stock JDK {@link XMLReader}.
*
* @param reader The target reader to modify.
*/
static void applyToJdkXmlReader(final XMLReader reader) {
JDK_LIMITS.forEach((name, supplier) -> setProperty(reader, name, Integer.toString(supplier.getAsInt())));
}
/**
* Sets every JDK-supported limit on a Woodstox {@link XMLInputFactory}.
*
* @param factory The target factory to modify.
*/
static void applyToWoodstox(final XMLInputFactory factory) {
setProperty(factory, WSTX_MAX_ENTITY_COUNT, getEntityExpansionLimit());
setProperty(factory, WSTX_MAX_ATTRIBUTES_PER_ELEMENT, getElementAttributeLimit());
setProperty(factory, WSTX_MAX_ELEMENT_DEPTH, getMaxElementDepth());
}
/**
* Sets every JDK-supported limit on a Xerces {@code org.apache.xerces.util.SecurityManager}.
*
* @param securityManager an instance of {@code org.apache.xerces.util.SecurityManager}; if {@code null} the call is a no-op.
*/
static void applyToXerces(final Object securityManager) {
if (securityManager == null) {
return;
}
try {
final Class<?> clazz = securityManager.getClass();
clazz.getMethod("setEntityExpansionLimit", int.class).invoke(securityManager, getEntityExpansionLimit());
clazz.getMethod("setMaxOccurNodeLimit", int.class).invoke(securityManager, getMaxOccurLimit());
} catch (final ReflectiveOperationException ignore) {
// Class on the classpath is not the expected Xerces SecurityManager; leave the limits at whatever defaults it carries.
}
}
private static Object newSecurityManager() {
try {
return Class.forName("org.apache.xerces.util.SecurityManager").getDeclaredConstructor().newInstance();
} catch (final ReflectiveOperationException e) {
throw new HardeningException("Failed to instantiate org.apache.xerces.util.SecurityManager; expected Xerces to be on the classpath", e);
}
}
private static int getElementAttributeLimit() {
return read(SP_ELEMENT_ATTRIBUTE_LIMIT, DEFAULT_ELEMENT_ATTRIBUTE_LIMIT);
}
private static int getEntityExpansionLimit() {
return read(SP_ENTITY_EXPANSION_LIMIT, DEFAULT_ENTITY_EXPANSION_LIMIT);
}
private static int getEntityReplacementLimit() {
return read(SP_ENTITY_REPLACEMENT_LIMIT, DEFAULT_ENTITY_REPLACEMENT_LIMIT);
}
private static int getGeneralEntitySizeLimit() {
return read(SP_GENERAL_ENTITY_SIZE_LIMIT, DEFAULT_GENERAL_ENTITY_SIZE_LIMIT);
}
private static int getMaxElementDepth() {
return read(SP_MAX_ELEMENT_DEPTH, DEFAULT_MAX_ELEMENT_DEPTH);
}
private static int getMaxNameLimit() {
return read(SP_MAX_NAME_LIMIT, DEFAULT_MAX_NAME_LIMIT);
}
private static int getMaxOccurLimit() {
return read(SP_MAX_OCCUR_LIMIT, DEFAULT_MAX_OCCUR_LIMIT);
}
private static int getParameterEntitySizeLimit() {
return read(SP_PARAMETER_ENTITY_SIZE_LIMIT, DEFAULT_PARAMETER_ENTITY_SIZE_LIMIT);
}
private static int getTotalEntitySizeLimit() {
return read(SP_TOTAL_ENTITY_SIZE_LIMIT, DEFAULT_TOTAL_ENTITY_SIZE_LIMIT);
}
private static int read(final String systemPropertyName, final int defaultValue) {
final String raw = System.getProperty(systemPropertyName);
if (raw == null || raw.isEmpty()) {
return defaultValue;
}
try {
return Integer.parseInt(raw.trim());
} catch (final NumberFormatException e) {
return defaultValue;
}
}
private Limits() {
}
}