1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.xml.secure;
19
20 import org.junit.jupiter.api.Assumptions;
21 import org.junit.jupiter.api.Tag;
22 import org.junit.jupiter.api.Test;
23
24 /**
25 * Tests whether parsers reject a Billion Laughs payload (nested entity expansion in the internal DTD subset).
26 *
27 * <p>
28 * Each {@code secure*} test asserts the library blocks the payload;
29 * its {@code unconfigured*} positive control asserts the same payload parses once the limit is disabled,
30 * so a block reflects the securing rather than a broken wrapper.
31 * The library pins no custom entity-expansion limit; each parser keeps its own secure-processing default, which varies by implementation:
32 * {@code 2,500} (stock JDK),
33 * {@code 64,000} (external Xerces under {@code FEATURE_SECURE_PROCESSING}),
34 * {@code 100,000} (external Xerces' and Woodstox's own security managers).
35 * A payload therefore has to exceed the largest of these.
36 * </p>
37 *
38 * <p>
39 * Every payload shares one six-level x10 {@link #DTD} (declaring entities is free until they are referenced) and varies only the body it expands:
40 * </p>
41 *
42 * <ul>
43 * <li>{@link #CONTENT_120K} ({@code 120,000}) on the JVM: above every JVM parser default.</li>
44 * <li>{@link #CONTENT_9M} ({@code 9,000,000}) on Android: above libexpat's 8 MiB billion-laughs activation threshold, the only defense there since the limit is
45 * not configurable. For that same reason the positive controls do not run on Android (see {@link #assumeEntityLimitConfigurable()}): a payload the secure test
46 * blocks cannot be parsed even without securing.</li>
47 * </ul>
48 *
49 * <p>
50 * The XSLT payload spreads those same {@code 120,000} expansions over two literal result elements with content {@link #CONTENT_60K} rather than one text node,
51 * because XSLTC caps a compiled literal at 65,535 bytes; see {@link #xsltPayload()}.
52 * A parser counts expansions across the whole document, so the split changes nothing on the secure side.
53 * </p>
54 *
55 * <p>
56 * Why a single character {@code "A"}: it makes the expanded size equal the expansion count, so a payload's size maps directly onto each parser's limit, and
57 * (being ASCII) onto XSLTC's byte-counted constant-pool ceiling as well.
58 * </p>
59 */
60 class BillionLaughsTest {
61
62 /**
63 * 6 x 10,000 = 60,000 expansions; each half of the split XSLT body.
64 */
65 private static final String CONTENT_60K = repeatRef("lol4", 6);
66 /**
67 * 100,000 + 2 x 10,000 = 120,000 expansions; above every JVM parser's secure default (2,500 / 64,000 / 100,000).
68 */
69 private static final String CONTENT_120K = "&lol5;&lol4;&lol4;";
70 /**
71 * 9 x 1,000,000 = 9,000,000 expansions; above libexpat's 8 MiB billion-laughs activation threshold.
72 */
73 private static final String CONTENT_9M = repeatRef("lol6", 9);
74 /**
75 * Shared DTD for every payload: a six-level x10 ladder, {@code &lol1;} through {@code &lol6;} ({@code &lol6;} expands to 1,000,000). Declaring an entity
76 * costs nothing until it is referenced, so the DTD is identical on every platform and only the expanded body ({@link #content()}) varies.
77 */
78 private static final String DTD =
79 " <!ENTITY lol \"A\">\n"
80 + entityLine("lol1", "lol") // 10
81 + entityLine("lol2", "lol1") // 100
82 + entityLine("lol3", "lol2") // 1000
83 + entityLine("lol4", "lol3") // 10000
84 + entityLine("lol5", "lol4") // 100000
85 + entityLine("lol6", "lol5"); // 1000000
86
87 /**
88 * Skips a positive control on Android.
89 *
90 * <p>
91 * The controls prove the secure test blocked a payload that would otherwise parse, so they must use the very payload the secure test blocks.
92 * On Android the entity-expansion limit is not configurable (libexpat's billion-laughs check cannot be lifted), so that payload
93 * cannot be parsed even without securing, leaving nothing to prove.
94 * </p>
95 */
96 private static void assumeEntityLimitConfigurable() {
97 Assumptions.assumeFalse(AttackTestSupport.IS_ANDROID, "Skipped on Android: the entity-expansion limit is not configurable");
98 }
99
100 /**
101 * The body to expand: 9,000,000 on Android, where libexpat is the only defense and the limit is not configurable, 120,000 on the JVM.
102 */
103 private static String content() {
104 return AttackTestSupport.IS_ANDROID ? CONTENT_9M : CONTENT_120K;
105 }
106
107 /**
108 * Renders {@code <!ENTITY name "&ref;&ref;...">}, one ladder rung: ten copies of {@code &ref;}.
109 */
110 private static String entityLine(final String name, final String ref) {
111 return " <!ENTITY " + name + " \"" + repeatRef(ref, 10) + "\">\n";
112 }
113
114 /**
115 * Builds {@code times} copies of the entity reference {@code &name;}.
116 */
117 private static String repeatRef(final String name, final int times) {
118 final String ref = "&" + name + ";";
119 final StringBuilder sb = new StringBuilder(ref.length() * times);
120 for (int i = 0; i < times; i++) {
121 sb.append(ref);
122 }
123 return sb.toString();
124 }
125
126 private static String withDoctype(final String rootQName, final String body) {
127 return "<?xml version=\"1.0\"?>\n"
128 + "<!DOCTYPE " + rootQName + " [\n"
129 + DTD
130 + "]>\n"
131 + body + "\n";
132 }
133
134 /**
135 * Payload for DOM/SAX/XmlReader/StAX/Transformer/Validator.
136 */
137 private static String xmlPayload() {
138 return withDoctype("root", AttackTestSupport.xmlBody(content()));
139 }
140
141 /**
142 * XSD payload.
143 */
144 private static String xsdPayload() {
145 return withDoctype("xs:schema", AttackTestSupport.xsdBody(content()));
146 }
147
148 /**
149 * XSLT payload; the JVM body splits its expansions across two literal result elements.
150 *
151 * <p>
152 * XSLTC compiles each literal text node into a class-file string constant, and a {@code CONSTANT_Utf8} entry holds at most 65,535 bytes.
153 * We split the payload into two constants to still trigger the Xerces 100k expansion limit, but without any text node above 64 KiB.
154 * </p>
155 *
156 * <p>
157 * Android keeps the single {@link #CONTENT_9M} body: libexpat aborts during the parse, so no translet is ever compiled.
158 * </p>
159 */
160 private static String xsltPayload() {
161 final String body = AttackTestSupport.IS_ANDROID
162 ? CONTENT_9M
163 : "<a>" + CONTENT_60K + "</a><b>" + CONTENT_60K + "</b>";
164 return withDoctype("xsl:stylesheet", AttackTestSupport.xsltBody(body));
165 }
166
167 @Test
168 @Tag("dom")
169 void secureDomBlocks() {
170 Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES,
171 "Skipped: platform DOM does not resolve user-defined entities");
172 AttackTestSupport.assertDomBlocks(xmlPayload());
173 }
174
175 @Test
176 @Tag("sax")
177 void secureSaxBlocks() {
178 AttackTestSupport.assertSaxBlocks(xmlPayload());
179 }
180
181 @Test
182 @Tag("schema")
183 void secureSchemaBlocks() {
184 AttackTestSupport.assertSchemaBlocks(AttackTestSupport.streamSource(xsdPayload()));
185 }
186
187 @Test
188 @Tag("stax")
189 void secureStaxBlocks() {
190 AttackTestSupport.assertStaxBlocks(xmlPayload());
191 }
192
193 @Test
194 @Tag("trax")
195 void secureTemplatesBlocks() {
196 AttackTestSupport.assertTemplatesBlocks(AttackTestSupport.streamSource(xsltPayload()));
197 }
198
199 @Test
200 @Tag("trax")
201 void secureTransformerBlocks() {
202 AttackTestSupport.assertTransformerBlocks(xmlPayload());
203 }
204
205 @Test
206 @Tag("schema")
207 void secureValidatorBlocks() {
208 AttackTestSupport.assertValidatorBlocks(xmlPayload());
209 }
210
211 @Test
212 @Tag("sax")
213 void secureXmlReaderBlocks() {
214 AttackTestSupport.assertXmlReaderBlocks(xmlPayload());
215 }
216
217 @Test
218 @Tag("dom")
219 void unconfiguredDomParses() {
220 Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES,
221 "Skipped: platform DOM does not resolve user-defined entities");
222 AttackTestSupport.assertPermissiveDomParses(xmlPayload());
223 }
224
225 @Test
226 @Tag("sax")
227 void unconfiguredSaxParses() {
228 assumeEntityLimitConfigurable();
229 AttackTestSupport.assertPermissiveSaxParses(xmlPayload());
230 }
231
232 @Test
233 @Tag("schema")
234 void unconfiguredSchemaCompiles() {
235 assumeEntityLimitConfigurable();
236 AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.streamSource(xsdPayload()));
237 }
238
239 @Test
240 @Tag("stax")
241 void unconfiguredStaxParses() {
242 assumeEntityLimitConfigurable();
243 AttackTestSupport.assertPermissiveStaxParses(xmlPayload());
244 }
245
246 @Test
247 @Tag("trax")
248 void unconfiguredTemplatesCompiles() {
249 assumeEntityLimitConfigurable();
250 AttackTestSupport.assertPermissiveTemplatesCompiles(xsltPayload());
251 }
252
253 @Test
254 @Tag("trax")
255 void unconfiguredTransformerTransforms() {
256 assumeEntityLimitConfigurable();
257 AttackTestSupport.assertPermissiveTransformerTransforms(xmlPayload());
258 }
259
260 @Test
261 @Tag("schema")
262 void unconfiguredValidatorValidates() {
263 assumeEntityLimitConfigurable();
264 AttackTestSupport.assertPermissiveValidatorValidates(xmlPayload());
265 }
266 }