View Javadoc
1   /*
2    * This file is part of dependency-check-utils.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Copyright (c) 2016 Jeremy Long. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.utils;
19  
20  import java.nio.file.Path;
21  import java.util.List;
22  import java.util.Optional;
23  import javax.xml.XMLConstants;
24  import javax.xml.parsers.DocumentBuilder;
25  import javax.xml.parsers.DocumentBuilderFactory;
26  import javax.xml.parsers.ParserConfigurationException;
27  import javax.xml.parsers.SAXParser;
28  import javax.xml.parsers.SAXParserFactory;
29  
30  import com.google.common.annotations.VisibleForTesting;
31  import org.jspecify.annotations.NonNull;
32  import org.xml.sax.EntityResolver;
33  import org.xml.sax.InputSource;
34  import org.xml.sax.SAXException;
35  import org.xml.sax.SAXNotRecognizedException;
36  import org.xml.sax.SAXNotSupportedException;
37  import org.xml.sax.SAXParseException;
38  import org.xml.sax.XMLReader;
39  
40  /**
41   * Collection of XML related code.
42   *
43   * @author Jeremy Long
44   * @version $Id: $Id
45   */
46  public final class XmlUtils {
47  
48      /**
49       * JAXP Schema Language. Source:
50       * <a href="https://docs.oracle.com/javase/tutorial/jaxp/sax/validation.html">...</a>
51       */
52      public static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
53      /**
54       * W3C XML Schema. Source:
55       * <a href="https://docs.oracle.com/javase/tutorial/jaxp/sax/validation.html">...</a>
56       */
57      public static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";
58      /**
59       * JAXP Schema Source. Source:
60       * <a href="https://docs.oracle.com/javase/tutorial/jaxp/sax/validation.html">...</a>
61       */
62      public static final String JAXP_SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource";
63  
64      /**
65       * Private constructor for a utility class.
66       */
67      private XmlUtils() {
68      }
69  
70      /**
71       * Constructs a validating secure SAX XMLReader that can validate against schemas maintained locally.
72       *
73       * @param schemas One or more schemas with the schema(s) that the
74       * parser should be able to validate the XML against, one InputSource per
75       * schema
76       * @return a validating SAX-based XML reader; pre-configured to validate against the locally passed schemas
77       * @throws javax.xml.parsers.ParserConfigurationException is thrown if there
78       * is a parser configuration exception
79       * @throws org.xml.sax.SAXException is thrown if there is an issue setting SAX features
80       * on the parser; or creating the parser
81       */
82      public static XMLReader buildSecureValidatingXmlReader(AutoCloseableInputSource... schemas) throws ParserConfigurationException,
83              SAXException {
84          final SAXParserFactory factory = buildSecureSaxParserFactory();
85  
86          factory.setNamespaceAware(true);
87          factory.setValidating(true);
88  
89          final SAXParser saxParser = factory.newSAXParser();
90  
91          // Support validating from a set of schemas where we dont have schema locations set
92          saxParser.setProperty(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
93          saxParser.setProperty(JAXP_SCHEMA_SOURCE, schemas);
94  
95          // Support validating where documents have schema location hints which we will intercept and load locally
96          XMLReader xmlReader = saxParser.getXMLReader();
97          xmlReader.setEntityResolver(new ExternalInterceptingEntityResolver(schemas));
98  
99          return xmlReader;
100     }
101 
102     /**
103      * Constructs a non-validating secure SAX XMLReader.
104      *
105      * @return a non-validating SAX-based XML reader
106      * @throws javax.xml.parsers.ParserConfigurationException is thrown if there
107      * is a parser configuration exception
108      * @throws org.xml.sax.SAXException is thrown if there is an issue setting SAX features
109      * on the parser; or creating the parser
110      */
111     public static XMLReader buildSecureXmlReader() throws ParserConfigurationException,
112             SAXException {
113         return buildSecureSaxParser().getXMLReader();
114     }
115 
116     /**
117      * Converts an attribute value representing an xsd:boolean value to a
118      * boolean using the rules as stated in the XML specification.
119      *
120      * @param lexicalXSDBoolean The string-value of the boolean
121      * @return the boolean value represented by {@code lexicalXSDBoolean}
122      * @throws java.lang.IllegalArgumentException When {@code lexicalXSDBoolean}
123      * does fit the lexical space of the XSD boolean datatype
124      */
125     public static boolean parseBoolean(String lexicalXSDBoolean) {
126         final boolean result;
127         switch (lexicalXSDBoolean) {
128             case "true":
129             case "1":
130                 result = true;
131                 break;
132             case "false":
133             case "0":
134                 result = false;
135                 break;
136             default:
137                 throw new IllegalArgumentException("'" + lexicalXSDBoolean + "' is not a valid xs:boolean value");
138         }
139         return result;
140     }
141 
142     /**
143      * Constructs a secure non-validating SAX Parser.
144      *
145      * @return a SAX Parser
146      * @throws javax.xml.parsers.ParserConfigurationException is thrown if there
147      * is a parser configuration exception
148      * @throws org.xml.sax.SAXException is thrown if there is an issue setting SAX features
149      * on the parser; or creating the parser
150      */
151     public static SAXParser buildSecureSaxParser() throws ParserConfigurationException,
152             SAXException {
153         return buildSecureSaxParserFactory().newSAXParser();
154     }
155 
156     private static @NonNull SAXParserFactory buildSecureSaxParserFactory() throws ParserConfigurationException, SAXNotRecognizedException, SAXNotSupportedException {
157         final SAXParserFactory factory = SAXParserFactory.newInstance();
158 
159         // See https://xerces.apache.org/xerces2-j/features.html and
160         // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#jaxp-documentbuilderfactory-saxparserfactory-and-dom4j
161         factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
162 
163         // No doctypes, no DTDs (XSD only)
164         factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
165         factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
166         factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
167 
168         // No XML Entity Expansion
169         factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
170         factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
171         return factory;
172     }
173 
174     /**
175      * Constructs a new document builder with security features enabled.
176      *
177      * @return a new document builder
178      * @throws javax.xml.parsers.ParserConfigurationException thrown if there is
179      * a parser configuration exception
180      */
181     public static DocumentBuilder buildSecureDocumentBuilder() throws ParserConfigurationException {
182         final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
183         factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
184         factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
185         factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
186         return factory.newDocumentBuilder();
187     }
188 
189     /**
190      * Builds a prettier exception message.
191      *
192      * @param ex the SAXParseException
193      * @return an easier to read exception message
194      */
195     public static String getPrettyParseExceptionInfo(SAXParseException ex) {
196 
197         final StringBuilder sb = new StringBuilder();
198 
199         if (ex.getSystemId() != null) {
200             sb.append("systemId=").append(ex.getSystemId()).append(", ");
201         }
202         if (ex.getPublicId() != null) {
203             sb.append("publicId=").append(ex.getPublicId()).append(", ");
204         }
205         if (ex.getLineNumber() > 0) {
206             sb.append("Line=").append(ex.getLineNumber());
207         }
208         if (ex.getColumnNumber() > 0) {
209             sb.append(", Column=").append(ex.getColumnNumber());
210         }
211         sb.append(": ").append(ex.getMessage());
212 
213         return sb.toString();
214     }
215 
216     /**
217      * Load HTTPS and file schema resources locally from the JAR files resources.
218      */
219     @VisibleForTesting
220     static class ExternalInterceptingEntityResolver implements EntityResolver {
221         private static final List<String> SCHEMA_LOCATION_PREFIXES_TO_INTERCEPT = List.of(
222                 // Canonical remote location for schemas published by Dependency-Check
223                 "https://dependency-check.github.io/DependencyCheck/",
224 
225                 // Legacy remote location for schemas published by Dependency-Check
226                 "https://jeremylong.github.io/DependencyCheck/",
227 
228                 // improper URIs, e.g "schema.xsd" will be assumed as file URIs relative to current working directory
229                 Path.of("").toUri().toString()
230         );
231 
232         private final List<InputSource> inputSources;
233 
234         @VisibleForTesting
235         ExternalInterceptingEntityResolver(InputSource[] inputSources) {
236             this.inputSources = List.of(inputSources);
237         }
238 
239         @Override
240         public InputSource resolveEntity(String publicId, String systemId) {
241            return Optional.ofNullable(systemId)
242                    .map(this::toNormalizedResourceSystemId)
243                    .flatMap(this::toKnownResourcePath)
244                    .orElse(null);
245         }
246 
247         private String toNormalizedResourceSystemId(String systemId) {
248             return matchedPrefixFor(systemId).map(prefix -> systemId.substring(prefix.length())).orElse(systemId);
249         }
250 
251         private Optional<String> matchedPrefixFor(String systemId) {
252             return SCHEMA_LOCATION_PREFIXES_TO_INTERCEPT.stream().filter(systemId::startsWith).findFirst();
253         }
254 
255         private Optional<InputSource> toKnownResourcePath(String resourceFilename) {
256             return inputSources.stream().filter( s -> resourceFilename.equals(s.getSystemId())).findFirst();
257         }
258     }
259 
260 }