/* * 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()); } }