(enhance) introduce resolving strategy (configurable xml security); introduce API configuration

This commit is contained in:
Andreas Penski (init) 2020-04-29 10:06:00 +02:00
parent 7a86f049ac
commit 35c0797898
67 changed files with 2441 additions and 845 deletions

View file

@ -0,0 +1,108 @@
package de.kosit.validationtool.impl.xml;
import static java.lang.String.format;
import javax.xml.XMLConstants;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import lombok.extern.slf4j.Slf4j;
import de.kosit.validationtool.api.ResolvingConfigurationStrategy;
/**
* @author Andreas Penski
*/
@Slf4j
public abstract class BaseResolvingStrategy implements ResolvingConfigurationStrategy {
protected static final String DISSALLOW_DOCTYPE_DECL_FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
protected static final String LOAD_EXTERNAL_DTD_FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
protected static final String FEATURE_SECURE_PROCESSING = "http://javax.xml.XMLConstants/feature/secure-processing";
private static final String ORACLE_XERCES_CLASS = "com.sun.org.apache.xerces.internal.impl.Constants";
public static void forceOpenJdkXmlImplementation() {
if (!isOpenJdkXmlImplementationAvailable()) {
throw new IllegalStateException("No OpenJDK version of XERCES found");
}
}
public static boolean isOpenJdkXmlImplementationAvailable() {
try {
Class.forName(ORACLE_XERCES_CLASS);
return true;
} catch (final ClassNotFoundException e) {
log.warn("No oracle JDK version of XERCES found. Configured security features may not have any effect.");
log.warn("Please take care of XML security while checking your xml contents");
return false;
}
}
private void setProperty(final PropertySetter setter, final boolean lenient, final String errorMessage) {
try {
setter.apply();
} catch (final SAXNotRecognizedException | SAXNotSupportedException e) {
if (lenient) {
log.warn(errorMessage);
log.debug(e.getMessage(), e);
} else {
throw new IllegalStateException(errorMessage);
}
}
}
protected void allowExternalSchema(final Validator validator, final String... scheme) {
allowExternalSchema(validator, false, scheme);
}
protected void allowExternalSchema(final SchemaFactory schemaFactory, final String... scheme) {
allowExternalSchema(schemaFactory, false, scheme);
}
protected void allowExternalSchema(final Validator validator, final boolean lenient, final String... schemes) {
final String schemeString = String.join(",", schemes);
setProperty(() -> validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, schemeString), lenient, format(
"Can set external schema access to schemes (%s). Maybe an unsupported JAXP implementation is used.", schemeString));
}
protected void allowExternalSchema(final SchemaFactory schemaFactory, final boolean lenient, final String... schemes) {
final String schemeString = String.join(",", schemes);
setProperty(() -> schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, schemeString), lenient, format(
"Can set external schema access to schemes (%s). Maybe an unsupported JAXP implementation is used.", schemeString));
}
protected void disableExternalEntities(final Validator validator) {
disableExternalEntities(validator, false);
}
protected void disableExternalEntities(final SchemaFactory schemaFactory) {
disableExternalEntities(schemaFactory, false);
}
protected void disableExternalEntities(final Validator validator, final boolean lenient) {
log.debug("Try to disable extern DTD access");
setProperty(() -> validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""), lenient,
"Can not disable external DTD access. Maybe an unsupported JAXP implementation is used.");
}
protected void disableExternalEntities(final SchemaFactory schemaFactory, final boolean lenient) {
log.debug("Try to disable extern DTD access");
setProperty(() -> schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""), lenient,
"Can not disable external DTD access. Maybe an unsupported JAXP implementation is used.");
}
@FunctionalInterface
private interface PropertySetter {
void apply() throws SAXNotRecognizedException, SAXNotSupportedException;
}
}

View file

@ -0,0 +1,102 @@
/*
* Licensed to the Koordinierungsstelle für IT-Standards (KoSIT) under
* one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. KoSIT licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package de.kosit.validationtool.impl.xml;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;
import javax.xml.transform.stream.StreamSource;
import lombok.RequiredArgsConstructor;
import net.sf.saxon.Configuration;
import net.sf.saxon.trans.XPathException;
/**
* {@link URIResolver} that resolves artifacts relative to a given base uri. The resolved URI must be resolving as child
* e.g. the baseUri must be a parent of the resolved artifact.
*
* @author Andreas Penski
*/
@RequiredArgsConstructor()
public class RelativeUriResolver implements URIResolver {
/** the base uri */
private final URI baseUri;
@Override
public Source resolve(final String href, final String base) throws TransformerException {
final URI resolved = resolve(URI.create(href), URI.create(base));
if (isUnderBaseUri(resolved)) {
try {
return new StreamSource(resolved.toURL().openStream(), resolved.toASCIIString());
} catch (final IOException e) {
throw new TransformerException(String.format("Can not resolve required %s", href), e);
}
} else {
throw new TransformerException(String
.format("The resolved transformation artifact %s is not within the configured repository %s", resolved, this.baseUri));
}
}
/**
* Resolves a relative uri including uris within a jar file.
*
* @param href the uri to resolve
* @param base the base uri
* @return the resolved uri
*/
public static URI resolve(final URI href, final URI base) {
final boolean jarURI = isJarURI(base);
final URI tmpBase = jarURI ? URI.create(base.toASCIIString().substring(4)) : base;
final URI result = tmpBase.resolve(href);
return jarURI ? URI.create("jar:" + result.toString()) : result;
}
static boolean isJarURI(final URI uri) {
return uri.isOpaque() && uri.getScheme().equals("jar");
}
private boolean isUnderBaseUri(final URI resolved) {
final String base = this.baseUri.toASCIIString().replaceAll("file:/+", "");
final String r = resolved.toASCIIString().replaceAll("file:/+", "");
return r.startsWith(base);
}
public Reader resolve(final URI absoluteURI, final String encoding, final Configuration config) throws XPathException {
if (isUnderBaseUri(absoluteURI)) {
try {
return new InputStreamReader(absoluteURI.toURL().openStream(), encoding);
} catch (final IOException e) {
throw new IllegalStateException(String.format("Can not resolve required %s", absoluteURI), e);
}
} else {
throw new IllegalStateException(String.format(
"The resolved transformation artifact %s is not within the configured repository %s", absoluteURI, this.baseUri));
}
}
}

