KEYCLOAK-16444 Initialize JAXP components consistently
This commit is contained in:
parent
cb1060799e
commit
5c2122d36f
4 changed files with 218 additions and 8 deletions
|
@ -22,6 +22,7 @@ import org.keycloak.saml.common.constants.GeneralConstants;
|
||||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||||
import org.keycloak.saml.common.util.DocumentUtil;
|
import org.keycloak.saml.common.util.DocumentUtil;
|
||||||
import org.keycloak.saml.common.util.SecurityActions;
|
import org.keycloak.saml.common.util.SecurityActions;
|
||||||
|
import org.keycloak.saml.common.util.StaxParserUtil;
|
||||||
import org.keycloak.saml.common.util.SystemPropertiesUtil;
|
import org.keycloak.saml.common.util.SystemPropertiesUtil;
|
||||||
import org.w3c.dom.Node;
|
import org.w3c.dom.Node;
|
||||||
import org.xml.sax.ErrorHandler;
|
import org.xml.sax.ErrorHandler;
|
||||||
|
@ -37,6 +38,14 @@ import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import javax.xml.XMLConstants;
|
||||||
|
import javax.xml.stream.XMLStreamException;
|
||||||
|
import javax.xml.transform.stax.StAXSource;
|
||||||
|
import org.xml.sax.SAXNotRecognizedException;
|
||||||
|
import org.xml.sax.SAXNotSupportedException;
|
||||||
|
import static org.keycloak.saml.common.util.DocumentUtil.feature_disallow_doctype_decl;
|
||||||
|
import static org.keycloak.saml.common.util.DocumentUtil.feature_external_general_entities;
|
||||||
|
import static org.keycloak.saml.common.util.DocumentUtil.feature_external_parameter_entities;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class associated with JAXP Validation
|
* Utility class associated with JAXP Validation
|
||||||
|
@ -52,12 +61,12 @@ public class JAXPValidationUtil {
|
||||||
|
|
||||||
protected static SchemaFactory schemaFactory;
|
protected static SchemaFactory schemaFactory;
|
||||||
|
|
||||||
public static void validate(String str) throws SAXException, IOException {
|
|
||||||
validator().validate(new StreamSource(str));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void validate(InputStream stream) throws SAXException, IOException {
|
public static void validate(InputStream stream) throws SAXException, IOException {
|
||||||
validator().validate(new StreamSource(stream));
|
try {
|
||||||
|
validator().validate(new StAXSource(StaxParserUtil.getXMLEventReader(stream)));
|
||||||
|
} catch (XMLStreamException ex) {
|
||||||
|
throw new IOException(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -86,11 +95,42 @@ public class JAXPValidationUtil {
|
||||||
throw logger.nullValueError("schema");
|
throw logger.nullValueError("schema");
|
||||||
|
|
||||||
validator = schema.newValidator();
|
validator = schema.newValidator();
|
||||||
|
// Do not optimize the following into setProperty(...) && setProperty(...).
|
||||||
|
// This way if it fails in the first setProperty, it will try the subsequent setProperty anyway
|
||||||
|
// which it would not due to short-circuiting in case of an && expression.
|
||||||
|
boolean successful1 = setProperty(validator, XMLConstants.ACCESS_EXTERNAL_DTD, "");
|
||||||
|
successful1 &= setProperty(validator, XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
|
||||||
|
boolean successful2 = setFeature(validator, feature_disallow_doctype_decl, true);
|
||||||
|
successful2 &= setFeature(validator, feature_external_general_entities, false);
|
||||||
|
successful2 &= setFeature(validator, feature_external_parameter_entities, false);
|
||||||
|
if (! successful1 && ! successful2) {
|
||||||
|
logger.warn("Cannot disable external access in XML validator");
|
||||||
|
}
|
||||||
validator.setErrorHandler(new CustomErrorHandler());
|
validator.setErrorHandler(new CustomErrorHandler());
|
||||||
}
|
}
|
||||||
return validator;
|
return validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean setProperty(Validator v, String property, String value) {
|
||||||
|
try {
|
||||||
|
v.setProperty(property, value);
|
||||||
|
} catch (SAXNotRecognizedException | SAXNotSupportedException ex) {
|
||||||
|
logger.debug("Cannot set " + property);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean setFeature(Validator v, String feature, boolean value) {
|
||||||
|
try {
|
||||||
|
v.setFeature(feature, value);
|
||||||
|
} catch (SAXNotRecognizedException | SAXNotSupportedException ex) {
|
||||||
|
logger.debug("Cannot set " + feature);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static Schema getSchema() throws IOException {
|
private static Schema getSchema() throws IOException {
|
||||||
boolean tccl_jaxp = SystemPropertiesUtil.getSystemProperty(GeneralConstants.TCCL_JAXP, "false").equalsIgnoreCase("true");
|
boolean tccl_jaxp = SystemPropertiesUtil.getSystemProperty(GeneralConstants.TCCL_JAXP, "false").equalsIgnoreCase("true");
|
||||||
|
|
||||||
|
@ -99,7 +139,7 @@ public class JAXPValidationUtil {
|
||||||
if (tccl_jaxp) {
|
if (tccl_jaxp) {
|
||||||
SecurityActions.setTCCL(JAXPValidationUtil.class.getClassLoader());
|
SecurityActions.setTCCL(JAXPValidationUtil.class.getClassLoader());
|
||||||
}
|
}
|
||||||
schemaFactory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
|
schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
|
||||||
|
|
||||||
schemaFactory.setResourceResolver(new IDFedLSInputResolver());
|
schemaFactory.setResourceResolver(new IDFedLSInputResolver());
|
||||||
schemaFactory.setErrorHandler(new CustomErrorHandler());
|
schemaFactory.setErrorHandler(new CustomErrorHandler());
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed 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 org.keycloak.saml.processing.core.util;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.xml.sax.SAXException;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class JAXPValidationUtilTest {
|
||||||
|
|
||||||
|
private static final String REQUEST_VALID = "<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"a123\" Version=\"2.0\" IssueInstant=\"2014-07-16T23:52:45Z\" >" +
|
||||||
|
"<saml:Issuer>urn:test</saml:Issuer>" +
|
||||||
|
"</samlp:AuthnRequest>";
|
||||||
|
|
||||||
|
private static final String REQUEST_FLAWED = "<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"&heh;\" Version=\"2.0\" IssueInstant=\"2014-07-16T23:52:45Z\" >" +
|
||||||
|
"<saml:Issuer>urn:test</saml:Issuer>" +
|
||||||
|
"</samlp:AuthnRequest>";
|
||||||
|
|
||||||
|
private static final String REQUEST_FLAWED_LOCAL = "<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"&heh;\" Version=\"2.0\" IssueInstant=\"2014-07-16T23:52:45Z\" >" +
|
||||||
|
"<saml:Issuer>urn:test</saml:Issuer>" +
|
||||||
|
"</samlp:AuthnRequest>";
|
||||||
|
|
||||||
|
private static final String REQUEST_INVALID = "<samlp:InvalidAuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"a123\" Version=\"2.0\" IssueInstant=\"2014-07-16T23:52:45Z\" >" +
|
||||||
|
"<saml:Issuer>urn:test</saml:Issuer>" +
|
||||||
|
"</samlp:AuthnRequest>";
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testServerSideValidator() throws Exception {
|
||||||
|
String preamble = "<!DOCTYPE AuthnRequest [" +
|
||||||
|
"<!ELEMENT AuthnRequest (#PCDATA)>" +
|
||||||
|
"<!ENTITY heh SYSTEM \"file:///etc/passwd\">" +
|
||||||
|
"]>";
|
||||||
|
|
||||||
|
assertInputValidation(REQUEST_VALID, Matchers.nullValue());
|
||||||
|
|
||||||
|
assertInputValidation(REQUEST_INVALID, Matchers.notNullValue());
|
||||||
|
assertInputValidation(preamble + REQUEST_FLAWED, Matchers.notNullValue());
|
||||||
|
assertInputValidation(preamble + REQUEST_FLAWED_LOCAL, Matchers.notNullValue());
|
||||||
|
assertInputValidation(preamble + "<AuthnRequest></AuthnRequest>", Matchers.notNullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertInputValidation(String s, Matcher<Object> matcher) {
|
||||||
|
String validationResult = null;
|
||||||
|
try {
|
||||||
|
JAXPValidationUtil.validate(new ByteArrayInputStream(s.getBytes()));
|
||||||
|
} catch (SAXException | IOException ex) {
|
||||||
|
validationResult = ex.getMessage();
|
||||||
|
}
|
||||||
|
// log.debugf("Validation result: '%s' for: %s", validationResult, s);
|
||||||
|
assertThat(s, validationResult, matcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -61,6 +61,7 @@ import org.keycloak.common.util.reflections.Reflections;
|
||||||
import org.keycloak.testsuite.arquillian.undertow.saml.util.RestSamlApplicationConfig;
|
import org.keycloak.testsuite.arquillian.undertow.saml.util.RestSamlApplicationConfig;
|
||||||
import org.keycloak.testsuite.utils.undertow.UndertowDeployerHelper;
|
import org.keycloak.testsuite.utils.undertow.UndertowDeployerHelper;
|
||||||
import org.keycloak.testsuite.utils.undertow.UndertowWarClassLoader;
|
import org.keycloak.testsuite.utils.undertow.UndertowWarClassLoader;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:vramik@redhat.com">Vlasta Ramik</a>
|
* @author <a href="mailto:vramik@redhat.com">Vlasta Ramik</a>
|
||||||
|
@ -229,8 +230,12 @@ public class UndertowAppServer implements DeployableContainer<UndertowAppServerC
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isJaxrsApp(WebArchive archive) throws DeploymentException {
|
private boolean isJaxrsApp(WebArchive archive) throws DeploymentException {
|
||||||
try {
|
if (! archive.contains("/WEB-INF/web.xml")) {
|
||||||
return IOUtils.toString(archive.get("/WEB-INF/web.xml").getAsset().openStream(), Charset.forName("UTF-8"))
|
return false;
|
||||||
|
}
|
||||||
|
try (InputStream stream = archive.get("/WEB-INF/web.xml").getAsset().openStream()) {
|
||||||
|
return
|
||||||
|
IOUtils.toString(stream, Charset.forName("UTF-8"))
|
||||||
.contains(Application.class.getName());
|
.contains(Application.class.getName());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new DeploymentException("Unable to read archive.", e);
|
throw new DeploymentException("Unable to read archive.", e);
|
||||||
|
|
|
@ -11,14 +11,36 @@ import org.junit.Test;
|
||||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||||
import org.keycloak.saml.processing.web.util.PostBindingUtil;
|
import org.keycloak.saml.processing.web.util.PostBindingUtil;
|
||||||
|
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
||||||
|
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URL;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
import org.jboss.arquillian.container.test.api.Deployment;
|
||||||
|
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
|
||||||
|
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||||
|
import org.jboss.shrinkwrap.api.ShrinkWrap;
|
||||||
|
import org.jboss.shrinkwrap.api.asset.StringAsset;
|
||||||
|
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||||
import static org.hamcrest.CoreMatchers.containsString;
|
import static org.hamcrest.CoreMatchers.containsString;
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
import static org.keycloak.testsuite.util.Matchers.bodyHC;
|
import static org.keycloak.testsuite.util.Matchers.bodyHC;
|
||||||
|
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
|
||||||
|
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_UNDERTOW)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY_DEPRECATED)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_EAP)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_EAP6)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_EAP71)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT7)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT8)
|
||||||
|
@AppServerContainer(ContainerConstants.APP_SERVER_TOMCAT9)
|
||||||
public class SamlXMLAttacksTest extends AbstractSamlTest {
|
public class SamlXMLAttacksTest extends AbstractSamlTest {
|
||||||
|
|
||||||
@Test(timeout = 4000)
|
@Test(timeout = 4000)
|
||||||
|
@ -64,4 +86,71 @@ public class SamlXMLAttacksTest extends AbstractSamlTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deployment(name = "DTD")
|
||||||
|
protected static WebArchive employee() {
|
||||||
|
String attackerDtd = "<!ENTITY % file SYSTEM \"file:///etc/passwd\">\n" +
|
||||||
|
"<!ENTITY % eval \"<!ENTITY % error SYSTEM 'file:///nonexistent/%file;'>\">\n" +
|
||||||
|
"%eval;\n" +
|
||||||
|
"%error;";
|
||||||
|
|
||||||
|
return ShrinkWrap.create(WebArchive.class, "dtd.war")
|
||||||
|
.add(new StringAsset(attackerDtd), "/attacker.dtd");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertBlackboxInputValidation(String s, Matcher<? super CloseableHttpResponse> matcher) throws IOException, RuntimeException {
|
||||||
|
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
|
||||||
|
HttpPost post = new HttpPost(getAuthServerSamlEndpoint(REALM_NAME));
|
||||||
|
|
||||||
|
List<NameValuePair> parameters = new LinkedList<>();
|
||||||
|
String encoded = PostBindingUtil.base64Encode(s);
|
||||||
|
parameters.add(new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY, encoded));
|
||||||
|
|
||||||
|
UrlEncodedFormEntity formEntity;
|
||||||
|
try {
|
||||||
|
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
post.setEntity(formEntity);
|
||||||
|
|
||||||
|
try (CloseableHttpResponse response = client.execute(post)) {
|
||||||
|
assertThat(response, matcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidator(@ArquillianResource @OperateOnDeployment("DTD") URL attackerDtdUrl) throws Exception {
|
||||||
|
String preamble = "<!DOCTYPE AuthnRequest [" +
|
||||||
|
"<!ELEMENT AuthnRequest (#PCDATA)>" +
|
||||||
|
"<!ENTITY % sp SYSTEM \"" + attackerDtdUrl + "/attacker.dtd\" >%sp;" +
|
||||||
|
"<!ENTITY heh SYSTEM \"file:///etc/passwd\">" +
|
||||||
|
"]>".replaceAll("//attacker", "/attacker");
|
||||||
|
|
||||||
|
assertBlackboxInputValidation(REQUEST_VALID, statusCodeIsHC(Response.Status.FOUND));
|
||||||
|
|
||||||
|
assertBlackboxInputValidation(REQUEST_INVALID, bodyHC(containsString("Invalid Request")));
|
||||||
|
assertBlackboxInputValidation(preamble + REQUEST_VALID, bodyHC(containsString("Invalid Request")));
|
||||||
|
assertBlackboxInputValidation(preamble + REQUEST_FLAWED, bodyHC(containsString("Invalid Request")));
|
||||||
|
assertBlackboxInputValidation(preamble + REQUEST_FLAWED_LOCAL, bodyHC(containsString("Invalid Request")));
|
||||||
|
assertBlackboxInputValidation(preamble + "<AuthnRequest></AuthnRequest>", bodyHC(containsString("Invalid Request")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String REQUEST_VALID = "<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"a123\" Version=\"2.0\" IssueInstant=\"2014-07-16T23:52:45Z\" >" +
|
||||||
|
"<saml:Issuer>" + SAML_CLIENT_ID_SALES_POST + "</saml:Issuer>" +
|
||||||
|
"</samlp:AuthnRequest>";
|
||||||
|
|
||||||
|
private static final String REQUEST_FLAWED = "<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"&sp;\" Version=\"2.0\" IssueInstant=\"2014-07-16T23:52:45Z\" >" +
|
||||||
|
"<saml:Issuer>" + SAML_CLIENT_ID_SALES_POST + "</saml:Issuer>" +
|
||||||
|
"</samlp:AuthnRequest>";
|
||||||
|
|
||||||
|
private static final String REQUEST_FLAWED_LOCAL = "<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"&heh;\" Version=\"2.0\" IssueInstant=\"2014-07-16T23:52:45Z\" >" +
|
||||||
|
"<saml:Issuer>" + SAML_CLIENT_ID_SALES_POST + "</saml:Issuer>" +
|
||||||
|
"</samlp:AuthnRequest>";
|
||||||
|
|
||||||
|
private static final String REQUEST_INVALID = "<samlp:InvalidAuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"a123\" Version=\"2.0\" IssueInstant=\"2014-07-16T23:52:45Z\" >" +
|
||||||
|
"<saml:Issuer>" + SAML_CLIENT_ID_SALES_POST + "</saml:Issuer>" +
|
||||||
|
"</samlp:AuthnRequest>";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue