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 }