Skip to content

Commit 8afc5d1

Browse files
author
Franz Forsthofer
committed
CAMEL-10894: DTD handling in the XML Validator corrected
1 parent 927244d commit 8afc5d1

File tree

4 files changed

+221
-2
lines changed

4 files changed

+221
-2
lines changed

camel-core/src/main/java/org/apache/camel/processor/validation/SchemaReader.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030

3131
import org.w3c.dom.ls.LSResourceResolver;
3232
import org.xml.sax.SAXException;
33-
3433
import org.apache.camel.CamelContext;
3534
import org.apache.camel.converter.IOConverter;
3635
import org.apache.camel.util.IOHelper;
@@ -46,8 +45,12 @@
4645
*/
4746
public class SchemaReader {
4847

48+
/** Key of the global option to switch either off or on the access to external DTDs in the XML Validator for StreamSources.
49+
* Only effective, if not a custom schema factory is used.*/
50+
public static final String ACCESS_EXTERNAL_DTD = "CamelXmlValidatorAccessExternalDTD";
51+
4952
private static final Logger LOG = LoggerFactory.getLogger(SchemaReader.class);
50-
53+
5154
private String schemaLanguage = XMLConstants.W3C_XML_SCHEMA_NS_URI;
5255
// must be volatile because is accessed from different threads see ValidatorEndpoint.clearCachedSchema
5356
private volatile Schema schema;
@@ -169,6 +172,14 @@ protected SchemaFactory createSchemaFactory() {
169172
SchemaFactory factory = SchemaFactory.newInstance(schemaLanguage);
170173
if (getResourceResolver() != null) {
171174
factory.setResourceResolver(getResourceResolver());
175+
}
176+
if (!Boolean.parseBoolean(camelContext.getGlobalOptions().get(ACCESS_EXTERNAL_DTD))) {
177+
try {
178+
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
179+
} catch (SAXException e) {
180+
LOG.error(e.getMessage(), e);
181+
throw new IllegalStateException(e);
182+
}
172183
}
173184
return factory;
174185
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.camel.component.validator;
18+
19+
import java.net.UnknownHostException;
20+
21+
import org.apache.camel.ContextTestSupport;
22+
import org.apache.camel.ValidationException;
23+
import org.apache.camel.builder.RouteBuilder;
24+
import org.apache.camel.component.mock.MockEndpoint;
25+
import org.apache.camel.processor.validation.SchemaReader;
26+
27+
public abstract class ValidatorDtdAccessAbstractTest extends ContextTestSupport {
28+
29+
protected MockEndpoint finallyEndpoint;
30+
protected MockEndpoint invalidEndpoint;
31+
protected MockEndpoint unknownHostExceptionEndpoint;
32+
protected MockEndpoint validEndpoint;
33+
34+
protected String payloud = getPayloudPart("Hello world!");
35+
36+
protected String ssrfPayloud = "<!DOCTYPE roottag PUBLIC \"-//VSR//PENTEST//EN\" \"http://notexisting/test\">\n" + payloud;
37+
38+
protected String xxePayloud = "<!DOCTYPE updateProfile [<!ENTITY file SYSTEM \"http://notexistinghost/test\">]>\n" + getPayloudPart("&file;");
39+
40+
private final boolean accessExternalDTD;
41+
42+
public ValidatorDtdAccessAbstractTest(boolean accessExternalDTD) {
43+
this.accessExternalDTD = accessExternalDTD;
44+
}
45+
46+
47+
private String getPayloudPart(String bodyValue) {
48+
return "<mail xmlns='http://foo.com/bar'><subject>Hey</subject><body>" + bodyValue + "</body></mail>";
49+
}
50+
51+
52+
@Override
53+
protected void setUp() throws Exception {
54+
super.setUp();
55+
56+
validEndpoint = resolveMandatoryEndpoint("mock:valid", MockEndpoint.class);
57+
invalidEndpoint = resolveMandatoryEndpoint("mock:invalid", MockEndpoint.class);
58+
unknownHostExceptionEndpoint = resolveMandatoryEndpoint("mock:unknownHostException", MockEndpoint.class);
59+
finallyEndpoint = resolveMandatoryEndpoint("mock:finally", MockEndpoint.class);
60+
}
61+
62+
@Override
63+
protected RouteBuilder createRouteBuilder() throws Exception {
64+
return new RouteBuilder() {
65+
66+
@Override
67+
public void configure() throws Exception {
68+
// switch on DTD Access
69+
if (accessExternalDTD) {
70+
getContext().getGlobalOptions().put(SchemaReader.ACCESS_EXTERNAL_DTD, "true");
71+
}
72+
from("direct:start")
73+
.doTry()
74+
.to("validator:org/apache/camel/component/validator/schema.xsd")
75+
.to("mock:valid")
76+
.doCatch(ValidationException.class)
77+
.to("mock:invalid")
78+
.doCatch(UnknownHostException.class)
79+
.to("mock:unknownHostException")
80+
.doFinally()
81+
.to("mock:finally").end();
82+
}
83+
};
84+
}
85+
86+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.camel.component.validator;
18+
19+
import java.io.ByteArrayInputStream;
20+
import java.io.InputStream;
21+
import java.nio.charset.StandardCharsets;
22+
23+
import org.apache.camel.component.mock.MockEndpoint;
24+
25+
public class ValidatorDtdAccessOffTest extends ValidatorDtdAccessAbstractTest {
26+
27+
public ValidatorDtdAccessOffTest() {
28+
super(false);
29+
}
30+
31+
/** Tests that no external DTD call is executed for StringSource. */
32+
public void testInvalidMessageWithExternalDTDStringSource() throws Exception {
33+
invalidEndpoint.expectedMessageCount(1);
34+
finallyEndpoint.expectedMessageCount(1);
35+
36+
template.sendBody("direct:start", ssrfPayloud);
37+
38+
MockEndpoint.assertIsSatisfied(validEndpoint, unknownHostExceptionEndpoint, finallyEndpoint);
39+
}
40+
41+
/** Tests that external DTD call is not executed for StreamSource. */
42+
public void testInvalidMessageWithExternalDTDStreamSource() throws Exception {
43+
invalidEndpoint.expectedMessageCount(1);
44+
finallyEndpoint.expectedMessageCount(1);
45+
InputStream is = new ByteArrayInputStream(ssrfPayloud.getBytes(StandardCharsets.UTF_8));
46+
template.sendBody("direct:start", is);
47+
48+
MockEndpoint.assertIsSatisfied(validEndpoint, unknownHostExceptionEndpoint, finallyEndpoint);
49+
}
50+
51+
/** Tests that XXE is not possible for StreamSource. */
52+
public void testInvalidMessageXXESourceStream() throws Exception {
53+
invalidEndpoint.expectedMessageCount(1);
54+
finallyEndpoint.expectedMessageCount(1);
55+
InputStream is = new ByteArrayInputStream(xxePayloud.getBytes(StandardCharsets.UTF_8));
56+
template.sendBody("direct:start", is);
57+
58+
MockEndpoint.assertIsSatisfied(validEndpoint, unknownHostExceptionEndpoint, finallyEndpoint);
59+
}
60+
61+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.camel.component.validator;
18+
19+
import java.io.ByteArrayInputStream;
20+
import java.io.InputStream;
21+
import java.nio.charset.StandardCharsets;
22+
23+
import org.apache.camel.component.mock.MockEndpoint;
24+
25+
public class ValidatorDtdAccessOnTest extends ValidatorDtdAccessAbstractTest {
26+
27+
public ValidatorDtdAccessOnTest() {
28+
super(true);
29+
}
30+
31+
/** Tests that external DTD call is executed for StringSource by expecting an UnkonwHostException. */
32+
public void testInvalidMessageWithExternalDTDStringSource() throws Exception {
33+
unknownHostExceptionEndpoint.expectedMessageCount(1);
34+
finallyEndpoint.expectedMessageCount(1);
35+
36+
template.sendBody("direct:start", ssrfPayloud);
37+
38+
MockEndpoint.assertIsSatisfied(validEndpoint, unknownHostExceptionEndpoint, finallyEndpoint);
39+
}
40+
41+
/** Tests that external DTD call is executed for StreamSourceby expecting an UnkonwHostException. */
42+
public void testInvalidMessageWithExternalDTDStreamSource() throws Exception {
43+
unknownHostExceptionEndpoint.expectedMessageCount(1);
44+
finallyEndpoint.expectedMessageCount(1);
45+
InputStream is = new ByteArrayInputStream(ssrfPayloud.getBytes(StandardCharsets.UTF_8));
46+
template.sendBody("direct:start", is);
47+
48+
MockEndpoint.assertIsSatisfied(validEndpoint, unknownHostExceptionEndpoint, finallyEndpoint);
49+
}
50+
51+
/** Tests that XXE is possible for StreamSource by expecting an UnkonwHostException. */
52+
public void testInvalidMessageXXESourceStream() throws Exception {
53+
unknownHostExceptionEndpoint.expectedMessageCount(1);
54+
finallyEndpoint.expectedMessageCount(1);
55+
InputStream is = new ByteArrayInputStream(xxePayloud.getBytes(StandardCharsets.UTF_8));
56+
template.sendBody("direct:start", is);
57+
58+
MockEndpoint.assertIsSatisfied(validEndpoint, unknownHostExceptionEndpoint, finallyEndpoint);
59+
}
60+
61+
}

0 commit comments

Comments
 (0)