blob: 7865cf0c71b9dcad0cf61f00eaff76349b33259b [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.xml.secure;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
/**
* Tests whether parsers reject a Billion Laughs payload (nested entity expansion in the internal DTD subset).
*
* <p>Each {@code secure*} test asserts the library blocks the payload;
* its {@code unconfigured*} positive control asserts the same payload parses once the limit is disabled,
* so a block reflects the secure rather than a broken wrapper.
* The library pins no custom entity-expansion limit; each parser keeps its own secure-processing default, which varies by implementation:
* {@code 2,500} (stock JDK),
* {@code 64,000} (external Xerces under {@code FEATURE_SECURE_PROCESSING}),
* {@code 100,000} (external Xerces' and Woodstox's own security managers).
* A payload therefore has to exceed the largest of these.</p>
*
* <p>Every payload shares one six-level x10 {@link #DTD} (declaring entities is free until they are referenced) and varies only the body it expands:</p>
*
* <ul>
* <li>{@link #CONTENT_120K} ({@code 120,000}) on the JVM: above every JVM parser default.</li>
* <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
* not configurable. For that same reason the positive controls do not run on Android (see {@link #assumeEntityLimitConfigurable()}): a payload the secure test
* blocks cannot be parsed even without securing.</li>
* </ul>
*
* <p>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,
* because XSLTC caps a compiled literal at 65,535 bytes; see {@link #xsltPayload()}.
* A parser counts expansions across the whole document, so the split changes nothing on the secure side.</p>
*
* <p>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
* (being ASCII) onto XSLTC's byte-counted constant-pool ceiling as well.</p>
*/
class BillionLaughsTest {
/** 6 x 10,000 = 60,000 expansions; each half of the split XSLT body. */
private static final String CONTENT_60K = repeatRef("lol4", 6);
/** 100,000 + 2 x 10,000 = 120,000 expansions; above every JVM parser's secure default (2,500 / 64,000 / 100,000). */
private static final String CONTENT_120K = "&lol5;&lol4;&lol4;";
/** 9 x 1,000,000 = 9,000,000 expansions; above libexpat's 8 MiB billion-laughs activation threshold. */
private static final String CONTENT_9M = repeatRef("lol6", 9);
/**
* 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 costs
* nothing until it is referenced, so the DTD is identical on every platform and only the expanded body ({@link #content()}) varies.
*/
private static final String DTD =
" <!ENTITY lol \"A\">\n"
+ entityLine("lol1", "lol") // 10
+ entityLine("lol2", "lol1") // 100
+ entityLine("lol3", "lol2") // 1000
+ entityLine("lol4", "lol3") // 10000
+ entityLine("lol5", "lol4") // 100000
+ entityLine("lol6", "lol5"); // 1000000
/**
* Skips a positive control on Android.
*
* <p>The controls prove the secure test blocked a payload that would otherwise parse, so they must use the very payload the secure test blocks.
* On Android the entity-expansion limit is not configurable (libexpat's billion-laughs check cannot be lifted), so that payload
* cannot be parsed even without securing, leaving nothing to prove.</p>
*/
private static void assumeEntityLimitConfigurable() {
Assumptions.assumeFalse(AttackTestSupport.IS_ANDROID, "Skipped on Android: the entity-expansion limit is not configurable");
}
/** 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. */
private static String content() {
return AttackTestSupport.IS_ANDROID ? CONTENT_9M : CONTENT_120K;
}
/** Renders {@code <!ENTITY name "&ref;&ref;...">}, one ladder rung: ten copies of {@code &ref;}. */
private static String entityLine(final String name, final String ref) {
return " <!ENTITY " + name + " \"" + repeatRef(ref, 10) + "\">\n";
}
/** Builds {@code times} copies of the entity reference {@code &name;}. */
private static String repeatRef(final String name, final int times) {
final String ref = "&" + name + ";";
final StringBuilder sb = new StringBuilder(ref.length() * times);
for (int i = 0; i < times; i++) {
sb.append(ref);
}
return sb.toString();
}
private static String withDoctype(final String rootQName, final String body) {
return "<?xml version=\"1.0\"?>\n"
+ "<!DOCTYPE " + rootQName + " [\n"
+ DTD
+ "]>\n"
+ body + "\n";
}
/** Payload for DOM/SAX/XmlReader/StAX/Transformer/Validator. */
private static String xmlPayload() {
return withDoctype("root", AttackTestSupport.xmlBody(content()));
}
/** XSD payload. */
private static String xsdPayload() {
return withDoctype("xs:schema", AttackTestSupport.xsdBody(content()));
}
/**
* XSLT payload; the JVM body splits its expansions across two literal result elements.
*
* <p>XSLTC compiles each literal text node into a class-file string constant, and a {@code CONSTANT_Utf8} entry holds at most 65,535 bytes.
* We split the payload into two constants to still trigger the Xerces 100k expansion limit, but without any text node above 64 KiB.</p>
*
* <p>Android keeps the single {@link #CONTENT_9M} body: libexpat aborts during the parse, so no translet is ever compiled.</p>
*/
private static String xsltPayload() {
final String body = AttackTestSupport.IS_ANDROID
? CONTENT_9M
: "<a>" + CONTENT_60K + "</a><b>" + CONTENT_60K + "</b>";
return withDoctype("xsl:stylesheet", AttackTestSupport.xsltBody(body));
}
@Test
@Tag("dom")
void secureDomBlocks() {
Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES,
"Skipped: platform DOM does not resolve user-defined entities");
AttackTestSupport.assertDomBlocks(xmlPayload());
}
@Test
@Tag("sax")
void secureSaxBlocks() {
AttackTestSupport.assertSaxBlocks(xmlPayload());
}
@Test
@Tag("schema")
void secureSchemaBlocks() {
AttackTestSupport.assertSchemaBlocks(AttackTestSupport.streamSource(xsdPayload()));
}
@Test
@Tag("stax")
void secureStaxBlocks() {
AttackTestSupport.assertStaxBlocks(xmlPayload());
}
@Test
@Tag("trax")
void secureTemplatesBlocks() {
AttackTestSupport.assertTemplatesBlocks(AttackTestSupport.streamSource(xsltPayload()));
}
@Test
@Tag("trax")
void secureTransformerBlocks() {
AttackTestSupport.assertTransformerBlocks(xmlPayload());
}
@Test
@Tag("schema")
void secureValidatorBlocks() {
AttackTestSupport.assertValidatorBlocks(xmlPayload());
}
@Test
@Tag("sax")
void secureXmlReaderBlocks() {
AttackTestSupport.assertXmlReaderBlocks(xmlPayload());
}
@Test
@Tag("dom")
void unconfiguredDomParses() {
Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES,
"Skipped: platform DOM does not resolve user-defined entities");
AttackTestSupport.assertPermissiveDomParses(xmlPayload());
}
@Test
@Tag("sax")
void unconfiguredSaxParses() {
assumeEntityLimitConfigurable();
AttackTestSupport.assertPermissiveSaxParses(xmlPayload());
}
@Test
@Tag("schema")
void unconfiguredSchemaCompiles() {
assumeEntityLimitConfigurable();
AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.streamSource(xsdPayload()));
}
@Test
@Tag("stax")
void unconfiguredStaxParses() {
assumeEntityLimitConfigurable();
AttackTestSupport.assertPermissiveStaxParses(xmlPayload());
}
@Test
@Tag("trax")
void unconfiguredTemplatesCompiles() {
assumeEntityLimitConfigurable();
AttackTestSupport.assertPermissiveTemplatesCompiles(xsltPayload());
}
@Test
@Tag("trax")
void unconfiguredTransformerTransforms() {
assumeEntityLimitConfigurable();
AttackTestSupport.assertPermissiveTransformerTransforms(xmlPayload());
}
@Test
@Tag("schema")
void unconfiguredValidatorValidates() {
assumeEntityLimitConfigurable();
AttackTestSupport.assertPermissiveValidatorValidates(xmlPayload());
}
}