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.assertFalse;
21 import static org.junit.jupiter.api.Assertions.assertNotEquals;
22 import static org.junit.jupiter.api.Assertions.assertTrue;
23
24 import java.io.StringWriter;
25
26 import javax.xml.transform.TransformerException;
27 import javax.xml.transform.TransformerFactory;
28 import javax.xml.transform.stream.StreamResult;
29
30 import org.junit.jupiter.api.Assumptions;
31 import org.junit.jupiter.api.Tag;
32 import org.junit.jupiter.api.Test;
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57 @Tag("trax")
58 class SaxonTransformerExternalCallsTest {
59
60 private static final String SAXON_TRANSFORMER_FACTORY_CLASS = "net.sf.saxon.TransformerFactoryImpl";
61
62
63
64
65 private static void assertSecureDoesNotLeak(final String expression) {
66 try {
67 final String result = transform(SecureTransformerFactory.secure(saxonFactory()), expression);
68 assertFalse(result.contains(AttackTestSupport.LEAKED_MARKER), "secure Saxon transform leaked through " + expression + ":\n" + result);
69 } catch (final TransformerException blocked) {
70
71 }
72 }
73
74
75
76
77 private static void assertUnconfiguredLeaks(final String expression) throws TransformerException {
78 final String result = transform(saxonFactory(), expression);
79 assertTrue(result.contains(AttackTestSupport.LEAKED_MARKER), "unconfigured Saxon was expected to resolve " + expression + ", got: " + result);
80 }
81
82 private static void assumeSaxonPresent() {
83 boolean present;
84 try {
85 Class.forName(SAXON_TRANSFORMER_FACTORY_CLASS);
86 present = true;
87 } catch (final ClassNotFoundException e) {
88 present = false;
89 }
90 Assumptions.assumeTrue(present, "Saxon is not on the classpath");
91 }
92
93
94
95
96 private static String availabilityUnderSecure(final String uri) {
97 try {
98 return transform(SecureTransformerFactory.secure(saxonFactory()), "unparsed-text-available('" + uri + "')").contains("true") ? "true" : "false";
99 } catch (final TransformerException blocked) {
100 return "blocked";
101 }
102 }
103
104
105
106
107 private static String missingUrl() {
108 return url("referenced.txt").replaceFirst("referenced\\.txt$", "does-not-exist.txt");
109 }
110
111 private static TransformerFactory saxonFactory() {
112 try {
113 return (TransformerFactory) Class.forName(SAXON_TRANSFORMER_FACTORY_CLASS).getDeclaredConstructor().newInstance();
114 } catch (final ReflectiveOperationException e) {
115 throw new AssertionError("Cannot instantiate " + SAXON_TRANSFORMER_FACTORY_CLASS, e);
116 }
117 }
118
119
120
121
122 private static String stylesheet(final String expression) {
123 return "<?xml version=\"1.0\"?>\n"
124 + "<xsl:stylesheet version=\"3.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n"
125 + " <xsl:template match=\"/\">\n"
126 + " <leaked><xsl:value-of select=\"" + expression + "\"/></leaked>\n"
127 + " </xsl:template>\n"
128 + "</xsl:stylesheet>\n";
129 }
130
131 private static String transform(final TransformerFactory factory, final String expression) throws TransformerException {
132 final StringWriter sink = new StringWriter();
133 factory.newTemplates(AttackTestSupport.streamSource(stylesheet(expression))).newTransformer()
134 .transform(AttackTestSupport.streamSource("<root/>"), new StreamResult(sink));
135 return sink.toString();
136 }
137
138
139
140
141 private static String url(final String name) {
142 return AttackTestSupport.resourceUrl(name).toString();
143 }
144
145 @Test
146 void secureTransformerBlocksJsonDoc() {
147 assumeSaxonPresent();
148 assertSecureDoesNotLeak("json-doc('" + url("referenced.json") + "')?leaked");
149 }
150
151 @Test
152 void secureTransformerBlocksUnparsedText() {
153 assumeSaxonPresent();
154 assertSecureDoesNotLeak("unparsed-text('" + url("referenced.txt") + "')");
155 }
156
157 @Test
158 void secureTransformerBlocksUnparsedTextLines() {
159 assumeSaxonPresent();
160 assertSecureDoesNotLeak("string-join(unparsed-text-lines('" + url("referenced.txt") + "'), ' ')");
161 }
162
163 @Test
164 void secureTransformerHidesUnparsedTextAvailability() {
165 assumeSaxonPresent();
166
167 final TransformerFactory unconfigured = saxonFactory();
168 try {
169 assertTrue(transform(unconfigured, "unparsed-text-available('" + url("referenced.txt") + "')").contains("true"),
170 "unconfigured Saxon should report the fixture as available");
171 assertTrue(transform(unconfigured, "unparsed-text-available('" + missingUrl() + "')").contains("false"),
172 "unconfigured Saxon should report the missing sibling as unavailable");
173 } catch (final TransformerException e) {
174 throw new AssertionError("unconfigured Saxon unparsed-text-available control failed", e);
175 }
176
177 final String secureExisting = availabilityUnderSecure(url("referenced.txt"));
178 final String secureMissing = availabilityUnderSecure(missingUrl());
179 assertNotEquals("true:false", secureExisting + ":" + secureMissing,
180 "secure Saxon unparsed-text-available still distinguishes an existing file from a missing one");
181 }
182
183 @Test
184 void unconfiguredTransformerLeaksJsonDoc() throws TransformerException {
185 assumeSaxonPresent();
186 assertUnconfiguredLeaks("json-doc('" + url("referenced.json") + "')?leaked");
187 }
188
189 @Test
190 void unconfiguredTransformerLeaksUnparsedText() throws TransformerException {
191 assumeSaxonPresent();
192 assertUnconfiguredLeaks("unparsed-text('" + url("referenced.txt") + "')");
193 }
194
195 @Test
196 void unconfiguredTransformerLeaksUnparsedTextLines() throws TransformerException {
197 assumeSaxonPresent();
198 assertUnconfiguredLeaks("string-join(unparsed-text-lines('" + url("referenced.txt") + "'), ' ')");
199 }
200 }