1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.xml.secure;
19
20 import static org.junit.jupiter.api.Assertions.assertEquals;
21 import static org.junit.jupiter.api.Assertions.assertFalse;
22 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
23 import static org.junit.jupiter.api.Assertions.assertNotNull;
24 import static org.junit.jupiter.api.Assertions.assertNotSame;
25 import static org.junit.jupiter.api.Assertions.assertNull;
26 import static org.junit.jupiter.api.Assertions.assertSame;
27 import static org.junit.jupiter.api.Assertions.assertThrows;
28 import static org.junit.jupiter.api.Assertions.assertTrue;
29
30 import java.io.ByteArrayInputStream;
31 import java.io.InputStream;
32 import java.io.Reader;
33 import java.io.StringReader;
34 import java.lang.reflect.Constructor;
35 import java.lang.reflect.Proxy;
36 import java.nio.charset.StandardCharsets;
37 import java.util.ArrayList;
38 import java.util.List;
39
40 import javax.xml.stream.EventFilter;
41 import javax.xml.stream.StreamFilter;
42 import javax.xml.stream.XMLEventReader;
43 import javax.xml.stream.XMLInputFactory;
44 import javax.xml.stream.XMLReporter;
45 import javax.xml.stream.XMLResolver;
46 import javax.xml.stream.XMLStreamConstants;
47 import javax.xml.stream.XMLStreamException;
48 import javax.xml.stream.XMLStreamReader;
49 import javax.xml.stream.events.XMLEvent;
50 import javax.xml.stream.util.XMLEventAllocator;
51 import javax.xml.transform.Source;
52 import javax.xml.transform.stream.StreamSource;
53
54 import org.junit.jupiter.api.Assumptions;
55 import org.junit.jupiter.api.Tag;
56 import org.junit.jupiter.api.Test;
57
58
59
60
61
62
63
64
65
66 @Tag("stax")
67 class SecureXMLInputFactoryTest {
68
69
70
71
72
73
74
75
76
77 private static final class RecordingXMLInputFactory extends XMLInputFactory {
78
79
80
81
82 static final XMLEventReader EVENT_SENTINEL = proxy(XMLEventReader.class);
83
84
85
86
87 static final XMLStreamReader STREAM_SENTINEL = proxy(XMLStreamReader.class);
88
89
90
91
92 static final XMLEventAllocator ALLOCATOR_SENTINEL = proxy(XMLEventAllocator.class);
93
94
95
96
97 static final XMLReporter REPORTER_SENTINEL = proxy(XMLReporter.class);
98
99
100
101
102 static final EventFilter EVENT_FILTER_SENTINEL = proxy(EventFilter.class);
103
104
105
106
107 static final StreamFilter STREAM_FILTER_SENTINEL = proxy(StreamFilter.class);
108
109
110
111
112 static String call(final String method, final Object... args) {
113 final StringBuilder entry = new StringBuilder(method).append('(');
114 for (int i = 0; i < args.length; i++) {
115 if (i > 0) {
116 entry.append(", ");
117 }
118 final Object arg = args[i];
119 entry.append(arg == null ? "null" : arg.getClass().getName()).append('@').append(System.identityHashCode(arg));
120 }
121 return entry.append(')').toString();
122 }
123
124
125
126
127
128 private static <T> T proxy(final Class<T> type) {
129 return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[] { type }, (p, method, args) -> {
130 if (method.getReturnType() == boolean.class) {
131 return Boolean.FALSE;
132 }
133 if (method.getReturnType() == int.class) {
134 return 0;
135 }
136 return null;
137 });
138 }
139
140
141
142
143 final List<String> calls = new ArrayList<>();
144
145
146
147
148 Object resolverHook;
149
150
151
152
153 XMLEventAllocator allocator;
154
155
156
157
158 XMLReporter reporter;
159
160
161
162
163 boolean supported = true;
164
165 @Override
166 public XMLEventReader createFilteredReader(final XMLEventReader reader, final EventFilter filter) {
167 record("createFilteredReader", reader, filter);
168 return EVENT_SENTINEL;
169 }
170
171 @Override
172 public XMLStreamReader createFilteredReader(final XMLStreamReader reader, final StreamFilter filter) {
173 record("createFilteredReader", reader, filter);
174 return STREAM_SENTINEL;
175 }
176
177 @Override
178 public XMLEventReader createXMLEventReader(final InputStream stream) {
179 record("createXMLEventReader", stream);
180 return EVENT_SENTINEL;
181 }
182
183 @Override
184 public XMLEventReader createXMLEventReader(final InputStream stream, final String encoding) {
185 record("createXMLEventReader", stream, encoding);
186 return EVENT_SENTINEL;
187 }
188
189 @Override
190 public XMLEventReader createXMLEventReader(final Reader reader) {
191 record("createXMLEventReader", reader);
192 return EVENT_SENTINEL;
193 }
194
195 @Override
196 public XMLEventReader createXMLEventReader(final Source source) {
197 record("createXMLEventReader", source);
198 return EVENT_SENTINEL;
199 }
200
201 @Override
202 public XMLEventReader createXMLEventReader(final String systemId, final InputStream stream) {
203 record("createXMLEventReader", systemId, stream);
204 return EVENT_SENTINEL;
205 }
206
207 @Override
208 public XMLEventReader createXMLEventReader(final String systemId, final Reader reader) {
209 record("createXMLEventReader", systemId, reader);
210 return EVENT_SENTINEL;
211 }
212
213 @Override
214 public XMLEventReader createXMLEventReader(final XMLStreamReader reader) {
215 record("createXMLEventReader", reader);
216 return EVENT_SENTINEL;
217 }
218
219 @Override
220 public XMLStreamReader createXMLStreamReader(final InputStream stream) {
221 record("createXMLStreamReader", stream);
222 return STREAM_SENTINEL;
223 }
224
225 @Override
226 public XMLStreamReader createXMLStreamReader(final InputStream stream, final String encoding) {
227 record("createXMLStreamReader", stream, encoding);
228 return STREAM_SENTINEL;
229 }
230
231 @Override
232 public XMLStreamReader createXMLStreamReader(final Reader reader) {
233 record("createXMLStreamReader", reader);
234 return STREAM_SENTINEL;
235 }
236
237 @Override
238 public XMLStreamReader createXMLStreamReader(final Source source) {
239 record("createXMLStreamReader", source);
240 return STREAM_SENTINEL;
241 }
242
243 @Override
244 public XMLStreamReader createXMLStreamReader(final String systemId, final InputStream stream) {
245 record("createXMLStreamReader", systemId, stream);
246 return STREAM_SENTINEL;
247 }
248
249 @Override
250 public XMLStreamReader createXMLStreamReader(final String systemId, final Reader reader) {
251 record("createXMLStreamReader", systemId, reader);
252 return STREAM_SENTINEL;
253 }
254
255 @Override
256 public XMLEventAllocator getEventAllocator() {
257 record("getEventAllocator");
258 return allocator;
259 }
260
261 @Override
262 public Object getProperty(final String name) {
263 record("getProperty", name);
264 return resolverHook;
265 }
266
267 @Override
268 public XMLReporter getXMLReporter() {
269 record("getXMLReporter");
270 return reporter;
271 }
272
273 @Override
274 public XMLResolver getXMLResolver() {
275 record("getXMLResolver");
276 return (XMLResolver) resolverHook;
277 }
278
279 @Override
280 public boolean isPropertySupported(final String name) {
281 record("isPropertySupported", name);
282 return supported;
283 }
284
285
286
287
288 private void record(final String method, final Object... args) {
289 calls.add(call(method, args));
290 }
291
292 @Override
293 public void setEventAllocator(final XMLEventAllocator eventAllocator) {
294 record("setEventAllocator", eventAllocator);
295 allocator = eventAllocator;
296 }
297
298 @Override
299 public void setProperty(final String name, final Object value) {
300 record("setProperty", name, value);
301 if (value == null || value instanceof XMLResolver) {
302 resolverHook = value;
303 }
304 }
305
306 @Override
307 public void setXMLReporter(final XMLReporter xmlReporter) {
308 record("setXMLReporter", xmlReporter);
309 reporter = xmlReporter;
310 }
311
312 @Override
313 public void setXMLResolver(final XMLResolver resolver) {
314 record("setXMLResolver", resolver);
315 resolverHook = resolver;
316 }
317 }
318
319 private static final String BENIGN_XML = "<?xml version=\"1.0\"?>\n<root><child>hello</child></root>\n";
320
321 private static final String SYSTEM_ID = "http://example.invalid/document.xml";
322
323
324
325
326 private static String drainEvents(final XMLEventReader reader) throws XMLStreamException {
327 final StringBuilder text = new StringBuilder();
328 try {
329 while (reader.hasNext()) {
330 final XMLEvent event = reader.nextEvent();
331 if (event.isCharacters() || event.getEventType() == XMLStreamConstants.CDATA) {
332 text.append(event.asCharacters().getData());
333 }
334 }
335 } finally {
336 reader.close();
337 }
338 return text.toString();
339 }
340
341
342
343
344 private static String drainStream(final XMLStreamReader reader) throws XMLStreamException {
345 final StringBuilder text = new StringBuilder();
346 try {
347 while (reader.hasNext()) {
348 if (reader.next() == XMLStreamConstants.CHARACTERS) {
349 text.append(reader.getText());
350 }
351 }
352 } finally {
353 reader.close();
354 }
355 return text.toString();
356 }
357
358 @Test
359 void getPropertyReportsForeignResolverUnchanged() {
360 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
361 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
362 final XMLResolver foreign = (publicID, systemID, baseURI, namespace) -> "foreign";
363 fake.setProperty(XMLInputFactory.RESOLVER, foreign);
364 assertSame(foreign, secure.getProperty(XMLInputFactory.RESOLVER), "a non-floor resolver must be reported unchanged");
365 assertSame(foreign, secure.getXMLResolver(), "getXMLResolver must report a non-floor resolver unchanged");
366 }
367
368 @Test
369 void getPropertyReportsNullResolver() {
370 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
371 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
372 fake.setProperty(XMLInputFactory.RESOLVER, null);
373 assertNull(secure.getProperty(XMLInputFactory.RESOLVER), "an empty hook must report no resolver");
374 assertNull(secure.getXMLResolver(), "an empty hook must report no resolver");
375 }
376
377 @Test
378 void getXMLResolverInitiallyNull() {
379 assertNull(SecureXMLInputFactory.newInstance().getXMLResolver(), "a fresh secure factory must report no caller resolver");
380 assertNull(SecureXMLInputFactory.newDefaultFactory().getXMLResolver(), "a fresh secure factory must report no caller resolver");
381 }
382
383 @Test
384 void newDefaultFactoryParsesBenignDocument() throws Exception {
385 final XMLInputFactory factory = SecureXMLInputFactory.newDefaultFactory();
386 assertEquals(Boolean.TRUE, factory.getProperty(XMLInputFactory.SUPPORT_DTD), "a secure factory must keep the implementation's DTD default");
387 assertTrue(drainStream(factory.createXMLStreamReader(new StringReader(BENIGN_XML))).contains("hello"), "stream reader must parse the document");
388 assertTrue(drainEvents(factory.createXMLEventReader(new StringReader(BENIGN_XML))).contains("hello"), "event reader must parse the document");
389 }
390
391 @Test
392 void newFactoryNullFactoryIdThrows() {
393 assertThrows(NullPointerException.class, () -> SecureXMLInputFactory.newFactory(null, null), "a null factory id must be rejected");
394 }
395
396 @Test
397 void newFactoryParsesBenignDocument() throws Exception {
398 final XMLInputFactory factory = SecureXMLInputFactory.newFactory();
399 assertTrue(drainStream(factory.createXMLStreamReader(new StringReader(BENIGN_XML))).contains("hello"), "stream reader must parse the document");
400 assertTrue(drainEvents(factory.createXMLEventReader(new StringReader(BENIGN_XML))).contains("hello"), "event reader must parse the document");
401 }
402
403 @Test
404 void newFactoryWithFactoryIdReturnsUsableSecureFactory() throws Exception {
405 final String factoryId = "org.apache.commons.xml.secure.test.inputFactory";
406 System.setProperty(factoryId, XMLInputFactory.newInstance().getClass().getName());
407 try {
408 final XMLInputFactory factory = SecureXMLInputFactory.newFactory(factoryId, getClass().getClassLoader());
409 assertNull(factory.getXMLResolver(), "a fresh secure factory must report no caller resolver");
410 assertTrue(drainStream(factory.createXMLStreamReader(new StringReader(BENIGN_XML))).contains("hello"), "factory must parse the document");
411 } finally {
412 System.clearProperty(factoryId);
413 }
414 }
415
416 @Test
417 void newInstanceParsesBenignDocument() throws Exception {
418 final XMLInputFactory factory = SecureXMLInputFactory.newInstance();
419 assertTrue(drainStream(factory.createXMLStreamReader(new StringReader(BENIGN_XML))).contains("hello"), "stream reader must parse the document");
420 assertTrue(drainEvents(factory.createXMLEventReader(new StringReader(BENIGN_XML))).contains("hello"), "event reader must parse the document");
421 }
422
423 @Test
424 void privateConstructorIsInvokable() throws Exception {
425 final Constructor<SecureXMLInputFactory> constructor = SecureXMLInputFactory.class.getDeclaredConstructor();
426 constructor.setAccessible(true);
427 assertNotNull(constructor.newInstance(), "the private constructor must exist and be invokable");
428 }
429
430 @Test
431 void secureNullDelegateThrows() {
432 assertThrows(NullPointerException.class, () -> SecureXMLInputFactory.secure(null), "a null delegate must be rejected");
433 }
434
435 @Test
436 void setPropertyCallerFloorTakesControl() {
437 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
438 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
439 final XMLResolver caller = (publicID, systemID, baseURI, namespace) -> "resolved";
440 final FallbackIgnoreXMLResolver ownFloor = new FallbackIgnoreXMLResolver(caller);
441 secure.setProperty(XMLInputFactory.RESOLVER, ownFloor);
442 assertSame(ownFloor, fake.resolverHook, "the caller's own floor must be handed to the delegate as-is");
443 assertSame(caller, secure.getXMLResolver(), "getXMLResolver must report the delegate of the caller's floor");
444 assertSame(caller, secure.getProperty(XMLInputFactory.RESOLVER), "getProperty must report the delegate of the caller's floor");
445 }
446
447 @Test
448 void setPropertyNullResolverClearsCallerDelegate() {
449 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
450 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
451 final FallbackIgnoreXMLResolver floor = (FallbackIgnoreXMLResolver) fake.resolverHook;
452 final XMLResolver caller = (publicID, systemID, baseURI, namespace) -> null;
453 secure.setXMLResolver(caller);
454 secure.setProperty(XMLInputFactory.RESOLVER, null);
455 assertNull(floor.getDelegate(), "a null resolver property must clear the floor's delegate");
456 assertNull(secure.getXMLResolver(), "getXMLResolver must report no caller resolver");
457 }
458
459 @Test
460 void setPropertyRoutesEveryResolverHookUniformly() {
461 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
462 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
463 final XMLResolver caller = (publicID, systemID, baseURI, namespace) -> "resolved";
464 for (final String hook : new String[] { XMLInputFactory.RESOLVER, SecureXMLInputFactory.WSTX_DTD_RESOLVER, SecureXMLInputFactory.WSTX_ENTITY_RESOLVER,
465 SecureXMLInputFactory.WSTX_UNDECLARED_ENTITY_RESOLVER }) {
466 fake.setProperty(hook, null);
467 secure.setProperty(hook, caller);
468 assertSame(caller, secure.getProperty(hook), "the caller's resolver must be reported unwrapped on " + hook);
469 }
470 }
471
472 @Test
473 void setPropertyWrapsCallerWhenHookIsNotAFloor() {
474 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
475 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
476 final XMLResolver caller = (publicID, systemID, baseURI, namespace) -> "resolved";
477 for (final Object foreign : new Object[] { null, (XMLResolver) (publicID, systemID, baseURI, namespace) -> "foreign" }) {
478 fake.setProperty(XMLInputFactory.RESOLVER, foreign);
479 secure.setProperty(XMLInputFactory.RESOLVER, caller);
480 assertInstanceOf(FallbackIgnoreXMLResolver.class, fake.resolverHook,
481 "a caller resolver must land behind a floor");
482 assertSame(caller, ((FallbackIgnoreXMLResolver) fake.resolverHook).getDelegate(), "the floor must delegate to the caller's resolver");
483 assertSame(caller, secure.getXMLResolver(), "getXMLResolver must report the caller's resolver unwrapped");
484 }
485 }
486
487 @Test
488 void setPropertyWrongTypeForResolverHookReachesDelegate() {
489 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
490 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
491 final Object wrongType = "not a resolver";
492 secure.setProperty(XMLInputFactory.RESOLVER, wrongType);
493 assertTrue(fake.calls.contains(RecordingXMLInputFactory.call("setProperty", XMLInputFactory.RESOLVER, wrongType)),
494 "a wrong-typed value must reach the delegate so it can reject it");
495 }
496
497 @Test
498 void setPropertyWrongTypeForResolverHookSurfacesDelegateException() {
499 final XMLInputFactory factory = SecureXMLInputFactory.newInstance();
500 assertThrows(ClassCastException.class, () -> factory.setProperty(XMLInputFactory.RESOLVER, "not a resolver"),
501 "the delegate must surface its own rejection of a wrong-typed resolver");
502 }
503
504 @Test
505 void settingAResolverInstallsAFreshFloorInsteadOfMutatingTheInstalledOne() {
506
507
508 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
509 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
510 final Object captured = fake.resolverHook;
511 secure.setXMLResolver((publicID, systemID, baseURI, namespace) -> null);
512 assertNotSame(captured, fake.resolverHook, "setting a resolver must install a fresh floor, not re-delegate the one already on the hook");
513 assertNull(((FallbackIgnoreXMLResolver) captured).getDelegate(), "the floor an existing reader captured must keep resolving to empty");
514 }
515
516 @Test
517 void setXMLResolverNullClearsCallerDelegate() {
518 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
519 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
520 final FallbackIgnoreXMLResolver floor = (FallbackIgnoreXMLResolver) fake.resolverHook;
521 final XMLResolver caller = (publicID, systemID, baseURI, namespace) -> null;
522 secure.setXMLResolver(caller);
523 secure.setXMLResolver(null);
524 assertNull(floor.getDelegate(), "a null caller resolver must clear the floor's delegate");
525 assertNull(secure.getXMLResolver(), "getXMLResolver must report no caller resolver");
526 }
527
528 @Test
529 void setXMLResolverRoutesCallerBehindInstalledFloor() {
530 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
531 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
532 final XMLResolver caller = (publicID, systemID, baseURI, namespace) -> null;
533 secure.setXMLResolver(caller);
534
535
536 assertInstanceOf(FallbackIgnoreXMLResolver.class, fake.resolverHook,
537 "a caller resolver must land behind a floor, not replace it on the delegate's hook");
538 assertSame(caller, ((FallbackIgnoreXMLResolver) fake.resolverHook).getDelegate(), "the caller's resolver must be the floor's delegate");
539 assertSame(caller, secure.getXMLResolver(), "getXMLResolver must report the caller's resolver unwrapped");
540 assertSame(caller, secure.getProperty(XMLInputFactory.RESOLVER), "getProperty must report the caller's resolver unwrapped");
541 }
542
543 @Test
544 void unsupportedResolverHookSurfacesDelegateError() {
545 final XMLInputFactory factory = SecureXMLInputFactory.newInstance();
546 Assumptions.assumeFalse(factory.isPropertySupported(SecureXMLInputFactory.WSTX_DTD_RESOLVER), "requires an implementation without the Woodstox hooks");
547 final XMLResolver caller = (publicID, systemID, baseURI, namespace) -> null;
548 assertThrows(IllegalArgumentException.class, () -> factory.setProperty(SecureXMLInputFactory.WSTX_DTD_RESOLVER, caller),
549 "the delegate must surface its own rejection of an unknown resolver hook");
550 assertThrows(IllegalArgumentException.class, () -> factory.getProperty(SecureXMLInputFactory.WSTX_DTD_RESOLVER),
551 "the delegate must surface its own rejection of an unknown resolver hook");
552 }
553
554 @Test
555 void woodstoxDtdHookRoutesBehindInstalledFloor() {
556 final XMLInputFactory factory = SecureXMLInputFactory.newInstance();
557 Assumptions.assumeTrue(factory.isPropertySupported(SecureXMLInputFactory.WSTX_DTD_RESOLVER), "requires the Woodstox DTD resolver hook");
558 final XMLResolver first = (publicID, systemID, baseURI, namespace) -> null;
559 factory.setProperty(SecureXMLInputFactory.WSTX_DTD_RESOLVER, first);
560 assertSame(first, factory.getProperty(SecureXMLInputFactory.WSTX_DTD_RESOLVER), "the Woodstox hook must report the caller's resolver unwrapped");
561 final XMLResolver second = (publicID, systemID, baseURI, namespace) -> "resolved";
562 factory.setProperty(SecureXMLInputFactory.WSTX_DTD_RESOLVER, second);
563 assertSame(second, factory.getProperty(SecureXMLInputFactory.WSTX_DTD_RESOLVER), "a second caller resolver must replace the first behind the floor");
564 }
565
566 @Test
567 void woodstoxResolverHooksStayIndependent() {
568
569
570 final XMLInputFactory secure = SecureXMLInputFactory.newInstance();
571 final XMLResolver dtd = (publicID, systemID, baseURI, namespace) -> null;
572 try {
573 secure.setProperty(SecureXMLInputFactory.WSTX_DTD_RESOLVER, dtd);
574 } catch (final IllegalArgumentException notWoodstox) {
575 Assumptions.abort("the implementation does not support " + SecureXMLInputFactory.WSTX_DTD_RESOLVER);
576 return;
577 }
578 assertSame(dtd, secure.getProperty(SecureXMLInputFactory.WSTX_DTD_RESOLVER), "the hook the caller named must report their resolver");
579 assertNull(secure.getProperty(SecureXMLInputFactory.WSTX_ENTITY_RESOLVER), "a resolver set on the DTD hook must not answer the entity hook");
580 }
581
582 @Test
583 void woodstoxUndeclaredEntityHookWrapsCallerResolver() {
584 final XMLInputFactory factory = SecureXMLInputFactory.newInstance();
585 Assumptions.assumeTrue(factory.isPropertySupported(SecureXMLInputFactory.WSTX_UNDECLARED_ENTITY_RESOLVER),
586 "requires the Woodstox undeclared-entity resolver hook");
587 final XMLResolver caller = (publicID, systemID, baseURI, namespace) -> null;
588 factory.setProperty(SecureXMLInputFactory.WSTX_UNDECLARED_ENTITY_RESOLVER, caller);
589 assertSame(caller, factory.getProperty(SecureXMLInputFactory.WSTX_UNDECLARED_ENTITY_RESOLVER),
590 "the Woodstox hook must report the caller's resolver unwrapped");
591 }
592
593 @Test
594 void wrapperDelegatesAllocatorAndReporter() {
595 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
596 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
597 final XMLEventAllocator allocator = RecordingXMLInputFactory.ALLOCATOR_SENTINEL;
598 secure.setEventAllocator(allocator);
599 assertSame(allocator, secure.getEventAllocator(), "the allocator must round-trip through the delegate");
600 assertTrue(fake.calls.contains(RecordingXMLInputFactory.call("setEventAllocator", allocator)), "the exact allocator must be forwarded");
601 final XMLReporter reporter = RecordingXMLInputFactory.REPORTER_SENTINEL;
602 secure.setXMLReporter(reporter);
603 assertSame(reporter, secure.getXMLReporter(), "the reporter must round-trip through the delegate");
604 assertTrue(fake.calls.contains(RecordingXMLInputFactory.call("setXMLReporter", reporter)), "the exact reporter must be forwarded");
605 }
606
607 @Test
608 void wrapperDelegatesIsPropertySupported() {
609 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
610 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
611 assertTrue(secure.isPropertySupported(XMLInputFactory.SUPPORT_DTD), "the delegate's answer must be reported");
612 fake.supported = false;
613 assertFalse(secure.isPropertySupported(XMLInputFactory.SUPPORT_DTD), "the delegate's answer must be reported");
614 assertTrue(fake.calls.contains(RecordingXMLInputFactory.call("isPropertySupported", XMLInputFactory.SUPPORT_DTD)),
615 "the exact property name must be forwarded");
616 }
617
618 @Test
619 void wrapperDelegatesNonResolverProperties() {
620 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
621 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
622 secure.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.TRUE);
623 assertTrue(fake.calls.contains(RecordingXMLInputFactory.call("setProperty", XMLInputFactory.SUPPORT_DTD, Boolean.TRUE)),
624 "a non-resolver property must reach the delegate unmodified");
625 fake.resolverHook = Boolean.TRUE;
626 assertEquals(Boolean.TRUE, secure.getProperty(XMLInputFactory.SUPPORT_DTD), "a non-resolver property must be reported unmodified");
627 }
628
629 @Test
630 void wrapperDelegatesReaderCreationToDelegate() throws Exception {
631 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
632 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
633 final XMLStreamReader streamSentinel = RecordingXMLInputFactory.STREAM_SENTINEL;
634 final XMLEventReader eventSentinel = RecordingXMLInputFactory.EVENT_SENTINEL;
635 final EventFilter eventFilter = RecordingXMLInputFactory.EVENT_FILTER_SENTINEL;
636 final StreamFilter streamFilter = RecordingXMLInputFactory.STREAM_FILTER_SENTINEL;
637 final InputStream stream = new ByteArrayInputStream(BENIGN_XML.getBytes(StandardCharsets.UTF_8));
638 final StringReader reader = new StringReader(BENIGN_XML);
639 final Source source = new StreamSource(new StringReader(BENIGN_XML));
640 assertSame(streamSentinel, secure.createXMLStreamReader(stream));
641 assertSame(streamSentinel, secure.createXMLStreamReader(stream, "UTF-8"));
642 assertSame(streamSentinel, secure.createXMLStreamReader(reader));
643 assertSame(streamSentinel, secure.createXMLStreamReader(source));
644 assertSame(streamSentinel, secure.createXMLStreamReader(SYSTEM_ID, stream));
645 assertSame(streamSentinel, secure.createXMLStreamReader(SYSTEM_ID, reader));
646 assertSame(eventSentinel, secure.createXMLEventReader(stream));
647 assertSame(eventSentinel, secure.createXMLEventReader(stream, "UTF-8"));
648 assertSame(eventSentinel, secure.createXMLEventReader(reader));
649 assertSame(eventSentinel, secure.createXMLEventReader(source));
650 assertSame(eventSentinel, secure.createXMLEventReader(SYSTEM_ID, stream));
651 assertSame(eventSentinel, secure.createXMLEventReader(SYSTEM_ID, reader));
652 assertSame(eventSentinel, secure.createXMLEventReader(streamSentinel));
653 assertSame(eventSentinel, secure.createFilteredReader(eventSentinel, eventFilter));
654 assertSame(streamSentinel, secure.createFilteredReader(streamSentinel, streamFilter));
655 assertTrue(fake.calls.contains(RecordingXMLInputFactory.call("createXMLStreamReader", stream)), "the exact stream must be forwarded");
656 assertTrue(fake.calls.contains(RecordingXMLInputFactory.call("createXMLStreamReader", reader)), "the exact reader must be forwarded");
657 assertTrue(fake.calls.contains(RecordingXMLInputFactory.call("createXMLStreamReader", source)), "the exact source must be forwarded");
658 assertTrue(fake.calls.contains(RecordingXMLInputFactory.call("createXMLStreamReader", SYSTEM_ID, stream)), "the exact system id must be forwarded");
659 assertTrue(fake.calls.contains(RecordingXMLInputFactory.call("createXMLEventReader", stream, "UTF-8")), "the exact encoding must be forwarded");
660 assertTrue(fake.calls.contains(RecordingXMLInputFactory.call("createFilteredReader", eventSentinel, eventFilter)),
661 "the exact event filter must be forwarded");
662 assertTrue(fake.calls.contains(RecordingXMLInputFactory.call("createFilteredReader", streamSentinel, streamFilter)),
663 "the exact stream filter must be forwarded");
664 }
665
666 @Test
667 void wrapperInstallsFloorOnDelegateHook() {
668 final RecordingXMLInputFactory fake = new RecordingXMLInputFactory();
669 final XMLInputFactory secure = SecureXMLInputFactory.secure(fake);
670 assertNotNull(secure);
671 assertTrue(fake.calls.contains(RecordingXMLInputFactory.call("setXMLResolver", fake.resolverHook)),
672 "the floor must be installed through the delegate's setXMLResolver");
673 assertInstanceOf(FallbackIgnoreXMLResolver.class, fake.resolverHook,
674 "the constructor must install the ignore-all floor on the delegate's resolver hook");
675 assertNull(((FallbackIgnoreXMLResolver) fake.resolverHook).getDelegate(), "the installed floor must have no caller delegate");
676 }
677 }