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