Skip to content

Commit 60abc95

Browse files
authored
TIKA-4471 -- add unit tests to confirm defense against xxe in sax, dom and stax. (#2318)
1 parent b70bb34 commit 60abc95

1 file changed

Lines changed: 163 additions & 26 deletions

File tree

tika-core/src/test/java/org/apache/tika/utils/XMLReaderUtilsTest.java

Lines changed: 163 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,54 +16,191 @@
1616
*/
1717
package org.apache.tika.utils;
1818

19+
import static org.junit.jupiter.api.Assertions.assertEquals;
1920
import static org.junit.jupiter.api.Assertions.fail;
2021

2122
import java.io.ByteArrayInputStream;
2223
import java.net.ConnectException;
2324
import java.nio.charset.StandardCharsets;
25+
import java.util.NoSuchElementException;
26+
import javax.xml.stream.XMLEventReader;
27+
import javax.xml.stream.XMLStreamException;
2428

2529
import org.junit.jupiter.api.Test;
30+
import org.w3c.dom.Document;
31+
import org.w3c.dom.Node;
32+
import org.w3c.dom.NodeList;
33+
import org.xml.sax.SAXException;
2634

2735
import org.apache.tika.parser.ParseContext;
2836
import org.apache.tika.sax.ToTextContentHandler;
2937

3038
public class XMLReaderUtilsTest {
39+
40+
private static final String EXTERNAL_DTD_SIMPLE_FILE = "<?xml version=\"1.0\" standalone=\"no\"?><!DOCTYPE foo SYSTEM \"tutorials.dtd\"><foo/>";
41+
private static final String EXTERNAL_DTD_SIMPLE_URL = "<?xml version=\"1.0\" standalone=\"no\"?><!DOCTYPE foo SYSTEM \"http://127.234.172.38:7845/bar\"><foo/>";
42+
private static final String EXTERNAL_ENTITY = "<!DOCTYPE foo [" + " <!ENTITY bar SYSTEM \"http://127.234.172.38:7845/bar\">" +
43+
" ]><foo>&bar;</foo>";
44+
private static final String EXTERNAL_LOCAL_DTD = "<!DOCTYPE foo [" +
45+
"<!ENTITY % local_dtd SYSTEM \"file:///usr/local/app/schema.dtd\">" +
46+
"%local_dtd;]><foo/>";
47+
48+
private static final String BILLION_LAUGHS_CLASSICAL = "<?xml version=\"1.0\"?>\n" + "<!DOCTYPE lolz [\n" + " <!ENTITY lol \"lol\">\n" + " <!ELEMENT lolz (#PCDATA)>\n" +
49+
" <!ENTITY lol1 \"&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;\">\n" + " <!ENTITY lol2 \"&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;\">\n" +
50+
" <!ENTITY lol3 \"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;\">\n" +
51+
" <!ENTITY lol4 \"&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;\">\n" +
52+
" <!ENTITY lol5 \"&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;\">\n" +
53+
" <!ENTITY lol6 \"&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;\">\n" +
54+
" <!ENTITY lol7 \"&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;\">\n" +
55+
" <!ENTITY lol8 \"&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;\">\n" +
56+
" <!ENTITY lol9 \"&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;\">\n" + "]>\n" + "<lolz>&lol9;</lolz>";
57+
58+
private static String BILLION_LAUGHS_VARIANT;
59+
60+
static {
61+
StringBuilder entity = new StringBuilder();
62+
for (int i = 0; i < 1000000; i++) {
63+
entity.append("a");
64+
}
65+
StringBuilder xml = new StringBuilder();
66+
xml.append("<?xml version=\"1.0\"?>\n" + "<!DOCTYPE kaboom [\n" + " <!ENTITY a \"");
67+
xml.append(entity.toString());
68+
xml.append("\">]>" + "<kaboom>");
69+
for (int i = 0; i < 100000; i++) {
70+
xml.append("&a;");
71+
}
72+
xml.append("</kaboom>");
73+
BILLION_LAUGHS_VARIANT = xml.toString();
74+
}
75+
76+
private static final String[] EXTERNAL_ENTITY_XMLS = new String[]{ EXTERNAL_DTD_SIMPLE_FILE, EXTERNAL_DTD_SIMPLE_URL,
77+
EXTERNAL_ENTITY, EXTERNAL_LOCAL_DTD };
78+
79+
private static final String[] BILLION_LAUGHS = new String[]{ BILLION_LAUGHS_CLASSICAL, BILLION_LAUGHS_VARIANT };
80+
3181
//make sure that parseSAX actually defends against external entities
3282
@Test
33-
public void testExternalDTD() throws Exception {
34-
String xml = "<!DOCTYPE foo SYSTEM \"http://127.234.172.38:7845/bar\"><foo/>";
35-
try {
36-
XMLReaderUtils.parseSAX(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
37-
new ToTextContentHandler(), new ParseContext());
38-
} catch (ConnectException e) {
39-
fail("Parser tried to access the external DTD:" + e);
83+
public void testSAX() throws Exception {
84+
for (String xml : EXTERNAL_ENTITY_XMLS) {
85+
try {
86+
XMLReaderUtils.parseSAX(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
87+
new ToTextContentHandler(), new ParseContext());
88+
} catch (ConnectException e) {
89+
fail("Parser tried to access resource: " + xml, e);
90+
}
91+
}
92+
}
93+
94+
@Test
95+
public void testDOM() throws Exception {
96+
for (String xml : EXTERNAL_ENTITY_XMLS) {
97+
try {
98+
XMLReaderUtils.buildDOM(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)), new ParseContext());
99+
} catch (ConnectException e) {
100+
fail("Parser tried to access resource: " + xml, e);
101+
}
40102
}
41103
}
42104

43105
@Test
44-
public void testExternalEntity() throws Exception {
45-
String xml =
46-
"<!DOCTYPE foo [" + " <!ENTITY bar SYSTEM \"http://127.234.172.38:7845/bar\">" +
47-
" ]><foo>&bar;</foo>";
48-
try {
49-
XMLReaderUtils.parseSAX(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
50-
new ToTextContentHandler(), new ParseContext());
51-
} catch (ConnectException e) {
52-
fail("Parser tried to access the external DTD:" + e);
106+
public void testStax() throws Exception {
107+
for (String xml : EXTERNAL_ENTITY_XMLS) {
108+
try {
109+
javax.xml.stream.XMLInputFactory xmlInputFactory = XMLReaderUtils.getXMLInputFactory(new ParseContext());
110+
XMLEventReader reader = xmlInputFactory.createXMLEventReader(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
111+
StringBuilder sb = new StringBuilder();
112+
while (reader.hasNext()) {
113+
sb.append(reader.next());
114+
}
115+
if (sb.toString().contains("Exception scanning External")) {
116+
fail("tried to read external dtd");
117+
}
118+
} catch (XMLStreamException e) {
119+
fail("StreamException: " + xml, e);
120+
} catch (NoSuchElementException e) {
121+
if (e.getMessage() != null) {
122+
if (e.getMessage().contains("Connection refused")) {
123+
fail("Vulnerable to ssrf via url: " + xml, e);
124+
} else if (e.getMessage().contains("No such file")) {
125+
fail("Vulnerable to local file read via external entity/dtd: " + xml, e);
126+
}
127+
}
128+
}
53129
}
54130
}
55131

56132
@Test
57-
public void testExternalEntityLocal() throws Exception {
58-
String xml =
59-
"<!DOCTYPE foo [" +
60-
"<!ENTITY % local_dtd SYSTEM \"file:///usr/local/app/schema.dtd\">" +
61-
"%local_dtd;]><foo/>";
62-
try {
63-
XMLReaderUtils.parseSAX(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
64-
new ToTextContentHandler(), new ParseContext());
65-
} catch (ConnectException e) {
66-
fail("Parser tried to access the external DTD:" + e);
133+
public void testSAXBillionLaughs() throws Exception {
134+
for (String xml : BILLION_LAUGHS) {
135+
try {
136+
XMLReaderUtils.parseSAX(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
137+
new ToTextContentHandler(), new ParseContext());
138+
} catch (SAXException e) {
139+
if (e.getMessage() != null && e
140+
.getMessage()
141+
.contains("entity expansions")) {
142+
//do nothing
143+
} else {
144+
throw e;
145+
}
146+
}
147+
}
148+
}
149+
150+
@Test
151+
public void testDOMBillionLaughs() throws Exception {
152+
//confirm that ExpandEntityReferences has been set to false.
153+
for (String xml : BILLION_LAUGHS) {
154+
Document doc = XMLReaderUtils.buildDOM(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)), new ParseContext());
155+
NodeList nodeList = doc.getChildNodes();
156+
StringBuilder sb = new StringBuilder();
157+
dumpChildren(nodeList, sb);
158+
assertEquals(0, sb
159+
.toString()
160+
.trim()
161+
.length(), sb.toString());
162+
}
163+
}
164+
165+
private void dumpChildren(NodeList nodeList, StringBuilder sb) {
166+
for (int i = 0; i < nodeList.getLength(); i++) {
167+
Node n = nodeList.item(i);
168+
String txt = n.getTextContent();
169+
if (txt != null) {
170+
sb.append(txt);
171+
}
172+
}
173+
}
174+
175+
@Test
176+
public void testStaxBillionLaughs() throws Exception {
177+
/*
178+
Turning off dtd support of the XMLInputFactory in XMLReaderUtils turns off entity expansions and
179+
causes a "NoSuchElementException" with the "'lol9' was referenced but not declared" message with this line:
180+
tryToSetStaxProperty(factory, XMLInputFactory.SUPPORT_DTD, false);
181+
If that line doesn't exist, then we get a
182+
NoSuchElementException with: "The parser has encountered more than "20" entity expansions in this document; this is the limit imposed by the JDK."
183+
*/
184+
185+
for (String xml : BILLION_LAUGHS) {
186+
javax.xml.stream.XMLInputFactory xmlInputFactory = XMLReaderUtils.getXMLInputFactory(new ParseContext());
187+
XMLEventReader reader = xmlInputFactory.createXMLEventReader(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
188+
try {
189+
while (reader.hasNext()) {
190+
reader.next();
191+
}
192+
} catch (NoSuchElementException e) {
193+
//full message on temurin-17: The entity "lol9" was referenced, but not declared.
194+
if (e.getMessage() != null && e
195+
.getMessage()
196+
.contains("referenced") && e
197+
.getMessage()
198+
.contains("not declared")) {
199+
//swallow -- this is expected
200+
} else {
201+
throw e;
202+
}
203+
}
67204
}
68205
}
69206
}

0 commit comments

Comments
 (0)