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>Three layers cooperate:</p> 069 * <ol> 070 * <li>{@link SecureSchemaFactory} installs an ignore-all {@link FallbackIgnoreLSResourceResolver} floor on the factory (blocking 071 * {@code xs:import}/{@code xs:include}/{@code xs:redefine} at compile time) and rewrites the Source on every {@code newSchema(Source[])} entry point 072 * through {@link SecureSAXParserFactory#secure(Source, boolean)}.</li> 073 * <li>{@link SecureSchema} wraps every Validator/ValidatorHandler the inner Schema produces and re-installs the floor on each (blocking 074 * {@code xsi:schemaLocation} at validation time), since neither the JDK nor Xerces reliably propagates it through {@code Schema}.</li> 075 * <li>{@link SecureValidator} rewrites the Source on every {@link Validator#validate(Source)} call.</li> 076 * </ol> 077 * 078 * <p> 079 * The secure reader supplied by {@link SecureSAXParserFactory#secure(Source, boolean)} already carries {@code FEATURE_SECURE_PROCESSING} and the processing limits, so a 080 * 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 081 * 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 082 * 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 083 * it unconditionally). The JAXP 1.5 {@code ACCESS_EXTERNAL_*} properties are still not set explicitly: the resolver floor already blocks the same fetches on 084 * 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 085 * 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 a 086 * non-{@code null} result) rather than replacing it, so the securing (or the floor) cannot be dropped by swapping the resolver. 087 * </p> 088 */ 089 private static final class Wrapper extends SchemaFactory { 090 091 private final SchemaFactory delegate; 092 093 094 private final FallbackIgnoreLSResourceResolver floor = new FallbackIgnoreLSResourceResolver(null); 095 096 /** 097 * Constructs a new instance. 098 * 099 * @param delegate The delegate to wrap; must not be {@code null}. 100 * @throws NullPointerException Thrown if {@code delegate} is {@code null}. 101 */ 102 private Wrapper(final SchemaFactory delegate) { 103 this.delegate = Objects.requireNonNull(delegate, "delegate"); 104 // Content-model expansion happens in the schema loader, after parsing, so the injected reader's limits cannot reach it. 105 SecureSchemaFactory.setFeature(delegate, XMLConstants.FEATURE_SECURE_PROCESSING, true); 106 // Compile-time block for xs:import/include/redefine; the wrappers carry the rest (per-product resolver, source rewriting, limits via the reader). 107 delegate.setResourceResolver(floor); 108 } 109 110 @Override 111 public ErrorHandler getErrorHandler() { 112 return delegate.getErrorHandler(); 113 } 114 115 @Override 116 public boolean getFeature(final String name) throws SAXNotRecognizedException, SAXNotSupportedException { 117 return delegate.getFeature(name); 118 } 119 120 @Override 121 public Object getProperty(final String name) throws SAXNotRecognizedException, SAXNotSupportedException { 122 return delegate.getProperty(name); 123 } 124 125 @Override 126 public LSResourceResolver getResourceResolver() { 127 return floor.getDelegate(); 128 } 129 130 @Override 131 public boolean isSchemaLanguageSupported(final String schemaLanguage) { 132 return delegate.isSchemaLanguageSupported(schemaLanguage); 133 } 134 135 @Override 136 public Schema newSchema() throws SAXException { 137 return new SecureSchema(delegate.newSchema(), overrideDefaultParser()); 138 } 139 140 /** 141 * {@inheritDoc} 142 * 143 * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service 144 * configuration error} or if the implementation is not available or cannot be instantiated. 145 */ 146 @Override 147 public Schema newSchema(final Source[] schemas) throws SAXException { 148 return new SecureSchema(delegate.newSchema(secure(schemas)), overrideDefaultParser()); 149 } 150 151 /** 152 * Tests whether parsers should be instantiated via {@code newInstance()} instead of {@code newDefaultInstance()}. 153 * 154 * <p>The JDK implementation of {@link SchemaFactory} uses the JDK parsers while {@value SecureSAXParserFactory#OVERRIDE_DEFAULT_PARSER} is unset or 155 * {@code false}.</p> 156 * 157 * @return {@code true} if parsers should be created via {@code newInstance()}. 158 */ 159 private boolean overrideDefaultParser() { 160 try { 161 return delegate.getFeature(SecureSAXParserFactory.OVERRIDE_DEFAULT_PARSER); 162 } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { 163 return true; 164 } 165 } 166 167 /** 168 * Secures every schema source through {@link SecureSAXParserFactory#secure(Source, boolean)}. 169 * 170 * @param schemas The schema sources to secure; must not be {@code null}. 171 * @return a new array of secure sources. 172 * @throws IllegalStateException Thrown if the underlying implementation cannot provide a secure reader. 173 * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service 174 * configuration error} or if the implementation is not available or cannot be instantiated. 175 */ 176 private Source[] secure(final Source[] schemas) { 177 final Source[] secure = new Source[schemas.length]; 178 final boolean overrideDefaultParser = overrideDefaultParser(); 179 for (int i = 0; i < schemas.length; i++) { 180 secure[i] = SecureSAXParserFactory.secure(schemas[i], overrideDefaultParser); 181 } 182 return secure; 183 } 184 185 @Override 186 public void setErrorHandler(final ErrorHandler errorHandler) { 187 delegate.setErrorHandler(errorHandler); 188 } 189 190 @Override 191 public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { 192 delegate.setFeature(name, value); 193 } 194 195 196 @Override 197 public void setProperty(final String name, final Object object) throws SAXNotRecognizedException, SAXNotSupportedException { 198 delegate.setProperty(name, object); 199 } 200 201 @Override 202 public void setResourceResolver(final LSResourceResolver resourceResolver) { 203 // Route a caller resolver through the floor instead of replacing it, so the ignore-all lower bound cannot be removed. 204 floor.setDelegate(resourceResolver); 205 } 206 } 207 208 /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */ 209 private static final String JDK_SCHEMA_FACTORY = "com.sun.org.apache.xerces.internal.jaxp.validation.XMLSchemaFactory"; 210 211 private static final MethodHandle MH_newDefaultInstance = MethodHandleFactory.findStatic(SchemaFactory.class, "newDefaultInstance"); 212 213 /** 214 * Returns a new, secure {@link SchemaFactory} of the system-default implementation, supporting W3C XML Schema 1.0. 215 * <p> 216 * Obtained from {@code SchemaFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), by instantiating the JDK's built-in 217 * implementation directly on Java 8, and by the standard {@link #newInstance(String)} lookup where the platform provides neither (for example Android, 218 * whose lookup falls back to exactly the Xerces implementation this library recognizes). 219 * </p> 220 * 221 * @return A secure factory. 222 * @throws IllegalStateException Thrown if a required secure setting cannot be applied to the underlying implementation. 223 * @throws IllegalArgumentException Thrown from the {@link #newInstance(String)} lookup this method falls back to on a platform that provides neither 224 * {@code newDefaultInstance()} nor the JDK's built-in implementation (for example Android). 225 */ 226 public static SchemaFactory newDefaultInstance() { 227 if (MH_newDefaultInstance != null) { 228 return secure(MethodHandleFactory.invokeExact(() -> (SchemaFactory) MH_newDefaultInstance.invokeExact(), SchemaFactoryConfigurationError.class)); 229 } 230 try { 231 // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead. 232 return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI, JDK_SCHEMA_FACTORY, null); 233 } catch (final IllegalArgumentException e) { 234 // Neither exists (for example Android): degrade to the regular lookup, whose Android fallback is exactly the Xerces implementation. 235 return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); 236 } 237 } 238 239 /** 240 * Returns a new, secure {@link SchemaFactory} for the given schema language. 241 * 242 * @param schemaLanguage The schema language, as accepted by {@link SchemaFactory#newInstance(String)}. 243 * @return A secure factory. 244 * @throws IllegalArgumentException Thrown if no implementation of the schema language is available. 245 * @throws NullPointerException Thrown if {@code schemaLanguage} is {@code null}. 246 * @throws SchemaFactoryConfigurationError Thrown if a configuration error is encountered. 247 */ 248 public static SchemaFactory newInstance(final String schemaLanguage) { 249 return secure(SchemaFactory.newInstance(schemaLanguage)); 250 } 251 252 /** 253 * Returns a new, secure {@link SchemaFactory} of the given implementation class. 254 * 255 * @param schemaLanguage The schema language, as accepted by {@link SchemaFactory#newInstance(String)}. 256 * @param factoryClassName The fully qualified class name of the {@link SchemaFactory} implementation. 257 * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. 258 * @return A secure factory. 259 * @throws IllegalArgumentException Thrown if {@code factoryClassName} is {@code null}, or if the factory class cannot be loaded or instantiated, or does 260 * not support {@code schemaLanguage}. 261 * @throws NullPointerException Thrown if {@code schemaLanguage} is {@code null}. 262 */ 263 public static SchemaFactory newInstance(final String schemaLanguage, final String factoryClassName, final ClassLoader classLoader) { 264 return secure(SchemaFactory.newInstance(schemaLanguage, factoryClassName, classLoader)); 265 } 266 267 /** 268 * Secures a {@link SchemaFactory}. 269 * 270 * <p>Unlike the other factory types there is no per-implementation branching: schema compilation and validation reach external resources only through the 271 * 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 272 * and instance documents is secured separately, through {@link SecureSAXParserFactory#secure(javax.xml.transform.Source, boolean)}; the factory carries 273 * {@code FEATURE_SECURE_PROCESSING} for the one limit that reader cannot supply, the loader's content-model expansion.</p> 274 * 275 * @param factory The factory to secure; never {@code null}. 276 * @return a secure factory. 277 */ 278 static SchemaFactory secure(final SchemaFactory factory) { 279 return new Wrapper(factory); 280 } 281 282 /** 283 * Sets a feature on the delegate, failing closed: an implementation that cannot accept it yields no factory rather than an unsecured one. 284 * 285 * @param factory The factory to configure; never {@code null}. 286 * @param feature The feature name. 287 * @param value The value to set. 288 * @throws SecureException Thrown if the implementation rejects the feature. 289 */ 290 private static void setFeature(final SchemaFactory factory, final String feature, final boolean value) { 291 try { 292 factory.setFeature(feature, value); 293 } catch (final Exception e) { 294 throw SecureException.featureFailed(feature, factory, e); 295 } 296 } 297 298 private SecureSchemaFactory() { 299 // static only 300 } 301}