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