View file

@ -0,0 +1,51 @@
package de.kosit.validationtool.impl.xml;
import java.net.URI;
import javax.xml.transform.URIResolver;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import lombok.extern.slf4j.Slf4j;
import net.sf.saxon.s9api.Processor;
/**
*
*
* @author Andreas Penski
*/
@Slf4j
public class StrictLocalResolvingStrategy extends StrictRelativeResolvingStrategy {
/**
* e.g. don't allow any scheme
*/
@Override
public SchemaFactory createSchemaFactory() {
final SchemaFactory schemaFactory = super.createSchemaFactory();
allowExternalSchema(schemaFactory, "file", "jar");
return schemaFactory;
}
@Override
public Processor createProcessor() {
return super.createProcessor();
}
@Override
public URIResolver createResolver(final URI repository) {
// intentionally return 'null', since all resolving is configured with the other objects
return null;
}
@Override
public Validator createValidator(final Schema schema) {
final Validator validator = super.createValidator(schema);
allowExternalSchema(validator, "file", "jar");
return validator;
}
}

View file

@ -0,0 +1,127 @@
package de.kosit.validationtool.impl.xml;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import javax.xml.XMLConstants;
import javax.xml.transform.Result;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import lombok.RequiredArgsConstructor;
import net.sf.saxon.Configuration;
import net.sf.saxon.expr.XPathContext;
import net.sf.saxon.lib.CollectionFinder;
import net.sf.saxon.lib.Feature;
import net.sf.saxon.lib.FeatureKeys;
import net.sf.saxon.lib.OutputURIResolver;
import net.sf.saxon.lib.ResourceCollection;
import net.sf.saxon.lib.UnparsedTextURIResolver;
import net.sf.saxon.s9api.Processor;
import net.sf.saxon.trans.XPathException;
/**
* @author Andreas Penski
*/
@RequiredArgsConstructor
public class StrictRelativeResolvingStrategy extends BaseResolvingStrategy {
private static class SecureUriResolver implements CollectionFinder, OutputURIResolver, UnparsedTextURIResolver {
public static final String MESSAGE = "Configuration error. Resolving ist not allowed";
@Override
public OutputURIResolver newInstance() {
return this;
}
@Override
public Result resolve(final String href, final String base) throws TransformerException {
throw new IllegalStateException(MESSAGE);
}
@Override
public void close(final Result result) throws TransformerException {
throw new IllegalStateException(MESSAGE);
}
@Override
public Reader resolve(final URI absoluteURI, final String encoding, final Configuration config) throws XPathException {
throw new IllegalStateException(MESSAGE);
}
@Override
public ResourceCollection findCollection(final XPathContext context, final String collectionURI) throws XPathException {
throw new IllegalStateException(MESSAGE);
}
}
/**
* e.g. don't allow any scheme
*/
private static final String EMPTY_SCHEME = "";
@Override
public SchemaFactory createSchemaFactory() {
final SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
disableExternalEntities(sf);
allowExternalSchema(sf, "file");
return sf;
}
@Override
public Processor createProcessor() {
final Processor processor = new Processor(false);
// verhindere global im Prinzip alle resolving strategien
final SecureUriResolver resolver = new SecureUriResolver();
processor.getUnderlyingConfiguration().setCollectionFinder(resolver);
processor.getUnderlyingConfiguration().setOutputURIResolver(resolver);
processor.getUnderlyingConfiguration().setUnparsedTextURIResolver(resolver);
// grundsätzlich Feature-konfiguration:
processor.setConfigurationProperty(Feature.DTD_VALIDATION, false);
processor.setConfigurationProperty(Feature.ENTITY_RESOLVER_CLASS, "");
processor.setConfigurationProperty(Feature.XINCLUDE, false);
processor.setConfigurationProperty(Feature.ALLOW_EXTERNAL_FUNCTIONS, false);
// Konfiguration des zu verwendenden Parsers, wenn Saxon selbst einen erzeugen muss, bspw. beim XSL parsen
processor.setConfigurationProperty(FeatureKeys.XML_PARSER_FEATURE + encode(FEATURE_SECURE_PROCESSING), true);
processor.setConfigurationProperty(FeatureKeys.XML_PARSER_FEATURE + encode(DISSALLOW_DOCTYPE_DECL_FEATURE), true);
processor.setConfigurationProperty(FeatureKeys.XML_PARSER_FEATURE + encode(LOAD_EXTERNAL_DTD_FEATURE), false);
return processor;
}
private static String encode(final String input) {
try {
return URLEncoder.encode(input, StandardCharsets.UTF_8.name());
} catch (final UnsupportedEncodingException e) {
throw new IllegalStateException("Error encoding property while initializing saxon", e);
}
}
@Override
public URIResolver createResolver(final URI repositoryURI) {
return new RelativeUriResolver(repositoryURI);
}
@Override
public Validator createValidator(final Schema schema) {
if (schema == null) {
throw new IllegalArgumentException("No schema supplied. Can not create validator");
}
forceOpenJdkXmlImplementation();
final Validator validator = schema.newValidator();
disableExternalEntities(validator);
allowExternalSchema(validator, "file" /* allow nothing external */);
return validator;
}
}