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.lang.invoke.MethodHandle; 021import java.util.Objects; 022 023import javax.xml.XMLConstants; 024import javax.xml.parsers.FactoryConfigurationError; 025import javax.xml.transform.Source; 026import javax.xml.validation.Schema; 027import javax.xml.validation.SchemaFactory; 028import javax.xml.validation.SchemaFactoryConfigurationError; 029import javax.xml.validation.Validator; 030 031import org.w3c.dom.ls.LSResourceResolver; 032import org.xml.sax.ErrorHandler; 033import org.xml.sax.SAXException; 034import org.xml.sax.SAXNotRecognizedException; 035import org.xml.sax.SAXNotSupportedException; 036 037/** 038 * Creates new, secure {@link SchemaFactory} instances. 039 * <p> 040 * Beyond the three universal guarantees on {@link org.apache.commons.xml.secure}: 041 * </p> 042 * <ul> 043 * <li>{@code xs:import}, {@code xs:include} and {@code xs:redefine} schemaLocation URIs are not resolved during schema compilation,</li> 044 * <li>{@code xsi:schemaLocation} / {@code xsi:noNamespaceSchemaLocation} hints in instance documents are not resolved during validation, and</li> 045 * <li>the content model a schema expands into is bounded, on every implementation offering a limit for it. A loader expands a repeated particle while building 046 * the DFA, so a compact schema carrying a large {@code maxOccurs} would otherwise exhaust memory or CPU (see Xerces' 047 * <a href="https://xerces.apache.org/xerces2-j/properties.html#security-manager">security manager</a>, which caps that expansion at 3,000 nodes).</li> 048 * </ul> 049 * <p> 050 * The same guarantees apply to {@link javax.xml.validation.Validator} and {@link javax.xml.validation.ValidatorHandler} instances produced from the resulting 051 * {@link javax.xml.validation.Schema}. 052 * </p> 053 * <p> 054 * This class is not itself a {@link SchemaFactory}, so it inherits none of the static JAXP factory methods. A caller therefore cannot obtain an unsecured 055 * factory through this class by calling a method such as {@code newDefaultInstance()}. The secure factories are instances of a nested, non-public wrapper 056 * class. 057 * </p> 058 * 059 * @see org.apache.commons.xml.secure 060 */ 061public final class SecureSchemaFactory { 062 063 /** 064 * Capability-driven secure wrapper for any {@link SchemaFactory} on the classpath, the same recipe for every implementation. It is the entry point reached 065 * by {@link SecureSchemaFactory#newInstance(String)}; there is no per-implementation branching and no limit configuration on the factory itself beyond 066 * {@code FEATURE_SECURE_PROCESSING}. 067 * 068 * <p> 069 * Three layers cooperate: 070 * </p> 071 * <ol> 072 * <li>{@link SecureSchemaFactory} installs an ignore-all {@link FallbackIgnoreLSResourceResolver} floor on the factory (blocking 073 * {@code xs:import}/{@code xs:include}/{@code xs:redefine} at compile time) and rewrites the Source on every {@code newSchema(Source[])} entry point 074 * through {@link SecureSAXParserFactory#secure(Source, boolean)}.</li> 075 * <li>{@link SecureSchema} wraps every Validator/ValidatorHandler the inner Schema produces and re-installs the floor on each (blocking 076 * {@code xsi:schemaLocation} at validation time), since neither the JDK nor Xerces reliably propagates it through {@code Schema}.</li> 077 * <li>{@link SecureValidator} rewrites the Source on every {@link Validator#validate(Source)} call.</li> 078 * </ol> 079 * 080 * <p> 081 * The secure reader supplied by {@link SecureSAXParserFactory#secure(Source, boolean)} already carries {@code FEATURE_SECURE_PROCESSING} and the processing 082 * limits, so a 083 * DOCTYPE, external entity or Billion Laughs payload in the schema or instance document is bounded there rather than on this factory. One limit it cannot 084 * supply is content-model expansion: a large {@code maxOccurs} is expanded by the schema loader when it builds the DFA, after parsing and without the 085 * reader, so {@code FEATURE_SECURE_PROCESSING} is set on the factory as well, which is what installs that bound on external Xerces (the stock JDK applies 086 * it unconditionally). The JAXP 1.5 {@code ACCESS_EXTERNAL_*} properties are still not set explicitly: the resolver floor already blocks the same fetches 087 * on 088 * every implementation, and the JDK 8 {@code SchemaFactory} has a bug whereby those properties keep blocking even when a caller's own resolver would grant 089 * access. The floor is a non-removable lower bound: a caller-set {@link LSResourceResolver} is routed through it (opting a specific lookup in by returning 090 * a non-{@code null} result) rather than replacing it, so the securing (or the floor) cannot be dropped by swapping the resolver. 091 * </p> 092 */ 093 private static final class Wrapper extends SchemaFactory { 094 095 private final SchemaFactory delegate; 096 097 098 private final FallbackIgnoreLSResourceResolver floor = new FallbackIgnoreLSResourceResolver(null); 099 100 /** 101 * Constructs a new instance. 102 * 103 * @param delegate The delegate to wrap; must not be {@code null}. 104 * @throws NullPointerException Thrown if {@code delegate} is {@code null}. 105 */ 106 private Wrapper(final SchemaFactory delegate) { 107 this.delegate = Objects.requireNonNull(delegate, "delegate"); 108 // Content-model expansion happens in the schema loader, after parsing, so the injected reader's limits cannot reach it. 109 SecureSchemaFactory.setFeature(delegate, XMLConstants.FEATURE_SECURE_PROCESSING, true); 110 // Compile-time block for xs:import/include/redefine; the wrappers carry the rest (per-product resolver, source rewriting, limits via the reader). 111 delegate.setResourceResolver(floor); 112 } 113 114 @Override 115 public ErrorHandler getErrorHandler() { 116 return delegate.getErrorHandler(); 117 } 118 119 @Override 120 public boolean getFeature(final String name) throws SAXNotRecognizedException, SAXNotSupportedException { 121 return delegate.getFeature(name); 122 } 123 124 @Override 125 public Object getProperty(final String name) throws SAXNotRecognizedException, SAXNotSupportedException { 126 return delegate.getProperty(name); 127 } 128 129 @Override 130 public LSResourceResolver getResourceResolver() { 131 return floor.getDelegate(); 132 } 133 134 @Override 135 public boolean isSchemaLanguageSupported(final String schemaLanguage) { 136 return delegate.isSchemaLanguageSupported(schemaLanguage); 137 } 138 139 @Override 140 public Schema newSchema() throws SAXException { 141 return new SecureSchema(delegate.newSchema(), overrideDefaultParser()); 142 } 143 144 /** 145 * {@inheritDoc} 146 * 147 * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service 148 * configuration error} or if the implementation is not available or cannot be instantiated. 149 */ 150 @Override 151 public Schema newSchema(final Source[] schemas) throws SAXException { 152 return new SecureSchema(delegate.newSchema(secure(schemas)), overrideDefaultParser()); 153 } 154 155 /** 156 * Tests whether parsers should be instantiated via {@code newInstance()} instead of {@code newDefaultInstance()}. 157 * 158 * <p> 159 * The JDK implementation of {@link SchemaFactory} uses the JDK parsers while {@value SecureSAXParserFactory#OVERRIDE_DEFAULT_PARSER} is unset or 160 * {@code false}. 161 * </p> 162 * 163 * @return {@code true} if parsers should be created via {@code newInstance()}. 164 */ 165 private boolean overrideDefaultParser() { 166 try { 167 return delegate.getFeature(SecureSAXParserFactory.OVERRIDE_DEFAULT_PARSER); 168 } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { 169 return true; 170 } 171 } 172 173 /** 174 * Secures every schema source through {@link SecureSAXParserFactory#secure(Source, boolean)}. 175 * 176 * @param schemas The schema sources to secure; must not be {@code null}. 177 * @return a new array of secure sources. 178 * @throws IllegalStateException Thrown if the underlying implementation cannot provide a secure reader. 179 * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service 180 * configuration error} or if the implementation is not available or cannot be instantiated. 181 */ 182 private Source[] secure(final Source[] schemas) { 183 final Source[] secure = new Source[schemas.length]; 184 final boolean overrideDefaultParser = overrideDefaultParser(); 185 for (int i = 0; i < schemas.length; i++) { 186 secure[i] = SecureSAXParserFactory.secure(schemas[i], overrideDefaultParser); 187 } 188 return secure; 189 } 190 191 @Override 192 public void setErrorHandler(final ErrorHandler errorHandler) { 193 delegate.setErrorHandler(errorHandler); 194 } 195 196 @Override 197 public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { 198 delegate.setFeature(name, value); 199 } 200 201 202 @Override 203 public void setProperty(final String name, final Object object) throws SAXNotRecognizedException, SAXNotSupportedException { 204 delegate.setProperty(name, object); 205 } 206 207 @Override 208 public void setResourceResolver(final LSResourceResolver resourceResolver) { 209 // Route a caller resolver through the floor instead of replacing it, so the ignore-all lower bound cannot be removed. 210 floor.setDelegate(resourceResolver); 211 } 212 } 213 214 /** 215 * Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. 216 */ 217 private static final String JDK_SCHEMA_FACTORY = "com.sun.org.apache.xerces.internal.jaxp.validation.XMLSchemaFactory"; 218 219 private static final MethodHandle MH_newDefaultInstance = MethodHandleFactory.findStatic(SchemaFactory.class, "newDefaultInstance"); 220 221 /** 222 * Returns a new, secure {@link SchemaFactory} of the system-default implementation, supporting W3C XML Schema 1.0. 223 * <p> 224 * Obtained from {@code SchemaFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), by instantiating the JDK's built-in 225 * implementation directly on Java 8, and by the standard {@link #newInstance(String)} lookup where the platform provides neither (for example, Android, 226 * whose lookup falls back to exactly the Xerces implementation this library recognizes). 227 * </p> 228 * 229 * @return A secure factory. 230 * @throws IllegalStateException Thrown if a required secure setting cannot be applied to the underlying implementation. 231 * @throws IllegalArgumentException Thrown from the {@link #newInstance(String)} lookup this method falls back to on a platform that provides neither 232 * {@code newDefaultInstance()} nor the JDK's built-in implementation (for example, Android). 233 */ 234 public static SchemaFactory newDefaultInstance() { 235 if (MH_newDefaultInstance != null) { 236 return secure(MethodHandleFactory.invokeExact(() -> (SchemaFactory) MH_newDefaultInstance.invokeExact(), SchemaFactoryConfigurationError.class)); 237 } 238 try { 239 // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead. 240 return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI, JDK_SCHEMA_FACTORY, null); 241 } catch (final IllegalArgumentException e) { 242 // Neither exists (for example Android): degrade to the regular lookup, whose Android fallback is exactly the Xerces implementation. 243 return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); 244 } 245 } 246 247 /** 248 * Returns a new, secure {@link SchemaFactory} for the given schema language. 249 * 250 * @param schemaLanguage The schema language, as accepted by {@link SchemaFactory#newInstance(String)}. 251 * @return A secure factory. 252 * @throws IllegalArgumentException Thrown if no implementation of the schema language is available. 253 * @throws NullPointerException Thrown if {@code schemaLanguage} is {@code null}. 254 * @throws SchemaFactoryConfigurationError Thrown if a configuration error is encountered. 255 */ 256 public static SchemaFactory newInstance(final String schemaLanguage) { 257 return secure(SchemaFactory.newInstance(schemaLanguage)); 258 } 259 260 /** 261 * Returns a new, secure {@link SchemaFactory} of the given implementation class. 262 * 263 * @param schemaLanguage The schema language, as accepted by {@link SchemaFactory#newInstance(String)}. 264 * @param factoryClassName The fully qualified class name of the {@link SchemaFactory} implementation. 265 * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. 266 * @return A secure factory. 267 * @throws IllegalArgumentException Thrown if {@code factoryClassName} is {@code null}, or if the factory class cannot be loaded or instantiated, or does 268 * not support {@code schemaLanguage}. 269 * @throws NullPointerException Thrown if {@code schemaLanguage} is {@code null}. 270 */ 271 public static SchemaFactory newInstance(final String schemaLanguage, final String factoryClassName, final ClassLoader classLoader) { 272 return secure(SchemaFactory.newInstance(schemaLanguage, factoryClassName, classLoader)); 273 } 274 275 /** 276 * Secures a {@link SchemaFactory}. 277 * 278 * <p> 279 * Unlike the other factory types, there is no per-implementation branching: schema compilation and validation reach external resources only through the 280 * resolver hook, so wrapping the factory with a non-removable ignore-all resolver floor is enough on every implementation. The reader used to parse schema 281 * and instance documents is secured separately, through {@link SecureSAXParserFactory#secure(javax.xml.transform.Source, boolean)}; the factory carries 282 * {@code FEATURE_SECURE_PROCESSING} for the one limit that reader cannot supply, the loader's content-model expansion. 283 * </p> 284 * 285 * @param factory The factory to secure; never {@code null}. 286 * @return a secure factory. 287 */ 288 static SchemaFactory secure(final SchemaFactory factory) { 289 return new Wrapper(factory); 290 } 291 292 /** 293 * Sets a feature on the delegate, failing closed: an implementation that cannot accept it yields no factory rather than an unsecured one. 294 * 295 * @param factory The factory to configure; never {@code null}. 296 * @param feature The feature name. 297 * @param value The value to set. 298 * @throws SecureException Thrown if the implementation rejects the feature. 299 */ 300 private static void setFeature(final SchemaFactory factory, final String feature, final boolean value) { 301 try { 302 factory.setFeature(feature, value); 303 } catch (final Exception e) { 304 throw SecureException.featureFailed(feature, factory, e); 305 } 306 } 307 308 private SecureSchemaFactory() { 309 // static only 310 } 311}