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 javax.xml.XMLConstants;
21 import javax.xml.validation.Schema;
22 import javax.xml.validation.SchemaFactory;
23 import javax.xml.validation.Validator;
24
25 import org.junit.jupiter.api.Tag;
26 import org.junit.jupiter.api.Test;
27 import org.xml.sax.SAXException;
28
29 /**
30 * Tests that an untrusted schema's content-model expansion is bounded, the one processing limit no reader can supply.
31 *
32 * <p>
33 * {@link BillionLaughsTest} covers entity expansion, which the secure reader injected into every {@code Source} bounds before a schema document reaches the
34 * loader. {@code maxOccurs} is a different mechanism: the loader expands a repeated particle into content-model nodes while building the DFA, which happens
35 * after parsing and never touches the reader. The bound for it is the schema implementation's own limit ({@code maxOccurLimit}, 3,000 nodes on Xerces), which
36 * external Xerces installs only when {@code FEATURE_SECURE_PROCESSING} is set on the {@link SchemaFactory}.
37 * </p>
38 *
39 * <p>
40 * The expansion is lazy on Xerces: {@code newSchema} returns in milliseconds whatever {@code maxOccurs} says, and the nodes are built on first validation.
41 * The payload therefore has to be validated, not just compiled, and the assertion accepts a rejection at either step. The repeated particle holds two elements
42 * so it cannot be collapsed into Xerces' compact repeating-leaf form, and {@link #MAX_OCCURS} clears both limits by little enough that an unbounded run still
43 * finishes, in seconds, rather than exhausting the heap.
44 * </p>
45 */
46 @Tag("schema")
47 class SchemaContentModelLimitTest {
48
49 /**
50 * Above both recognized implementations' limits (3,000 nodes on Xerces, 5,000 on the stock JDK); an unbounded run still finishes in seconds.
51 */
52 private static final int MAX_OCCURS = 5_001;
53
54 /**
55 * Compiles the payload through {@code factory} and validates a matching instance, the step that forces the expansion.
56 */
57 private static void compileAndValidate(final SchemaFactory factory) throws Exception {
58 factory.setErrorHandler(AttackTestSupport.STRICT_REPORTER);
59 final Schema schema = factory.newSchema(AttackTestSupport.streamSource(maxOccursPayload()));
60 final Validator validator = schema.newValidator();
61 validator.setErrorHandler(AttackTestSupport.STRICT_REPORTER);
62 validator.validate(AttackTestSupport.streamSource("<root><a>x</a><b>y</b></root>"));
63 }
64
65 private static String maxOccursPayload() {
66 return "<?xml version=\"1.0\"?>\n"
67 + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n"
68 + " <xs:element name=\"root\" type=\"bomb\"/>\n"
69 + " <xs:complexType name=\"bomb\">\n"
70 + " <xs:sequence>\n"
71 + " <xs:sequence minOccurs=\"0\" maxOccurs=\"" + MAX_OCCURS + "\">\n"
72 + " <xs:element name=\"a\" type=\"xs:string\"/>\n"
73 + " <xs:element name=\"b\" type=\"xs:string\"/>\n"
74 + " </xs:sequence>\n"
75 + " </xs:sequence>\n"
76 + " </xs:complexType>\n"
77 + "</xs:schema>\n";
78 }
79
80 @Test
81 void secureSchemaBoundsContentModelExpansion() {
82 AttackTestSupport.assertParseFails(() -> compileAndValidate(SecureSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)),
83 "Schema content-model expansion", SAXException.class);
84 }
85
86 @Test
87 void unconfiguredSchemaValidatesWhereTheLimitIsOptional() {
88 // Control: the payload is a valid schema and instance, so a rejection above is the limit firing and not a malformed fixture. It is skipped on an
89 // implementation that bounds the expansion unconditionally (the stock JDK), where there is no unbounded run to compare against.
90 final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
91 AttackTestSupport.assumeDoesNotThrow(() -> compileAndValidate(factory));
92 }
93
94 @Test
95 void unconfiguredSchemaWithSecureProcessingBoundsContentModelExpansion() {
96 // Control: the payload does trip the limit once secure processing is on, so a pass above is the limit firing rather than the payload being harmless.
97 AttackTestSupport.assertParseFails(() -> {
98 final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
99 factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
100 compileAndValidate(factory);
101 }, "Schema content-model expansion", SAXException.class);
102 }
103 }