blob: 082a35af2ed20253dfc7b89ad81b8f091f611415 [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 java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import javax.xml.stream.XMLResolver;
import javax.xml.stream.XMLStreamException;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.ext.DefaultHandler2;
import org.xml.sax.ext.EntityResolver2;
/**
* Policy resolvers that fix the outcome of every external lookup. Each member is a floor with two defining properties:
*
* <ol>
* <li><strong>Non-removable, and it wraps the resolver the caller sets.</strong> The hardened wrappers install one and route a caller-set resolver through
* {@code setDelegate} rather than letting it replace the floor, so the caller's resolver is consulted first but cannot remove the floor underneath it.</li>
* <li><strong>It supplies the default action for a lookup the caller's resolver does not resolve</strong> (a {@code null} return, or no caller resolver at
* all). This is where a floor departs from stock JAXP: normally an unresolved lookup falls back to the processor's built-in resolution and the resource is
* <em>fetched</em>; a floor instead <em>denies</em> it (throws).</li>
* </ol>
*
* <p>The deny members are {@link FallbackDenyResolver} (for {@code EntityResolver}), {@link FallbackDenyLSResourceResolver} (for {@link LSResourceResolver}),
* {@link FallbackDenyURIResolver} (for {@link URIResolver}) and {@link FallbackDenyXMLResolver} (for {@link XMLResolver}). {@link FallbackIgnoreXMLResolver} is a
* variant whose default action returns an empty input instead of throwing, for the Woodstox DTD-subset and undeclared-entity hooks where a missing resource must
* be skipped rather than denied.</p>
*/
final class Resolvers {
/**
* Entity resolver that consults an optional caller-supplied resolver and denies (throws) whatever the caller does not resolve.
*
* <p>This is the entity-resolution counterpart of the JAXP 1.5 {@code ACCESS_EXTERNAL_*} properties: a non-overridable floor. The hardened DOM and SAX
* wrappers install one of these and, when the caller sets their own {@link EntityResolver}, route it through {@link #setDelegate} rather than letting it
* replace the floor. A caller therefore opts a specific resource in by returning a non-{@code null} {@link InputSource} from their resolver; anything they
* leave unresolved (a {@code null} return, or no caller resolver at all) goes to {@link #onUnresolved}, which denies by default.</p>
*
* <p>It extends {@link DefaultHandler2} so it is also usable as a {@link org.xml.sax.ext.LexicalHandler} (see {@code SAXParserHardener}'s Android subclass,
* which needs {@code startDTD}/{@code endDTD}); {@link #getExternalSubset} therefore inherits the {@code DefaultHandler2} "no synthetic subset" default. Only
* {@link #resolveEntity(String, String, String, String) resolveEntity} (the actual external fetch) reaches the deny fallback.</p>
*/
static class FallbackDenyResolver extends DefaultHandler2 {
/**
* Caller-supplied resolver consulted first, or {@code null} for a pure deny-all floor.
*/
private EntityResolver delegate;
FallbackDenyResolver(final EntityResolver delegate) {
this.delegate = delegate;
}
/**
* Replaces the caller resolver consulted ahead of the floor; lets a single floor instance back successive {@code setEntityResolver} calls.
*
* @param delegate the caller-supplied resolver, or {@code null} for a pure deny-all floor.
*/
final void setDelegate(final EntityResolver delegate) {
this.delegate = delegate;
}
final EntityResolver getDelegate() {
return delegate;
}
@Override
public final InputSource resolveEntity(final String publicId, final String systemId) throws SAXException, IOException {
return resolveEntity(null, publicId, null, systemId);
}
@Override
public final InputSource resolveEntity(final String name, final String publicId, final String baseURI, final String systemId)
throws SAXException, IOException {
final InputSource resolved = resolveWithDelegate(name, publicId, baseURI, systemId);
return resolved != null ? resolved : onUnresolved(name, publicId, baseURI, systemId);
}
/**
* Outcome when neither the caller delegate nor this resolver provides the entity. Denies by default; a subclass may permit specific lookups (e.g. the
* external DTD subset) by returning {@code null} or an {@link InputSource} instead of calling {@code super}.
*
* @param name the entity name, or {@code null} on the 2-arg resolution path.
* @param publicId the public identifier, or {@code null} if none.
* @param baseURI the base URI for relative resolution, or {@code null}.
* @param systemId the system identifier of the unresolved entity.
* @return An {@link InputSource} to permit the lookup, or {@code null} to skip it silently; the default implementation never returns normally.
* @throws SAXException to deny the lookup (the default behavior).
* @throws IOException if a subclass opens a stream that fails.
*/
protected InputSource onUnresolved(final String name, final String publicId, final String baseURI, final String systemId)
throws SAXException, IOException {
throw new SAXException(forbiddenMessage(name, null, publicId, systemId, baseURI));
}
private InputSource resolveWithDelegate(final String name, final String publicId, final String baseURI,
final String systemId) throws SAXException, IOException {
if (delegate != null) {
return delegate instanceof EntityResolver2 ? ((EntityResolver2) delegate).resolveEntity(name, publicId, baseURI, systemId) :
// We need to resolve the systemId against baseURI, because a plain EntityResolver expects an absolute URI.
delegate.resolveEntity(publicId, absolutize(baseURI, systemId));
}
return null;
}
}
/**
* {@link LSResourceResolver} floor: consults an optional caller-supplied resolver and denies (throws) whatever the caller does not resolve.
*
* <p>The schema-compile counterpart of {@link FallbackDenyResolver}. The hardened {@link javax.xml.validation.SchemaFactory}, {@link
* javax.xml.validation.Validator} and {@link javax.xml.validation.ValidatorHandler} wrappers install one of these and route a caller-set resolver through
* {@link #setDelegate} rather than letting it replace the floor. A caller opts a specific resource in by returning a non-{@code null} {@link LSInput};
* anything left unresolved is denied.</p>
*/
static final class FallbackDenyLSResourceResolver implements LSResourceResolver {
private LSResourceResolver delegate;
FallbackDenyLSResourceResolver(final LSResourceResolver delegate) {
this.delegate = delegate;
}
void setDelegate(final LSResourceResolver delegate) {
this.delegate = delegate;
}
LSResourceResolver getDelegate() {
return delegate;
}
@Override
public LSInput resolveResource(final String type, final String namespaceURI, final String publicId, final String systemId, final String baseURI) {
final LSInput resolved = delegate != null ? delegate.resolveResource(type, namespaceURI, publicId, systemId, baseURI) : null;
if (resolved != null) {
return resolved;
}
throw new SecurityException(forbiddenMessage(type, namespaceURI, publicId, systemId, baseURI));
}
}
/**
* {@link URIResolver} floor: consults an optional caller-supplied resolver and denies (throws) whatever the caller does not resolve.
*
* <p>The XSLT counterpart of {@link FallbackDenyResolver}, guarding {@code xsl:import}/{@code xsl:include} at compile time and {@code document()} at
* transform time. The hardened {@link javax.xml.transform.TransformerFactory} and {@link javax.xml.transform.Transformer} wrappers install one of these and
* route a caller-set resolver through {@link #setDelegate} rather than letting it replace the floor. A caller opts a specific URI in by returning a
* non-{@code null} {@link Source}; anything left unresolved is denied.</p>
*/
static final class FallbackDenyURIResolver implements URIResolver {
private URIResolver delegate;
FallbackDenyURIResolver(final URIResolver delegate) {
this.delegate = delegate;
}
void setDelegate(final URIResolver delegate) {
this.delegate = delegate;
}
URIResolver getDelegate() {
return delegate;
}
@Override
public Source resolve(final String href, final String base) throws TransformerException {
final Source resolved = delegate != null ? delegate.resolve(href, base) : null;
if (resolved != null) {
return resolved;
}
throw new TransformerException(forbiddenMessage("uri", null, null, href, base));
}
}
/**
* {@link XMLResolver} floor: consults an optional caller-supplied resolver and denies (throws) whatever the caller does not resolve.
*
* <p>The StAX counterpart of {@link FallbackDenyResolver}, installed on each entity-resolution hook. The hardened {@link javax.xml.stream.XMLInputFactory}
* wrapper routes a caller-set resolver through {@link #setDelegate} rather than letting it replace the floor. A caller opts a specific entity in by returning
* a non-{@code null} result; anything left unresolved goes to {@link #onUnresolved}, which denies by default. Subclasses override {@code onUnresolved} to give
* a hook a different unresolved policy (e.g. return an empty input for the external DTD subset, or for undeclared entities) while keeping the caller-delegate
* behavior.</p>
*/
static class FallbackDenyXMLResolver implements XMLResolver {
private XMLResolver delegate;
FallbackDenyXMLResolver(final XMLResolver delegate) {
this.delegate = delegate;
}
final void setDelegate(final XMLResolver delegate) {
this.delegate = delegate;
}
final XMLResolver getDelegate() {
return delegate;
}
@Override
public final Object resolveEntity(final String publicID, final String systemID, final String baseURI, final String namespace) throws XMLStreamException {
final Object resolved = delegate != null ? delegate.resolveEntity(publicID, systemID, baseURI, namespace) : null;
return resolved != null ? resolved : onUnresolved(publicID, systemID, baseURI, namespace);
}
/**
* Outcome when the caller delegate does not resolve the entity. Denies by default; a subclass may return an {@link java.io.InputStream}, {@link Source} or
* other {@link XMLResolver}-supported value (for example an empty input) instead of calling {@code super}, or {@code throw}
* {@link #denied(String, String, String, String)} to deny only some lookups.
*
* @param publicID the public identifier, or {@code null} if none.
* @param systemID the system identifier of the unresolved entity.
* @param baseURI the base URI for relative resolution, or {@code null}.
* @param namespace the namespace (or, for Woodstox, the entity name), or {@code null}.
* @return The replacement input, or a value the caller's parser accepts; the default implementation never returns normally.
* @throws XMLStreamException to deny the lookup (the default behavior).
*/
protected Object onUnresolved(final String publicID, final String systemID, final String baseURI, final String namespace) throws XMLStreamException {
throw denied(publicID, systemID, baseURI, namespace);
}
/**
* Builds the standard "forbidden by hardening" exception for a denied lookup, so a subclass with a mixed policy can reuse the deny outcome for the
* lookups it refuses.
*
* @param publicID the public identifier, or {@code null} if none.
* @param systemID the system identifier of the unresolved entity.
* @param baseURI the base URI for relative resolution, or {@code null}.
* @param namespace the namespace (or, for Woodstox, the entity name), or {@code null}.
* @return The exception to throw.
*/
protected final XMLStreamException denied(final String publicID, final String systemID, final String baseURI, final String namespace) {
return new XMLStreamException(forbiddenMessage(null, namespace, publicID, systemID, baseURI));
}
}
/**
* {@link FallbackDenyXMLResolver} variant whose unresolved policy returns an empty input instead of throwing, so the parse continues with no replacement
* content. Used on Woodstox's DTD-subset and undeclared-entity hooks (where a missing resource must be skipped, not denied), while still consulting an
* optional caller-supplied resolver first.
*/
static class FallbackIgnoreXMLResolver extends FallbackDenyXMLResolver {
/**
* Empty {@link ByteArrayInputStream} shared across every call. {@code read()} on a zero-length array always returns {@code -1}, so reusing the instance
* is safe even though the type is technically stateful.
*/
private static final InputStream EMPTY = new ByteArrayInputStream(new byte[0]);
FallbackIgnoreXMLResolver(final XMLResolver delegate) {
super(delegate);
}
@Override
protected Object onUnresolved(final String publicID, final String systemID, final String baseURI, final String namespace) throws XMLStreamException {
return EMPTY;
}
}
/**
* Resolves {@code systemId} against {@code baseURI}.
*
* @param baseURI the absolute base URI to resolve against, or {@code null} if none is available.
* @param systemId the system identifier, possibly relative to {@code baseURI}.
* @return The absolutized system identifier, or {@code systemId} unchanged when it cannot or need not be resolved.
*/
private static String absolutize(final String baseURI, final String systemId) {
if (systemId == null || baseURI == null) {
return systemId;
}
try {
final URI system = new URI(systemId);
return system.isAbsolute() ? systemId : new URI(baseURI).resolve(system).toString();
} catch (final URISyntaxException e) {
return systemId;
}
}
private static String forbiddenMessage(final String type, final String namespace, final String publicId, final String systemId, final String baseURI) {
return String.format("External resource fetch forbidden by hardening: type=%s, namespace=%s, publicId=%s, systemId=%s, baseURI=%s", type, namespace,
publicId, systemId, baseURI);
}
private Resolvers() {
}
}