001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.xml.secure; 019 020import java.io.InputStream; 021import java.io.Reader; 022import java.lang.invoke.MethodHandle; 023import java.util.Objects; 024 025import javax.xml.stream.EventFilter; 026import javax.xml.stream.FactoryConfigurationError; 027import javax.xml.stream.StreamFilter; 028import javax.xml.stream.XMLEventReader; 029import javax.xml.stream.XMLInputFactory; 030import javax.xml.stream.XMLReporter; 031import javax.xml.stream.XMLResolver; 032import javax.xml.stream.XMLStreamException; 033import javax.xml.stream.XMLStreamReader; 034import javax.xml.stream.util.XMLEventAllocator; 035import javax.xml.transform.Source; 036 037/** 038 * Creates new, secure {@link XMLInputFactory} instances. 039 * <p> 040 * The three universal guarantees on {@link org.apache.commons.xml.secure} apply; StAX exposes no additional vectors beyond them. 041 * </p> 042 * <p> 043 * This class is not itself a {@link XMLInputFactory}, so it inherits none of the static JAXP factory methods. A caller therefore cannot obtain an unsecured 044 * factory through this class by calling a method such as {@code newDefaultFactory()}. The secure factories are instances of a nested, non-public wrapper class. 045 * </p> 046 * 047 * @see org.apache.commons.xml.secure 048 */ 049public final class SecureXMLInputFactory { 050 051 /** 052 * {@link XMLInputFactory} wrapper that installs a non-removable {@link FallbackIgnoreXMLResolver} floor on the delegate's entity-resolution hook so the 053 * caller cannot remove it. 054 * <p> 055 * The constructor installs the floor through {@code setXMLResolver}, which every implementation routes external resolution through (Woodstox fans it out to 056 * both its DTD-subset and entity resolvers). Woodstox keeps one hook outside that fan-out, {@value SecureXMLInputFactory#WSTX_UNDECLARED_ENTITY_RESOLVER}, 057 * which is deliberately left empty: emptying the external subset leaves any entity it declared undeclared, and Woodstox then rejects the reference. The 058 * rejection is implementation-prescribed and keeps the resource unfetched, like the empty resolution the other implementations produce; a caller who wants 059 * those references resolved can still set the property, and their resolver lands behind a floor like on every other resolver hook. 060 * </p> 061 * <p> 062 * Every resolver-valued entry point ({@link #setXMLResolver(XMLResolver)}, {@code setProperty(XMLInputFactory.RESOLVER, ...)} and the Woodstox 063 * {@code com.ctc.wstx.*Resolver} keys) is routed uniformly: a caller who supplies their own {@link FallbackIgnoreXMLResolver} takes control and it is 064 * passed straight to the delegate; otherwise the caller's resolver is wrapped in a new floor installed on that hook, so the floor cannot be removed by dropping 065 * the resolver. This matters because Woodstox does not chain resolvers: when a resolver returns {@code null}, 066 * {@code DefaultInputResolver} falls through to fetching the systemId URL itself, so a caller-set resolver that returns {@code null} must still land behind 067 * the floor. {@link #getXMLResolver()} and {@code getProperty} report the caller's resolver unwrapped. 068 * </p> 069 * <p> 070 * The floor on a hook is replaced rather than mutated, which is what keeps each hook and each reader independent. Woodstox routes {@code setXMLResolver} to 071 * both its DTD-subset and entity hooks, so one floor object sits on several of them, and it copies that reference into every reader it creates; setting a 072 * delegate on the object in place would therefore also answer hooks the caller never named, and would change the resolution policy of readers already 073 * created, including ones parsing on another thread. Installing a new floor leaves both untouched: a hook keeps whatever floor it was given, and a reader 074 * keeps the one it captured when it was created. 075 * </p> 076 */ 077 private static final class Wrapper extends XMLInputFactory { 078 079 private static boolean isResolverProperty(final String name) { 080 return XMLInputFactory.RESOLVER.equals(name) 081 || WSTX_DTD_RESOLVER.equals(name) 082 || WSTX_ENTITY_RESOLVER.equals(name) 083 || WSTX_UNDECLARED_ENTITY_RESOLVER.equals(name); 084 } 085 086 087 private static XMLResolver unwrap(final XMLResolver resolver) { 088 return resolver instanceof FallbackIgnoreXMLResolver ? ((FallbackIgnoreXMLResolver) resolver).getDelegate() : resolver; 089 } 090 091 private final XMLInputFactory delegate; 092 093 /** 094 * Constructs a new instance. 095 * 096 * @param delegate The delegate to wrap; must not be {@code null}. 097 * @throws NullPointerException Thrown if {@code delegate} is {@code null}. 098 */ 099 private Wrapper(final XMLInputFactory delegate) { 100 this.delegate = Objects.requireNonNull(delegate, "delegate"); 101 delegate.setXMLResolver(new FallbackIgnoreXMLResolver(null)); 102 } 103 104 @Override 105 public XMLEventReader createFilteredReader(final XMLEventReader reader, final EventFilter filter) throws XMLStreamException { 106 return delegate.createFilteredReader(reader, filter); 107 } 108 109 @Override 110 public XMLStreamReader createFilteredReader(final XMLStreamReader reader, final StreamFilter filter) throws XMLStreamException { 111 return delegate.createFilteredReader(reader, filter); 112 } 113 114 @Override 115 public XMLEventReader createXMLEventReader(final InputStream stream) throws XMLStreamException { 116 return delegate.createXMLEventReader(stream); 117 } 118 119 @Override 120 public XMLEventReader createXMLEventReader(final InputStream stream, final String encoding) throws XMLStreamException { 121 return delegate.createXMLEventReader(stream, encoding); 122 } 123 124 @Override 125 public XMLEventReader createXMLEventReader(final Reader reader) throws XMLStreamException { 126 return delegate.createXMLEventReader(reader); 127 } 128 129 @Override 130 public XMLEventReader createXMLEventReader(final Source source) throws XMLStreamException { 131 return delegate.createXMLEventReader(source); 132 } 133 134 @Override 135 public XMLEventReader createXMLEventReader(final String systemId, final InputStream stream) throws XMLStreamException { 136 return delegate.createXMLEventReader(systemId, stream); 137 } 138 139 @Override 140 public XMLEventReader createXMLEventReader(final String systemId, final Reader reader) throws XMLStreamException { 141 return delegate.createXMLEventReader(systemId, reader); 142 } 143 144 @Override 145 public XMLEventReader createXMLEventReader(final XMLStreamReader reader) throws XMLStreamException { 146 return delegate.createXMLEventReader(reader); 147 } 148 149 @Override 150 public XMLStreamReader createXMLStreamReader(final InputStream stream) throws XMLStreamException { 151 return delegate.createXMLStreamReader(stream); 152 } 153 154 @Override 155 public XMLStreamReader createXMLStreamReader(final InputStream stream, final String encoding) throws XMLStreamException { 156 return delegate.createXMLStreamReader(stream, encoding); 157 } 158 159 @Override 160 public XMLStreamReader createXMLStreamReader(final Reader reader) throws XMLStreamException { 161 return delegate.createXMLStreamReader(reader); 162 } 163 164 @Override 165 public XMLStreamReader createXMLStreamReader(final Source source) throws XMLStreamException { 166 return delegate.createXMLStreamReader(source); 167 } 168 169 @Override 170 public XMLStreamReader createXMLStreamReader(final String systemId, final InputStream stream) throws XMLStreamException { 171 return delegate.createXMLStreamReader(systemId, stream); 172 } 173 174 @Override 175 public XMLStreamReader createXMLStreamReader(final String systemId, final Reader reader) throws XMLStreamException { 176 return delegate.createXMLStreamReader(systemId, reader); 177 } 178 179 180 @Override 181 public XMLEventAllocator getEventAllocator() { 182 return delegate.getEventAllocator(); 183 } 184 185 @Override 186 public Object getProperty(final String name) { 187 if (isResolverProperty(name)) { 188 return unwrap((XMLResolver) delegate.getProperty(name)); 189 } 190 return delegate.getProperty(name); 191 } 192 193 @Override 194 public XMLReporter getXMLReporter() { 195 return delegate.getXMLReporter(); 196 } 197 198 @Override 199 public XMLResolver getXMLResolver() { 200 return unwrap(delegate.getXMLResolver()); 201 } 202 203 @Override 204 public boolean isPropertySupported(final String name) { 205 return delegate.isPropertySupported(name); 206 } 207 208 @Override 209 public void setEventAllocator(final XMLEventAllocator allocator) { 210 delegate.setEventAllocator(allocator); 211 } 212 213 @Override 214 public void setProperty(final String name, final Object value) { 215 // If a resolver property has a value of the wrong type, pass it to the delegate to generate an appropriate exception. 216 if (isResolverProperty(name) && (value == null || value instanceof XMLResolver)) { 217 setResolverProperty(name, (XMLResolver) value); 218 } else { 219 delegate.setProperty(name, value); 220 } 221 } 222 223 /** 224 * Routes a caller-set resolver for the property {@code name} behind the floor currently installed on that hook. 225 * 226 * @param name The resolver-valued property being set. 227 * @param resolver The caller's resolver, or their own {@link FallbackIgnoreXMLResolver} to take control. 228 */ 229 private void setResolverProperty(final String name, final XMLResolver resolver) { 230 if (resolver instanceof FallbackIgnoreXMLResolver) { 231 // The caller supplies their own floor: hand it to the delegate as-is. 232 delegate.setProperty(name, resolver); 233 } else { 234 // A fresh floor for this hook rather than a new delegate on the floor already there: Woodstox puts one floor object on several hooks and copies 235 // the reference into every reader it creates, so mutating it would reach hooks, and readers already parsing, that this call never named. 236 delegate.setProperty(name, new FallbackIgnoreXMLResolver(resolver)); 237 } 238 } 239 240 @Override 241 public void setXMLReporter(final XMLReporter reporter) { 242 delegate.setXMLReporter(reporter); 243 } 244 245 @Override 246 public void setXMLResolver(final XMLResolver resolver) { 247 setResolverProperty(XMLInputFactory.RESOLVER, resolver); 248 } 249 } 250 /** Woodstox property: resolver consulted for the external DTD subset. */ 251 static final String WSTX_DTD_RESOLVER = "com.ctc.wstx.dtdResolver"; 252 /** Woodstox property: resolver consulted for declared external general entities. */ 253 static final String WSTX_ENTITY_RESOLVER = "com.ctc.wstx.entityResolver"; 254 /** Woodstox property: resolver consulted for undeclared entity references. */ 255 static final String WSTX_UNDECLARED_ENTITY_RESOLVER = "com.ctc.wstx.undeclaredEntityResolver"; 256 257 /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultFactory()}. */ 258 private static final String JDK_XML_INPUT_FACTORY = "com.sun.xml.internal.stream.XMLInputFactoryImpl"; 259 260 private static final MethodHandle MH_newDefaultInstance = MethodHandleFactory.findStatic(XMLInputFactory.class, "newDefaultFactory"); 261 262 /** 263 * Returns a new, secure {@link XMLInputFactory} of the system-default implementation. 264 * <p> 265 * Obtained from {@code XMLInputFactory.newDefaultFactory()} where the platform provides it (Java 9 or later), and by instantiating the JDK's built-in 266 * implementation directly on Java 8. 267 * </p> 268 * 269 * @return A secure factory. 270 * @throws IllegalStateException Thrown if a required secure setting cannot be applied to the underlying implementation. 271 * @throws FactoryConfigurationError Thrown if the running platform provides neither {@code newDefaultFactory()} nor the JDK's built-in implementation 272 * (for example Android). 273 */ 274 public static XMLInputFactory newDefaultFactory() { 275 if (MH_newDefaultInstance != null) { 276 return secure(MethodHandleFactory.invokeExact(() -> (XMLInputFactory) MH_newDefaultInstance.invokeExact(), FactoryConfigurationError.class)); 277 } 278 try { 279 // Java 8: the method does not exist, and XMLInputFactory has no class-name-taking lookup; instantiate the JDK's built-in default directly. 280 return secure((XMLInputFactory) Class.forName(JDK_XML_INPUT_FACTORY).getConstructor().newInstance()); 281 } catch (final ReflectiveOperationException e) { 282 // Where the class does not exist either (for example Android), report the miss like any StAX factory lookup: with FactoryConfigurationError. 283 throw new FactoryConfigurationError(e, "Neither XMLInputFactory.newDefaultFactory() nor " + JDK_XML_INPUT_FACTORY + " is available"); 284 } 285 } 286 287 /** 288 * Returns a new, secure {@link XMLInputFactory}, like {@link XMLInputFactory#newFactory()}. 289 * 290 * @return A secure factory. 291 * @throws IllegalStateException Thrown if a required secure setting cannot be applied to the underlying implementation. 292 * @throws FactoryConfigurationError Thrown if an instance of this factory cannot be loaded. 293 */ 294 public static XMLInputFactory newFactory() { 295 // XMLInputFactory.newInstance, not newFactory: the same specified lookup, but Android's StAX API predates newFactory. 296 return secure(XMLInputFactory.newInstance()); 297 } 298 299 /** 300 * Returns a new, secure {@link XMLInputFactory} resolved from the given factory id. 301 * 302 * @param factoryId The name of the factory to find; a system property or service id to look up, not the class name of the implementation. 303 * @param classLoader The class loader used in the lookup; {@code null} means the current thread's context class loader. 304 * @return A secure factory. 305 * @throws IllegalStateException Thrown if a required secure setting cannot be applied to the underlying implementation. 306 * @throws FactoryConfigurationError Thrown in case of a service configuration error or if the implementation is not available or cannot be instantiated. 307 * @throws NullPointerException Thrown if {@code factoryId} is {@code null}. 308 */ 309 public static XMLInputFactory newFactory(final String factoryId, final ClassLoader classLoader) { 310 return secure(XMLInputFactory.newFactory(factoryId, classLoader)); 311 } 312 313 /** 314 * Returns a new, secure {@link XMLInputFactory}. 315 * 316 * @return A secure factory. 317 * @throws IllegalStateException Thrown if a required secure setting cannot be applied to the underlying implementation. 318 * @throws FactoryConfigurationError Thrown if an instance of this factory cannot be loaded. 319 */ 320 public static XMLInputFactory newInstance() { 321 return secure(XMLInputFactory.newInstance()); 322 } 323 324 /** 325 * Capability-driven secure for any {@link XMLInputFactory} (StAX) on the classpath. 326 * 327 * <p>One recipe covers both the JDK Zephyr and Woodstox: the wrapper installs a non-removable {@link FallbackIgnoreXMLResolver} floor on 328 * every entity-resolution hook, leaving the standard {@code SUPPORT_DTD} / {@code IS_SUPPORTING_EXTERNAL_ENTITIES} defaults untouched; see the wrapper's 329 * Javadoc for the per-implementation hook routing.</p> 330 * 331 * @param factory The factory to secure; never {@code null}. 332 * @return a secure factory. 333 */ 334 static XMLInputFactory secure(final XMLInputFactory factory) { 335 // The wrapper installs the non-removable ignore-all resolver floor that resolves every external DTD and entity to empty content. 336 return new Wrapper(factory); 337 } 338 339 private SecureXMLInputFactory() { 340 // static only 341 } 342}