1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.commons.xml.secure;
20
21 import static org.apache.commons.xml.secure.AttackTestSupport.LEAKED_MARKER;
22 import static org.apache.commons.xml.secure.AttackTestSupport.captureCharacters;
23 import static org.apache.commons.xml.secure.AttackTestSupport.inputSource;
24 import static org.apache.commons.xml.secure.AttackTestSupport.resourceUrl;
25 import static org.junit.jupiter.api.Assertions.assertEquals;
26 import static org.junit.jupiter.api.Assertions.assertFalse;
27 import static org.junit.jupiter.api.Assertions.assertThrows;
28 import static org.junit.jupiter.api.Assertions.assertTrue;
29
30 import java.io.StringReader;
31
32 import javax.xml.parsers.DocumentBuilder;
33 import javax.xml.parsers.DocumentBuilderFactory;
34 import javax.xml.parsers.SAXParserFactory;
35
36 import org.junit.jupiter.api.Assumptions;
37 import org.junit.jupiter.api.Tag;
38 import org.junit.jupiter.api.Test;
39 import org.w3c.dom.Document;
40 import org.xml.sax.EntityResolver;
41 import org.xml.sax.InputSource;
42 import org.xml.sax.SAXException;
43 import org.xml.sax.XMLReader;
44
45
46
47
48
49
50
51
52
53
54
55
56 class XIncludeTest {
57
58
59
60
61
62
63 private static final class AllowListResolver implements EntityResolver {
64
65 @Override
66 public InputSource resolveEntity(final String publicId, final String systemId) {
67 final InputSource source;
68 if (REFERENCED_XML.equals(systemId)) {
69 source = new InputSource(new StringReader("<allowed>" + RESOLVED_MARKER + "</allowed>"));
70 } else if (REFERENCED_TEXT.equals(systemId)) {
71 source = new InputSource(new StringReader(RESOLVED_MARKER));
72 } else {
73 return null;
74 }
75 source.setPublicId(publicId);
76 source.setSystemId(systemId);
77 return source;
78 }
79 }
80
81
82
83
84 private static final String REFERENCED_XML = resourceUrl("referenced.xml").toString();
85
86
87
88
89 private static final String REFERENCED_TEXT = resourceUrl("referenced.txt").toString();
90
91
92
93
94 private static final String RESOLVED_MARKER = "XINCLUDE-RESOLVED-905bbbce-16ee-4a0c-b165-d1f8c663934c";
95
96
97
98
99 private static final EntityResolver NO_OP_RESOLVER = (publicId, systemId) -> null;
100
101
102
103
104
105
106
107
108 private static void assumeXIncludeAware(final DocumentBuilderFactory factory) {
109 try {
110 factory.setXIncludeAware(true);
111 } catch (final UnsupportedOperationException e) {
112 Assumptions.abort("XInclude not supported on this platform");
113 }
114 }
115
116
117
118
119
120
121
122
123 private static void assumeXIncludeAware(final SAXParserFactory factory) {
124 try {
125 factory.setXIncludeAware(true);
126 } catch (final UnsupportedOperationException e) {
127 Assumptions.abort("XInclude not supported on this platform");
128 }
129 }
130
131
132
133
134 private static String xiIncludeXml(final String href, final String parseMode) {
135 return "<?xml version=\"1.0\"?>\n"
136 + "<root xmlns:xi=\"http://www.w3.org/2001/XInclude\">\n"
137 + " <xi:include href=\"" + href + "\" parse=\"" + parseMode + "\"/>\n"
138 + "</root>";
139 }
140
141 @Test
142 @Tag("dom")
143 void baselineDomLeaksParseText() throws Exception {
144 final InputSource input = inputSource(xiIncludeXml(REFERENCED_TEXT, "text"));
145
146 final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
147 factory.setNamespaceAware(true);
148 assumeXIncludeAware(factory);
149 final Document doc = factory.newDocumentBuilder().parse(input);
150 final String text = doc.getDocumentElement().getTextContent();
151 assertTrue(text != null && text.contains(LEAKED_MARKER),
152 "Baseline DOM parse=text should leak marker; got: " + text);
153 }
154
155 @Test
156 @Tag("dom")
157 void baselineDomLeaksParseXml() throws Exception {
158 final InputSource input = inputSource(xiIncludeXml(REFERENCED_XML, "xml"));
159
160 final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
161 factory.setNamespaceAware(true);
162 assumeXIncludeAware(factory);
163 final Document doc = factory.newDocumentBuilder().parse(input);
164 final String text = doc.getDocumentElement().getTextContent();
165 assertEquals(LEAKED_MARKER, text.trim(),
166 "Baseline DOM parse=xml should leak marker; got: " + text);
167 }
168
169 @Test
170 @Tag("sax")
171 void baselineSaxLeaksParseText() throws Exception {
172 final String input = xiIncludeXml(REFERENCED_TEXT, "text");
173
174 final SAXParserFactory factory = SAXParserFactory.newInstance();
175 factory.setNamespaceAware(true);
176 assumeXIncludeAware(factory);
177 final String captured = captureCharacters(factory.newSAXParser().getXMLReader(), input);
178 assertTrue(captured.contains(LEAKED_MARKER),
179 "Baseline SAX parse=text should leak marker; got: " + captured);
180 }
181
182 @Test
183 @Tag("sax")
184 void baselineSaxLeaksParseXml() throws Exception {
185 final String input = xiIncludeXml(REFERENCED_XML, "xml");
186
187 final SAXParserFactory factory = SAXParserFactory.newInstance();
188 factory.setNamespaceAware(true);
189 assumeXIncludeAware(factory);
190 final String captured = captureCharacters(factory.newSAXParser().getXMLReader(), input);
191 assertEquals(LEAKED_MARKER, captured.trim(),
192 "Baseline SAX parse=xml should leak marker; got: " + captured);
193 }
194
195 @Test
196 @Tag("dom")
197 void secureDomBlocksParseText() throws Exception {
198 final InputSource input = inputSource(xiIncludeXml(REFERENCED_TEXT, "text"));
199
200 final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newInstance();
201 factory.setNamespaceAware(true);
202 assumeXIncludeAware(factory);
203 final Document doc = factory.newDocumentBuilder().parse(input);
204 final String text = doc.getDocumentElement().getTextContent();
205 assertFalse(text.contains(LEAKED_MARKER),
206 "Secured DOM parse=text must resolve the include to empty, not leak; got: " + text);
207 }
208
209 @Test
210 @Tag("dom")
211 void secureDomBlocksParseXml() {
212 final InputSource input = inputSource(xiIncludeXml(REFERENCED_XML, "xml"));
213
214 final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newInstance();
215 factory.setNamespaceAware(true);
216 assumeXIncludeAware(factory);
217 assertThrows(SAXException.class, () -> {
218 final DocumentBuilder builder = factory.newDocumentBuilder();
219 builder.parse(input);
220 }, "Secured DOM parse=xml should throw");
221 }
222
223 @Test
224 @Tag("dom")
225 void secureDomNullResolverDoesNotLeak() throws Exception {
226 final InputSource input = inputSource(xiIncludeXml(REFERENCED_XML, "xml"));
227
228 final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newInstance();
229 factory.setNamespaceAware(true);
230 assumeXIncludeAware(factory);
231 final DocumentBuilder builder = factory.newDocumentBuilder();
232 builder.setEntityResolver(NO_OP_RESOLVER);
233 assertThrows(SAXException.class, () -> builder.parse(input),
234 "a resolver that returns null must not leak: the ignore-all floor blocks the real href");
235 }
236
237 @Test
238 @Tag("dom")
239 void secureDomWithAllowListResolvesParseText() throws Exception {
240 final InputSource input = inputSource(xiIncludeXml(REFERENCED_TEXT, "text"));
241
242 final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newInstance();
243 factory.setNamespaceAware(true);
244 assumeXIncludeAware(factory);
245 final DocumentBuilder builder = factory.newDocumentBuilder();
246 builder.setEntityResolver(new AllowListResolver());
247 final Document doc = builder.parse(input);
248 assertEquals(RESOLVED_MARKER, doc.getDocumentElement().getTextContent().trim(),
249 "DOM parse=text with allow-list should resolve to the resolver's content");
250 }
251
252 @Test
253 @Tag("dom")
254 void secureDomWithAllowListResolvesParseXml() throws Exception {
255 final InputSource input = inputSource(xiIncludeXml(REFERENCED_XML, "xml"));
256
257 final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newInstance();
258 factory.setNamespaceAware(true);
259 assumeXIncludeAware(factory);
260 final DocumentBuilder builder = factory.newDocumentBuilder();
261 builder.setEntityResolver(new AllowListResolver());
262 final Document doc = builder.parse(input);
263 assertEquals(RESOLVED_MARKER, doc.getDocumentElement().getTextContent().trim(),
264 "DOM parse=xml with allow-list should resolve to the resolver's content");
265 }
266
267 @Test
268 @Tag("sax")
269 void secureReaderAllowListResolvesParseXml() throws Exception {
270 final String input = xiIncludeXml(REFERENCED_XML, "xml");
271
272 final SAXParserFactory unsecuredFactory = SAXParserFactory.newInstance();
273 unsecuredFactory.setNamespaceAware(true);
274 assumeXIncludeAware(unsecuredFactory);
275 final XMLReader reader = SecureSAXParserFactory.secure(unsecuredFactory.newSAXParser().getXMLReader());
276 reader.setEntityResolver(new AllowListResolver());
277 final String captured = captureCharacters(reader, input);
278 assertEquals(RESOLVED_MARKER, captured.trim(),
279 "hardenReader + allow-list should resolve to the resolver's content on a reader with XInclude already enabled");
280 }
281
282 @Test
283 @Tag("sax")
284 void secureReaderBlocksParseText() throws Exception {
285 final String input = xiIncludeXml(REFERENCED_TEXT, "text");
286
287 final SAXParserFactory unsecuredFactory = SAXParserFactory.newInstance();
288 unsecuredFactory.setNamespaceAware(true);
289 assumeXIncludeAware(unsecuredFactory);
290 final XMLReader reader = SecureSAXParserFactory.secure(unsecuredFactory.newSAXParser().getXMLReader());
291 final String captured = captureCharacters(reader, input);
292 assertFalse(captured.contains(LEAKED_MARKER),
293 "hardenReader parse=text must resolve the include to empty, not leak; got: " + captured);
294 }
295
296 @Test
297 @Tag("sax")
298 void secureReaderBlocksParseXml() throws Exception {
299 final InputSource input = inputSource(xiIncludeXml(REFERENCED_XML, "xml"));
300
301
302 final SAXParserFactory unsecuredFactory = SAXParserFactory.newInstance();
303 unsecuredFactory.setNamespaceAware(true);
304 assumeXIncludeAware(unsecuredFactory);
305 final XMLReader reader = SecureSAXParserFactory.secure(unsecuredFactory.newSAXParser().getXMLReader());
306 assertThrows(SAXException.class, () -> reader.parse(input),
307 "hardenReader should block XInclude parse=xml on reader with XInclude already enabled");
308 }
309
310 @Test
311 @Tag("sax")
312 void secureSaxBlocksParseText() throws Exception {
313 final String input = xiIncludeXml(REFERENCED_TEXT, "text");
314
315 final SAXParserFactory factory = SecureSAXParserFactory.newInstance();
316 factory.setNamespaceAware(true);
317 assumeXIncludeAware(factory);
318 final String captured = captureCharacters(factory.newSAXParser().getXMLReader(), input);
319 assertFalse(captured.contains(LEAKED_MARKER),
320 "Secured SAX parse=text must resolve the include to empty, not leak; got: " + captured);
321 }
322
323 @Test
324 @Tag("sax")
325 void secureSaxBlocksParseXml() {
326 final InputSource input = inputSource(xiIncludeXml(REFERENCED_XML, "xml"));
327
328 final SAXParserFactory factory = SecureSAXParserFactory.newInstance();
329 factory.setNamespaceAware(true);
330 assumeXIncludeAware(factory);
331 assertThrows(SAXException.class, () -> {
332 final XMLReader reader = factory.newSAXParser().getXMLReader();
333 reader.parse(input);
334 }, "Secured SAX parse=xml should throw");
335 }
336
337 @Test
338 @Tag("sax")
339 void secureSaxNullResolverDoesNotLeak() throws Exception {
340 final InputSource input = inputSource(xiIncludeXml(REFERENCED_XML, "xml"));
341
342 final SAXParserFactory factory = SecureSAXParserFactory.newInstance();
343 factory.setNamespaceAware(true);
344 assumeXIncludeAware(factory);
345 final XMLReader reader = factory.newSAXParser().getXMLReader();
346 reader.setEntityResolver(NO_OP_RESOLVER);
347 assertThrows(SAXException.class, () -> reader.parse(input),
348 "a resolver that returns null must not leak: the ignore-all floor blocks the real href");
349 }
350
351 @Test
352 @Tag("sax")
353 void secureSaxWithAllowListResolvesParseText() throws Exception {
354 final String input = xiIncludeXml(REFERENCED_TEXT, "text");
355
356 final SAXParserFactory factory = SecureSAXParserFactory.newInstance();
357 factory.setNamespaceAware(true);
358 assumeXIncludeAware(factory);
359 final XMLReader reader = factory.newSAXParser().getXMLReader();
360 reader.setEntityResolver(new AllowListResolver());
361 final String captured = captureCharacters(reader, input);
362 assertEquals(RESOLVED_MARKER, captured.trim(),
363 "SAX parse=text with allow-list should resolve to the resolver's content");
364 }
365
366 @Test
367 @Tag("sax")
368 void secureSaxWithAllowListResolvesParseXml() throws Exception {
369 final String input = xiIncludeXml(REFERENCED_XML, "xml");
370
371 final SAXParserFactory factory = SecureSAXParserFactory.newInstance();
372 factory.setNamespaceAware(true);
373 assumeXIncludeAware(factory);
374 final XMLReader reader = factory.newSAXParser().getXMLReader();
375 reader.setEntityResolver(new AllowListResolver());
376 final String captured = captureCharacters(reader, input);
377 assertEquals(RESOLVED_MARKER, captured.trim(),
378 "SAX parse=xml with allow-list should resolve to the resolver's content");
379 }
380
381 }