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