blob: 3bf47eb576b3f8deccd9c50829a04190bc5f141c [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.setFeature;
import static org.apache.commons.xml.JaxpSetters.setOptionalFeature;
import static org.apache.commons.xml.JaxpSetters.trySetProperty;
import java.io.IOException;
import java.util.Objects;
import javax.xml.XMLConstants;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.LexicalHandler;
/**
* Capability-driven hardening for any {@link SAXParserFactory} on the classpath.
*
* <p>Rather than branching on the implementation class, {@link #harden(SAXParserFactory)} probes what the parser supports and adapts. Because
* {@link SAXParserFactory} exposes only a feature API and no property API, the per-parse configuration runs on each {@link XMLReader} the factory produces,
* funnelled through {@link HardeningSAXParserFactory} into {@link #hardenReader(XMLReader)}:</p>
* <ul>
* <li><strong>Android</strong> (Harmony / Expat): {@link XMLConstants#FEATURE_SECURE_PROCESSING FSP} and the JAXP 1.5 {@code ACCESS_EXTERNAL_*} properties
* are not recognized, and libexpat enforces its own Billion Laughs check, so neither is applied. Two fixups are still needed: a subset-aware deny-all
* resolver (Expat ignores external fetches silently when no resolver is set, so an explicit one is required to <em>fail</em> on external entities while
* still letting an unused external subset load), and an {@link ExpatReaderWrapper} so the unsupported {@code namespace-prefixes} feature is rejected at
* configuration time rather than mid-parse.</li>
* <li><strong>FSP</strong>: required on every other reader. It switches on the implementation's built-in security manager, which is what carries the
* processing limits.</li>
* <li><strong>{@code XERCES_LOAD_EXTERNAL_DTD}</strong>: optional. Where supported, it skips the external DTD subset on non-validating parsers so a
* DOCTYPE-only document parses without a fetch attempt. If not supported, the fetch will throw instead, due to the following settings.</li>
* <li><strong>Limits</strong>: applied best-effort by {@link Limits#tryApply(XMLReader)}, which adapts to the JDK limit properties or Xerces'
* {@code SecurityManager} as appropriate.</li>
* <li><strong>{@code ACCESS_EXTERNAL_*}</strong>: the dividing capability. Readers that honour it (the JDK-internal Xerces) block external fetches through
* the JAXP 1.5 properties and are returned as-is. Readers that reject it (the external Xerces distribution) are wrapped in a {@link HardeningXMLReader}
* that keeps a deny-all {@link EntityResolver} floor a caller-set resolver cannot remove.</li>
* </ul>
*/
final class SAXParserHardener {
/**
* Deny floor that additionally lets the external DTD subset declared by the DOCTYPE be skipped silently; merely <em>declaring</em> an external subset does
* not throw.
*
* <p>Android's Expat routes every external fetch (subset, DOCTYPE {@code SYSTEM}, general/parameter entity) through the 2-arg
* {@link EntityResolver#resolveEntity(String, String)}; a deny-all resolver there would also reject a DOCTYPE that merely <em>names</em> an unused external
* subset. As a {@link Resolvers.FallbackDenyResolver} it consults the caller's resolver first; as a {@link LexicalHandler} (via {@code DefaultHandler2}) it
* tracks the declared subset's identifiers so {@link #onUnresolved} can tell the subset apart from a forbidden external general or parameter entity. It is
* stateful, so a fresh instance is installed per reader.</p>
*/
private static final class DtdAwareDenyResolver extends Resolvers.FallbackDenyResolver {
private String dtdPublicId;
private String dtdSystemId;
private boolean inDtd;
DtdAwareDenyResolver() {
super(null);
}
@Override
public void startDTD(final String name, final String publicId, final String systemId) {
inDtd = true;
dtdPublicId = publicId;
dtdSystemId = systemId;
}
@Override
public void endDTD() {
inDtd = false;
}
@Override
protected InputSource onUnresolved(final String name, final String publicId, final String baseURI, final String systemId)
throws SAXException, IOException {
// Declaring (but not using) an external subset must not throw: let the parser skip it silently. Everything else is denied by the floor.
if (inDtd && Objects.equals(publicId, dtdPublicId) && Objects.equals(systemId, dtdSystemId)) {
return null;
}
return super.onUnresolved(name, publicId, baseURI, systemId);
}
}
/**
* Wrapper around Android's {@code org.apache.harmony.xml.ExpatReader} that surfaces its {@code namespace-prefixes} limitation at configuration time.
*
* <p>ExpatReader does not actually support the {@code namespace-prefixes} feature: enabling it is accepted by {@code setFeature} but fails later, during
* {@code parse}, with a {@link SAXNotSupportedException}. Reporting the rejection eagerly from {@link #setFeature(String, boolean)} lets consumers that probe
* the feature, such as Xalan's identity transformer, catch the exception and fall back instead of failing the whole parse.</p>
*/
static final class ExpatReaderWrapper extends DelegatingXMLReader {
private static final String NAMESPACE_PREFIXES_FEATURE = "http://xml.org/sax/features/namespace-prefixes";
ExpatReaderWrapper(final XMLReader delegate) {
super(delegate);
}
@Override
public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
if (value && NAMESPACE_PREFIXES_FEATURE.equals(name)) {
throw new SAXNotSupportedException("ExpatReader does not support enabling the '" + NAMESPACE_PREFIXES_FEATURE + "' feature");
}
super.setFeature(name, value);
}
}
/** Class name of Android's Harmony-based {@link SAXParserFactory}, backed by the native Expat parser. */
private static final String ANDROID_SAX_PARSER_FACTORY = "org.apache.harmony.xml.parsers.SAXParserFactoryImpl";
/** Class name of Android's Expat-backed {@link XMLReader}. */
private static final String ANDROID_EXPAT_READER = "org.apache.harmony.xml.ExpatReader";
/** SAX property carrying the {@link LexicalHandler}; used to observe the DTD boundary on Android's Expat. */
private static final String LEXICAL_HANDLER_PROPERTY = "http://xml.org/sax/properties/lexical-handler";
/** Xerces feature: load the external DTD subset for non-validating parsers. */
private static final String XERCES_LOAD_EXTERNAL_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
static SAXParserFactory harden(final SAXParserFactory factory) {
// Required: enables the implementation's security manager, which carries the limits. Android's Expat rejects FSP, so it is skipped there.
if (!ANDROID_SAX_PARSER_FACTORY.equals(factory.getClass().getName())) {
setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
}
// The per-parse hardening (limits, entity blocking, Android fixups) lives in hardenReader() because SAXParserFactory has no property API.
return new HardeningSAXParserFactory(factory);
}
/**
* Hardens an existing {@link XMLReader}.
*
* @param reader the reader to harden; never {@code null}.
* @return a hardened reader.
* @throws IllegalStateException if a required hardening setting cannot be applied to the underlying implementation.
*/
static XMLReader hardenReader(final XMLReader reader) {
if (reader instanceof HardeningXMLReader) {
// Already hardened (e.g. handed back through XmlFactories.harden(XMLReader)); the floor is already in place.
return reader;
}
if (ANDROID_EXPAT_READER.equals(reader.getClass().getName())) {
// Expat ignores external fetches when no resolver is set; a subset-aware deny floor fails on external entities but lets an unused subset load.
// Reject the unsupported namespace-prefixes feature eagerly rather than mid-parse.
final ExpatReaderWrapper guarded = new ExpatReaderWrapper(reader);
final DtdAwareDenyResolver floor = new DtdAwareDenyResolver();
// The floor needs the DTD-boundary events to tell the subset apart from entities; Expat recognizes the lexical-handler property.
trySetProperty(guarded, LEXICAL_HANDLER_PROPERTY, floor);
// HardeningXMLReader keeps the floor non-bypassable and routes a caller-set resolver (including SAXParser.parse's handler) through it.
return new HardeningXMLReader(guarded, floor);
}
// Required: enables the JDK XMLSecurityManager / Xerces SecurityManager limits.
setFeature(reader, XMLConstants.FEATURE_SECURE_PROCESSING, true);
// Optional: skip the external DTD subset on non-validating parsers so DOCTYPE-only documents parse without a blocked fetch attempt.
setOptionalFeature(reader, XERCES_LOAD_EXTERNAL_DTD, false);
// Optional, implementation-based: JDK limit properties or Xerces' SecurityManager.
Limits.tryApply(reader);
// ACCESS_EXTERNAL_* support is the dividing capability between JAXP 1.5 implementations and older ones.
if (trySetProperty(reader, XMLConstants.ACCESS_EXTERNAL_DTD, "")
&& trySetProperty(reader, XMLConstants.ACCESS_EXTERNAL_SCHEMA, "")) {
// Honoured (stock JDK): the JAXP 1.5 properties block external fetches, so the bare reader is already hardened.
return reader;
}
// Rejected: external Xerces ignores ACCESS_EXTERNAL_*; wrap the reader so a deny-all resolver floor blocks external fetches and a caller-set resolver
// cannot replace it.
return new HardeningXMLReader(reader);
}
private SAXParserHardener() {
}
}