blob: 63c07068b076f41d5ac05b10a1f3c99a1344f7fc [file]
/*
* 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.setOptionalProperty;
import static org.apache.commons.xml.JaxpSetters.trySetProperty;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
/**
* Capability-driven hardening for any {@link XMLInputFactory} (StAX) on the classpath.
*
* <p>Rather than branching on the implementation class, {@link #harden(XMLInputFactory)} consolidates the JDK Zephyr and Woodstox recipes into one pass that
* probes which properties each factory accepts and adapts:</p>
* <ul>
* <li><strong>Limits</strong>: applied best-effort by {@link Limits#tryApply(XMLInputFactory)}, which sets both the JDK and the Woodstox limit properties;
* each implementation honours its own and rejects the other's.</li>
* <li><strong>External DTD subset</strong>: skipped via Zephyr's {@value #ZEPHYR_IGNORE_EXTERNAL_DTD} (best-effort), so a DOCTYPE-only document parses
* without a fetch attempt instead of tripping the deny-all resolver below. Woodstox skips it through {@value #WSTX_DTD_RESOLVER} instead.</li>
* <li><strong>External entities</strong>: denied through a non-removable {@link Resolvers.FallbackDenyXMLResolver} floor on the entity-resolution hook,
* leaving the standard {@code SUPPORT_DTD} / {@code IS_SUPPORTING_EXTERNAL_ENTITIES} defaults untouched. Woodstox exposes fine-grained hooks, so when all
* three apply the factory is Woodstox: {@value #WSTX_DTD_RESOLVER} (empty external subset, but a thrown error on external parameter entities, which share
* that hook), {@value #WSTX_ENTITY_RESOLVER} (the floor, denying declared external general entities) and {@value #WSTX_UNDECLARED_ENTITY_RESOLVER}
* (silently drop undeclared references left by the skipped subset). Any factory that does not accept that trio (the JDK Zephyr, or an unrecognized
* implementation) instead gets the floor through {@code setXMLResolver}. Either way the factory is wrapped in a {@link HardeningXMLInputFactory} that
* routes a caller-set resolver through the floor rather than letting it replace the deny-all block.</li>
* </ul>
*/
final class StaxHardener {
/** Woodstox property: resolver consulted for the external DTD subset. */
static final String WSTX_DTD_RESOLVER = "com.ctc.wstx.dtdResolver";
/** Woodstox property: resolver consulted for declared external general entities. */
static final String WSTX_ENTITY_RESOLVER = "com.ctc.wstx.entityResolver";
/** Woodstox property: resolver consulted for undeclared entity references. */
static final String WSTX_UNDECLARED_ENTITY_RESOLVER = "com.ctc.wstx.undeclaredEntityResolver";
/** Zephyr property: skip external DTD subset loading entirely, so a DOCTYPE-only document parses without a fetch attempt. */
private static final String ZEPHYR_IGNORE_EXTERNAL_DTD = "http://java.sun.com/xml/stream/properties/ignore-external-dtd";
/**
* Woodstox DTD-subset floor: a {@link Resolvers.FallbackIgnoreXMLResolver} that returns the empty input for the external DTD subset (its inherited policy)
* but throws on external parameter entities.
*
* <p>Woodstox calls this hook with {@code entityName == null} for the subset and {@code entityName != null} for parameter-entity expansion (that
* discriminator is the 4th {@code resolveEntity} argument; the JDK Zephyr always passes {@code null} there). Applied best-effort, ignored by implementations
* that do not recognize the property.</p>
*/
private static final class DtdSubsetFloor extends Resolvers.FallbackIgnoreXMLResolver {
DtdSubsetFloor() {
super(null);
}
@Override
protected Object onUnresolved(final String publicID, final String systemID, final String baseURI, final String entityName) throws XMLStreamException {
// External parameter entity (entityName != null): deny, reusing the standard hardening message.
if (entityName != null) {
throw denied(publicID, systemID, baseURI, entityName);
}
// Subset (entityName == null): skip it with the empty input from the ignore floor.
return super.onUnresolved(publicID, systemID, baseURI, entityName);
}
}
static XMLInputFactory harden(final XMLInputFactory factory) {
// Optional, implementation-based: JDK limit properties or Woodstox limit properties.
Limits.tryApply(factory);
// Optional: Zephyr's StAX equivalent of XERCES_LOAD_EXTERNAL_DTD=false skips the external DTD subset entirely.
setOptionalProperty(factory, ZEPHYR_IGNORE_EXTERNAL_DTD, true);
// Each hook carries its own FallbackDenyXMLResolver floor; a caller can opt specific entities in through it, but cannot remove it (see
// HardeningXMLInputFactory, which routes a caller-set resolver into the floor rather than replacing it). The DTD-subset and undeclared-entity hooks skip
// (empty input) rather than deny on an unresolved lookup, so a DOCTYPE-only document still parses.
if (!(trySetProperty(factory, WSTX_DTD_RESOLVER, new DtdSubsetFloor())
&& trySetProperty(factory, WSTX_ENTITY_RESOLVER, new Resolvers.FallbackDenyXMLResolver(null))
&& trySetProperty(factory, WSTX_UNDECLARED_ENTITY_RESOLVER, new Resolvers.FallbackIgnoreXMLResolver(null)))) {
// Fallback (JDK Zephyr or unrecognized): the single resolver carries the deny-all floor.
factory.setXMLResolver(new Resolvers.FallbackDenyXMLResolver(null));
}
return new HardeningXMLInputFactory(factory);
}
private StaxHardener() {
}
}