| /* |
| * 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.trySetProperty; |
| |
| 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 org.xml.sax.SAXNotRecognizedException; |
| import org.xml.sax.SAXNotSupportedException; |
| 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; |
| /** |
| * 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"; |
| /** |
| * Class name of the external Apache Xerces {@link XMLReader}, whose limits live on a {@code SecurityManager} rather than JDK properties. |
| */ |
| private static final String EXTERNAL_XERCES_SAX_READER = "org.apache.xerces.jaxp.SAXParserImpl$JAXPSAXParser"; |
| /** |
| * 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"; |
| /** |
| * Xerces-specific property whose value is an {@code org.apache.xerces.util.SecurityManager} instance carrying processing-limit thresholds |
| */ |
| private static final String XERCES_SECURITY_MANAGER_PROPERTY = "http://apache.org/xml/properties/security-manager"; |
| |
| 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); |
| } |
| |
| /** |
| * 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 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 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 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; |
| } |
| } |
| |
| /** |
| * 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, 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()))); |
| } |
| |
| /** |
| * Best-effort application of the JDK processing limits to a {@link TransformerFactory}. |
| * |
| * <p>The stock JDK's XSLTC honours the JDK limit attributes, so this pins them to JDK 25 secure values; other implementations (Apache Xalan) reject the |
| * attributes and their caps come from {@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING} instead. Neither path throws if the implementation declines a |
| * limit.</p> |
| * |
| * @param factory The target factory to modify. |
| */ |
| static void tryApply(final TransformerFactory factory) { |
| JDK_LIMITS.forEach((name, supplier) -> setOptionalAttribute(factory, name, Integer.toString(supplier.getAsInt()))); |
| } |
| |
| /** |
| * Best-effort application of the processing limits to an {@link XMLInputFactory}, regardless of implementation. |
| * |
| * <p>The JDK's Zephyr honours the JDK URL limit properties; Woodstox honours its own {@code com.ctc.wstx.*} properties. Each implementation rejects the |
| * other's, so both sets are applied best-effort and the rejected ones are skipped silently.</p> |
| * |
| * @param factory The target factory to modify. |
| */ |
| static void tryApply(final XMLInputFactory factory) { |
| JDK_LIMITS.forEach((name, supplier) -> trySetProperty(factory, name, Integer.toString(supplier.getAsInt()))); |
| trySetProperty(factory, WSTX_MAX_ENTITY_COUNT, getEntityExpansionLimit()); |
| trySetProperty(factory, WSTX_MAX_ATTRIBUTES_PER_ELEMENT, getElementAttributeLimit()); |
| trySetProperty(factory, WSTX_MAX_ELEMENT_DEPTH, getMaxElementDepth()); |
| } |
| |
| /** |
| * Best-effort application of the processing limits to an {@link XMLReader}, dispatched on the implementation. |
| * |
| * <p>External Xerces carries its limits on an {@code org.apache.xerces.util.SecurityManager} instance reachable through the |
| * {@value #XERCES_SECURITY_MANAGER_PROPERTY} property. The stock JDK reader is itself a Xerces fork that exposes the same property, so a probe |
| * cannot tell the two apart; the external distribution is therefore matched by class name, and every other reader (the stock JDK, a Saxon-picked JDK reader, |
| * any future attribute-based parser) takes the JDK limit properties. Neither path throws if the reader declines a limit.</p> |
| * |
| * @param reader The target reader to modify. |
| */ |
| static void tryApply(final XMLReader reader) { |
| if (EXTERNAL_XERCES_SAX_READER.equals(reader.getClass().getName())) { |
| try { |
| // External Xerces: tighten the SecurityManager it already installed under FSP to JDK 25 limits. |
| applyToXerces(reader.getProperty(XERCES_SECURITY_MANAGER_PROPERTY)); |
| } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { |
| throw new HardeningException("Failed to read Xerces security manager from XMLReader", e); |
| } |
| return; |
| } |
| // Pin the JDK limit properties to JDK 25 secure values; skip silently any property the reader does not recognize. |
| JDK_LIMITS.forEach((name, supplier) -> trySetProperty(reader, name, Integer.toString(supplier.getAsInt()))); |
| } |
| |
| private Limits() { |
| } |
| } |