View Javadoc
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 }