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() {
    }
}