View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.xml.secure;
19  
20  import java.io.ByteArrayInputStream;
21  import java.io.IOException;
22  import java.net.URI;
23  import java.net.URISyntaxException;
24  
25  import org.xml.sax.EntityResolver;
26  import org.xml.sax.InputSource;
27  import org.xml.sax.SAXException;
28  import org.xml.sax.ext.DefaultHandler2;
29  import org.xml.sax.ext.EntityResolver2;
30  
31  /**
32   * Entity resolver that consults an optional caller-supplied resolver and ignores (resolves to empty) whatever the caller does not resolve.
33   * <p>
34   * The canonical secure floor, and the entity-resolution counterpart of the JAXP 1.5 {@code ACCESS_EXTERNAL_*} properties. Every floor
35   * ({@link FallbackIgnoreLSResourceResolver}, {@link FallbackIgnoreURIResolver} and {@link FallbackIgnoreXMLResolver}) shares two defining properties:
36   * </p>
37   * <ol>
38   * <li><strong>Non-removable, and it wraps the resolver the caller sets.</strong> The secure wrappers install one and route a caller-set resolver through
39   * {@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>
40   * <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
41   * 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
42   * <em>fetched</em>; a floor instead resolves it to <em>empty</em> content, so the parse continues without the external fetch and without a leak.</li>
43   * </ol>
44   * <p>
45   * The secure DOM and SAX wrappers install one of these and, when the caller sets their own {@link EntityResolver}, route it through {@link #setDelegate}
46   * 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
47   * resolver; anything they leave unresolved (a {@code null} return, or no caller resolver at all) goes to {@link #onUnresolved}, which returns empty content by
48   * default.
49   * </p>
50   */
51  final class FallbackIgnoreEntityResolver2 extends DefaultHandler2 {
52  
53      private static final byte[] EMPTY = {};
54  
55      /**
56       * Resolves {@code systemId} against {@code baseURI}.
57       *
58       * @param baseURI  The absolute base URI to resolve against, or {@code null} if none is available.
59       * @param systemId The system identifier, possibly relative to {@code baseURI}.
60       * @return The absolutized system identifier, or {@code systemId} unchanged when it cannot or need not be resolved.
61       */
62      private static String absolutize(final String baseURI, final String systemId) {
63          if (systemId == null || baseURI == null) {
64              return systemId;
65          }
66          try {
67              final URI system = new URI(systemId);
68              return system.isAbsolute() ? systemId : new URI(baseURI).resolve(system).toString();
69          } catch (final URISyntaxException e) {
70              return systemId;
71          }
72      }
73  
74      /**
75       * Caller-supplied resolver consulted first, or {@code null} for a pure ignore-all floor.
76       */
77      private EntityResolver delegate;
78  
79      /**
80       * Constructs a new ignore-all floor with an optional caller-supplied resolver.
81       *
82       * @param delegate The caller-supplied resolver, or {@code null} for a pure ignore-all floor.
83       */
84      FallbackIgnoreEntityResolver2(final EntityResolver delegate) {
85          this.delegate = delegate;
86      }
87  
88      /**
89       * Gets the delegate provided by the constructor or set by {@link #setDelegate}, may be {@code null}.
90       *
91       * @return The delegate provided by the constructor or set by {@link #setDelegate}, may be {@code null}.
92       */
93      EntityResolver getDelegate() {
94          return delegate;
95      }
96  
97      @Override
98      public InputSource getExternalSubset(final String name, final String baseURI) throws SAXException, IOException {
99          // A null return means "no synthetic subset", not "unresolved", nothing is fetched.
100         return delegate instanceof EntityResolver2 ? ((EntityResolver2) delegate).getExternalSubset(name, baseURI) : null;
101     }
102 
103     /**
104      * Outcome when neither the caller delegate nor this resolver provides the entity. Resolves to empty content by default, so the external resource is neither
105      * fetched nor leaked and the parse continues with no replacement text. The returned source echoes the requested identifiers (with {@code systemId}
106      * absolutized): the parser reads the empty byte stream, but Xerces still derives the entity's base URI from the system id and fails on a {@code null} one.
107      *
108      * @param name     The entity name, or {@code null} on the 2-arg resolution path.
109      * @param publicId The public identifier, or {@code null} if none.
110      * @param baseURI  The base URI for relative resolution, or {@code null}.
111      * @param systemId The system identifier of the unresolved entity.
112      * @return an empty {@link InputSource} carrying the requested identifiers.
113      * @throws SAXException Thrown when {@value SecureException#THROW_ON_UNRESOLVED} is set: unresolved references are rejected instead of resolved to empty.
114      * @throws IOException  Never thrown by the default implementation.
115      */
116     private InputSource onUnresolved(final String name, final String publicId, final String baseURI, final String systemId) throws SAXException {
117         if (SecureException.throwOnUnresolved()) {
118             throw new SAXException(SecureException.forbidden(name, null, publicId, systemId, baseURI));
119         }
120         final InputSource empty = new InputSource(new ByteArrayInputStream(EMPTY));
121         empty.setPublicId(publicId);
122         empty.setSystemId(absolutize(baseURI, systemId));
123         return empty;
124     }
125 
126     @Override
127     public InputSource resolveEntity(final String publicId, final String systemId) throws SAXException, IOException {
128         return resolveEntity(null, publicId, null, systemId);
129     }
130 
131     @Override
132     public InputSource resolveEntity(final String name, final String publicId, final String baseURI, final String systemId)
133             throws SAXException, IOException {
134         final InputSource resolved = resolveWithDelegate(name, publicId, baseURI, systemId);
135         return resolved != null ? resolved : onUnresolved(name, publicId, baseURI, systemId);
136     }
137 
138     private InputSource resolveWithDelegate(final String name, final String publicId, final String baseURI, final String systemId)
139             throws SAXException, IOException {
140         if (delegate != null) {
141             return delegate instanceof EntityResolver2 ? ((EntityResolver2) delegate).resolveEntity(name, publicId, baseURI, systemId) :
142             // We need to resolve the systemId against baseURI, because a plain EntityResolver expects an absolute URI.
143                     delegate.resolveEntity(publicId, absolutize(baseURI, systemId));
144         }
145         return null;
146     }
147 
148     /**
149      * Replaces the caller resolver consulted ahead of the floor; lets a single floor instance back successive {@code setEntityResolver} calls.
150      *
151      * @param delegate The caller-supplied resolver, or {@code null} for a pure ignore-all floor.
152      */
153     void setDelegate(final EntityResolver delegate) {
154         this.delegate = delegate;
155     }
156 }