1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
42
43
44
45
46 public final class XmlUtils {
47
48
49
50
51
52 public static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
53
54
55
56
57 public static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";
58
59
60
61
62 public static final String JAXP_SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource";
63
64
65
66
67 private XmlUtils() {
68 }
69
70
71
72
73
74
75
76
77
78
79
80
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
92 saxParser.setProperty(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
93 saxParser.setProperty(JAXP_SCHEMA_SOURCE, schemas);
94
95
96 XMLReader xmlReader = saxParser.getXMLReader();
97 xmlReader.setEntityResolver(new ExternalInterceptingEntityResolver(schemas));
98
99 return xmlReader;
100 }
101
102
103
104
105
106
107
108
109
110
111 public static XMLReader buildSecureXmlReader() throws ParserConfigurationException,
112 SAXException {
113 return buildSecureSaxParser().getXMLReader();
114 }
115
116
117
118
119
120
121
122
123
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
144
145
146
147
148
149
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
160
161 factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
162
163
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
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
176
177
178
179
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
191
192
193
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
218
219 @VisibleForTesting
220 static class ExternalInterceptingEntityResolver implements EntityResolver {
221 private static final List<String> SCHEMA_LOCATION_PREFIXES_TO_INTERCEPT = List.of(
222
223 "https://dependency-check.github.io/DependencyCheck/",
224
225
226 "https://jeremylong.github.io/DependencyCheck/",
227
228
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 }