/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 utils.xml;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.fail;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import com.amazonaws.util.XmlUtils;
public class PortSwiggerXxeTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private static String getFile(String name) {
return PortSwiggerXxeTests.class.getResource("/resources/xml/xxe/" + name).getFile();
}
// https://portswigger.net/web-security/xxe#exploiting-xxe-to-retrieve-files
@Test
public void xxe_retrieve_file() throws ParserConfigurationException, SAXException, IOException {
thrown.expect(SAXParseException.class);
thrown.expectMessage("DOCTYPE is disallowed when the feature \"http://apache.org/xml/features/disallow-doctype-decl\" set to true.");
String xml = String.format("\n" +
" ]>\n" +
"&xxe;",
getFile("sensitive.txt"));
System.out.println("xml " + xml);
XmlUtils.parse(
new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)), new DefaultHandler());
}
// https://portswigger.net/web-security/xxe#exploiting-xxe-to-perform-ssrf-attacks
@Test
public void xxe_ssrf() throws IOException, ParserConfigurationException, SAXException {
thrown.expect(SAXParseException.class);
thrown.expectMessage("DOCTYPE is disallowed when the feature \"http://apache.org/xml/features/disallow-doctype-decl\" set to true.");
TestHttpServer server = new TestHttpServer("data");
String xml = String.format("\n" +
" ]>\n" +
"&xxe;", server.url());
try {
XmlUtils.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
new DefaultHandler());
} finally {
assertThat(server.accepted(), equalTo(false));
server.stop();
}
}
// https://portswigger.net/web-security/xxe#xinclude-attacks
@Test
@Ignore // Can't find a good way to test this. Looks like this would be handled by startElement
public void xxe_xinclude() throws ParserConfigurationException, SAXException, IOException, TransformerException {
thrown.expect(SAXParseException.class);
thrown.expectMessage("DOCTYPE is disallowed when the feature \"http://apache.org/xml/features/disallow-doctype-decl\" set to true.");
String xml = String.format("" +
"",
getFile("sensitive.txt"));
final AtomicBoolean skippedXInclude = new AtomicBoolean(true);
XmlUtils.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
new DefaultHandler() {
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if (localName.contains("include")) {
skippedXInclude.set(false);
}
super.startElement(uri, localName, qName, attributes);
}
});
assertThat(skippedXInclude.get(), equalTo(true));
}
// https://portswigger.net/web-security/xxe/blind#detecting-blind-xxe-using-out-of-band-oast-techniques
@Test
public void blind_xxe_oast_parameter() throws IOException, ParserConfigurationException, SAXException {
thrown.expect(SAXParseException.class);
thrown.expectMessage("DOCTYPE is disallowed when the feature \"http://apache.org/xml/features/disallow-doctype-decl\" set to true.");
TestHttpServer server = new TestHttpServer("data");
try {
String xml = String.format(" %%xxe; ]>\n" +
"1234", server.url());
XmlUtils.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
new DefaultHandler());
} finally {
assertThat(server.accepted(), equalTo(false));
server.stop();
}
}
// https://portswigger.net/web-security/xxe/blind#exploiting-blind-xxe-to-exfiltrate-data-out-of-band
@Test
public void blind_xxe_dtd() throws IOException, ParserConfigurationException, SAXException {
thrown.expect(SAXParseException.class);
thrown.expectMessage("DOCTYPE is disallowed when the feature \"http://apache.org/xml/features/disallow-doctype-decl\" set to true.");
String dtd = "\n" +
"\">\n" +
"%eval;\n" +
"%exfiltrate;\n";
TestHttpServer server = new TestHttpServer(dtd);
try {
String xml = String.format(" %%xxe;]>",
server.url());
XmlUtils.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
new DefaultHandler());
} finally {
assertThat(server.accepted(), equalTo(false));
server.stop();
}
}
// https://portswigger.net/web-security/xxe/blind#exploiting-blind-xxe-to-retrieve-data-via-error-messages
@Test
public void blind_xxe_data_via_error() throws IOException, ParserConfigurationException, SAXException {
thrown.expect(SAXParseException.class);
thrown.expectMessage("DOCTYPE is disallowed when the feature \"http://apache.org/xml/features/disallow-doctype-decl\" set to true.");
String dtd = String.format("\n" +
"\">\n" +
"%%eval;\n" +
"%%exfil;",
getFile("/sensitive.txt"));
TestHttpServer server = new TestHttpServer(dtd);
try {
String xml = String.format(" %%xxe;]>", server.url());
XmlUtils.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
new DefaultHandler());
} finally {
assertThat(server.accepted(), equalTo(false));
server.stop();
}
}
// https://portswigger.net/web-security/xxe/blind#exploiting-blind-xxe-by-repurposing-a-local-dtd
@Test
public void blind_xxe_local_repurpose_local_dtd() throws ParserConfigurationException, SAXException, IOException {
thrown.expect(SAXParseException.class);
thrown.expectMessage("DOCTYPE is disallowed when the feature \"http://apache.org/xml/features/disallow-doctype-decl\" set to true.");
String xml = String.format("\n" +
"\n" +
"\">\n" +
"%eval;\n" +
"%error;\n" +
"'>\n" +
"%%local_dtd;\n" +
"]>",
getFile("/local.dtd"), getFile("/sensitive.txt"));
XmlUtils.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
new DefaultHandler());
}
// Existing tests from XpathUtils below this line
@Test
public void testExternalEntity() throws Exception {
thrown.expect(SAXParseException.class);
thrown.expectMessage("DOCTYPE is disallowed when the feature \"http://apache.org/xml/features/disallow-doctype-decl\" set to true.");
TestHttpServer server = new TestHttpServer("secret");
try {
String xml = String.format(
"\n"
+ "\n"
+ "]>\n"
+ "&foo;",
server.url());
XmlUtils.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)), new DefaultHandler());
} finally {
if (server.accepted()) {
fail("Oops! The server has been reached!");
}
server.stop();
}
}
@Test
public void testExternalSchema() throws Exception {
thrown.expect(SAXParseException.class);
thrown.expectMessage("DOCTYPE is disallowed when the feature \"http://apache.org/xml/features/disallow-doctype-decl\" set to true.");
TestHttpServer server = new TestHttpServer("secret");
try {
String xml = String.format(
"\n"
+ "\n"
+ "",
server.url());
XmlUtils.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
new DefaultHandler());
} finally {
if (server.accepted()) {
fail("Oops! The server has been reached!");
}
server.stop();
}
}
@Test
public void testExternalEntityParameter() throws Exception {
thrown.expect(SAXParseException.class);
thrown.expectMessage("DOCTYPE is disallowed when the feature \"http://apache.org/xml/features/disallow-doctype-decl\" set to true.");
TestHttpServer server = new TestHttpServer("secret");
try {
String xml = String.format(
"\n"
+ "\n"
+ "%%sp;"
+ "]>\n"
+ "",
server.url());
XmlUtils.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
new DefaultHandler());
} finally {
server.stop();
if (server.accepted()) {
fail("Oops! The server has been reached!");
}
}
}
@Test
public void billionLaughs() throws Exception {
thrown.expect(SAXParseException.class);
thrown.expectMessage("DOCTYPE is disallowed when the feature \"http://apache.org/xml/features/disallow-doctype-decl\" set to true.");
String xml =
"\n" +
"\n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
"]>\n" +
"&lol9;";
XmlUtils.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
new DefaultHandler());
}
}