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 * <p>
51 * It extends {@link DefaultHandler2} so it is also usable as a {@link org.xml.sax.ext.LexicalHandler}; {@link #getExternalSubset} therefore inherits the
52 * {@code DefaultHandler2} "no synthetic subset" default. Only {@link #resolveEntity(String, String, String, String) resolveEntity} (the actual external fetch)
53 * reaches the ignore fallback.
54 * </p>
55 */
56 class FallbackIgnoreEntityResolver2 extends DefaultHandler2 {
57
58 private static final byte[] EMPTY = {};
59
60 /**
61 * Resolves {@code systemId} against {@code baseURI}.
62 *
63 * @param baseURI The absolute base URI to resolve against, or {@code null} if none is available.
64 * @param systemId The system identifier, possibly relative to {@code baseURI}.
65 * @return The absolutized system identifier, or {@code systemId} unchanged when it cannot or need not be resolved.
66 */
67 private static String absolutize(final String baseURI, final String systemId) {
68 if (systemId == null || baseURI == null) {
69 return systemId;
70 }
71 try {
72 final URI system = new URI(systemId);
73 return system.isAbsolute() ? systemId : new URI(baseURI).resolve(system).toString();
74 } catch (final URISyntaxException e) {
75 return systemId;
76 }
77 }
78
79 /**
80 * Caller-supplied resolver consulted first, or {@code null} for a pure ignore-all floor.
81 */
82 private EntityResolver delegate;
83
84 /**
85 * Constructs a new ignore-all floor with an optional caller-supplied resolver.
86 *
87 * @param delegate The caller-supplied resolver, or {@code null} for a pure ignore-all floor.
88 */
89 FallbackIgnoreEntityResolver2(final EntityResolver delegate) {
90 this.delegate = delegate;
91 }
92
93 /**
94 * Gets the delegate provided by the constructor or set by {@link #setDelegate}, may be {@code null}.
95 *
96 * @return The delegate provided by the constructor or set by {@link #setDelegate}, may be {@code null}.
97 */
98 final EntityResolver getDelegate() {
99 return delegate;
100 }
101
102 /**
103 * Outcome when neither the caller delegate nor this resolver provides the entity. Resolves to empty content by default, so the external resource is neither
104 * fetched nor leaked and the parse continues with no replacement text. The returned source echoes the requested identifiers (with {@code systemId}
105 * 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.
106 *
107 * @param name The entity name, or {@code null} on the 2-arg resolution path.
108 * @param publicId The public identifier, or {@code null} if none.
109 * @param baseURI The base URI for relative resolution, or {@code null}.
110 * @param systemId The system identifier of the unresolved entity.
111 * @return An empty {@link InputSource} carrying the requested identifiers.
112 * @throws SAXException when {@value SecureException#THROW_ON_UNRESOLVED} is set: unresolved references are rejected instead of resolved to empty.
113 * @throws IOException never by the default implementation.
114 */
115 protected InputSource onUnresolved(final String name, final String publicId, final String baseURI, final String systemId) throws SAXException, IOException {
116 if (SecureException.throwOnUnresolved()) {
117 throw new SAXException(SecureException.forbidden(name, null, publicId, systemId, baseURI));
118 }
119 final InputSource empty = new InputSource(new ByteArrayInputStream(EMPTY));
120 empty.setPublicId(publicId);
121 empty.setSystemId(absolutize(baseURI, systemId));
122 return empty;
123 }
124
125 @Override
126 public final InputSource resolveEntity(final String publicId, final String systemId) throws SAXException, IOException {
127 return resolveEntity(null, publicId, null, systemId);
128 }
129
130 @Override
131 public final InputSource resolveEntity(final String name, final String publicId, final String baseURI, final String systemId)
132 throws SAXException, IOException {
133 final InputSource resolved = resolveWithDelegate(name, publicId, baseURI, systemId);
134 return resolved != null ? resolved : onUnresolved(name, publicId, baseURI, systemId);
135 }
136
137 private InputSource resolveWithDelegate(final String name, final String publicId, final String baseURI, final String systemId)
138 throws SAXException, IOException {
139 if (delegate != null) {
140 return delegate instanceof EntityResolver2 ? ((EntityResolver2) delegate).resolveEntity(name, publicId, baseURI, systemId) :
141 // We need to resolve the systemId against baseURI, because a plain EntityResolver expects an absolute URI.
142 delegate.resolveEntity(publicId, absolutize(baseURI, systemId));
143 }
144 return null;
145 }
146
147 /**
148 * Replaces the caller resolver consulted ahead of the floor; lets a single floor instance back successive {@code setEntityResolver} calls.
149 *
150 * @param delegate The caller-supplied resolver, or {@code null} for a pure ignore-all floor.
151 */
152 final void setDelegate(final EntityResolver delegate) {
153 this.delegate = delegate;
154 }
155 }