Skip to content

Commit 6ba85be

Browse files
committed
XML parser: refactor hardening to XMLSecurity class, and make sure parser used in JAXBSerialiser on a SAXSource is hardened if not using MOXy (which sets up its own hardened parser, and varies behaviour if called with raw source vs a parsed source)
1 parent d69c113 commit 6ba85be

4 files changed

Lines changed: 207 additions & 13 deletions

File tree

‎stdlib/src/main/java/com/peterphi/std/util/DOMUtils.java‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,7 @@ private static DocumentBuilderFactory createDocumentBuilderFactory()
5050
{
5151
factory.setNamespaceAware(true);
5252

53-
// Disable DTDs
54-
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false);
55-
56-
57-
factory.setXIncludeAware(false);
58-
factory.setExpandEntityReferences(false);
53+
XMLSecurity.harden(factory);
5954
}
6055
catch (ParserConfigurationException e)
6156
{
@@ -66,6 +61,23 @@ private static DocumentBuilderFactory createDocumentBuilderFactory()
6661
}
6762

6863

64+
private static TransformerFactory createTransformerFactory()
65+
{
66+
TransformerFactory factory = TransformerFactory.newInstance();
67+
68+
try
69+
{
70+
XMLSecurity.harden(factory);
71+
}
72+
catch (TransformerConfigurationException e)
73+
{
74+
throw new RuntimeException("Could not configure XML TransformerFactory!", e);
75+
}
76+
77+
return factory;
78+
}
79+
80+
6981
/**
7082
* Create a new (namespace-aware) DocumentBuilder
7183
*
@@ -191,7 +203,7 @@ public static void serialise(Node n, StreamResult result)
191203

192204
try
193205
{
194-
Transformer transform = TransformerFactory.newInstance().newTransformer();
206+
Transformer transform = createTransformerFactory().newTransformer();
195207
transform.transform(new DOMSource(n), result);
196208
}
197209
catch (TransformerConfigurationException e)
@@ -323,7 +335,7 @@ public static void pretty(final Source input, final StreamResult output)
323335
try
324336
{
325337
// Configure transformer
326-
Transformer transformer = TransformerFactory.newInstance().newTransformer();
338+
Transformer transformer = createTransformerFactory().newTransformer();
327339

328340
transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8");
329341
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");

‎stdlib/src/main/java/com/peterphi/std/util/JDOMUtils.java‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,18 @@ public static org.jdom2.Element convert(org.w3c.dom.Element node)
117117
// Parse
118118
//
119119

120+
private static SAXBuilder createHardenedSAXBuilder()
121+
{
122+
SAXBuilder builder = new SAXBuilder();
123+
124+
builder.setFeature(XMLSecurity.DISALLOW_DOCTYPE_DECL, true);
125+
builder.setFeature(XMLSecurity.EXTERNAL_GENERAL_ENTITIES, false);
126+
builder.setFeature(XMLSecurity.EXTERNAL_PARAMETER_ENTITIES, false);
127+
builder.setExpandEntities(false);
128+
129+
return builder;
130+
}
131+
120132
public static Document parse(String xml)
121133
{
122134
return parse(new StringReader(xml));
@@ -151,7 +163,7 @@ public static Document parse(InputStream is)
151163
{
152164
try
153165
{
154-
return new SAXBuilder().build(is);
166+
return createHardenedSAXBuilder().build(is);
155167
}
156168
catch (IOException e)
157169
{
@@ -167,7 +179,7 @@ public static Document parse(Reader reader)
167179
{
168180
try
169181
{
170-
return new SAXBuilder().build(reader);
182+
return createHardenedSAXBuilder().build(reader);
171183
}
172184
catch (IOException e)
173185
{
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.peterphi.std.util;
2+
3+
import org.xml.sax.SAXException;
4+
5+
import javax.xml.XMLConstants;
6+
import javax.xml.parsers.DocumentBuilderFactory;
7+
import javax.xml.parsers.ParserConfigurationException;
8+
import javax.xml.parsers.SAXParserFactory;
9+
import javax.xml.transform.TransformerConfigurationException;
10+
import javax.xml.transform.TransformerFactory;
11+
12+
/**
13+
* Hardens JAXP XML factories against XXE by disabling DTDs and external entities. Security features fail fast: if a factory
14+
* does not honour a feature, configuration throws rather than silently leaving the parser vulnerable.
15+
*/
16+
public final class XMLSecurity
17+
{
18+
public static final String DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl";
19+
public static final String EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities";
20+
public static final String EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities";
21+
public static final String LOAD_EXTERNAL_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
22+
23+
private XMLSecurity()
24+
{
25+
}
26+
27+
28+
public static void harden(final DocumentBuilderFactory factory) throws ParserConfigurationException
29+
{
30+
factory.setFeature(DISALLOW_DOCTYPE_DECL, true);
31+
factory.setFeature(EXTERNAL_GENERAL_ENTITIES, false);
32+
factory.setFeature(EXTERNAL_PARAMETER_ENTITIES, false);
33+
factory.setFeature(LOAD_EXTERNAL_DTD, false);
34+
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
35+
36+
factory.setXIncludeAware(false);
37+
factory.setExpandEntityReferences(false);
38+
}
39+
40+
41+
public static void harden(final SAXParserFactory factory) throws ParserConfigurationException, SAXException
42+
{
43+
factory.setFeature(DISALLOW_DOCTYPE_DECL, true);
44+
factory.setFeature(EXTERNAL_GENERAL_ENTITIES, false);
45+
factory.setFeature(EXTERNAL_PARAMETER_ENTITIES, false);
46+
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
47+
}
48+
49+
50+
public static void harden(final TransformerFactory factory) throws TransformerConfigurationException
51+
{
52+
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
53+
54+
// ACCESS_EXTERNAL_* are optional; not all TransformerFactory implementations support them
55+
trySetAttribute(factory, XMLConstants.ACCESS_EXTERNAL_DTD, "");
56+
trySetAttribute(factory, XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
57+
}
58+
59+
60+
private static void trySetAttribute(final TransformerFactory factory, final String name, final Object value)
61+
{
62+
try
63+
{
64+
factory.setAttribute(name, value);
65+
}
66+
catch (IllegalArgumentException e)
67+
{
68+
// Attribute not supported by this implementation
69+
}
70+
}
71+
}

‎stdlib/src/main/java/com/peterphi/std/util/jaxb/JAXBSerialiser.java‎

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
package com.peterphi.std.util.jaxb;
22

33
import com.peterphi.std.util.DOMUtils;
4+
import com.peterphi.std.util.XMLSecurity;
45
import com.peterphi.std.util.jaxb.exception.JAXBRuntimeException;
56
import org.slf4j.Logger;
67
import org.slf4j.LoggerFactory;
78
import org.w3c.dom.Document;
89
import org.w3c.dom.Element;
910
import org.w3c.dom.Node;
1011
import org.xml.sax.InputSource;
12+
import org.xml.sax.SAXException;
13+
import org.xml.sax.XMLReader;
1114

1215
import javax.xml.bind.JAXBContext;
1316
import javax.xml.bind.JAXBElement;
1417
import javax.xml.bind.JAXBException;
1518
import javax.xml.bind.Marshaller;
1619
import javax.xml.bind.PropertyException;
1720
import javax.xml.bind.Unmarshaller;
21+
import javax.xml.parsers.ParserConfigurationException;
22+
import javax.xml.parsers.SAXParserFactory;
1823
import javax.xml.stream.XMLStreamReader;
1924
import javax.xml.stream.XMLStreamWriter;
2025
import javax.xml.transform.Source;
26+
import javax.xml.transform.sax.SAXSource;
2127
import javax.xml.validation.Schema;
2228
import java.io.File;
2329
import java.io.InputStream;
@@ -37,7 +43,16 @@ public class JAXBSerialiser
3743
{
3844
private static final Logger log = LoggerFactory.getLogger(JAXBSerialiser.class);
3945

46+
private static SAXParserFactory SAX_PARSER_FACTORY;
47+
4048
private final JAXBContext context;
49+
50+
/**
51+
* True if the underlying JAXB provider hardens its own internally-created XML parser against XXE (so we should let it own
52+
* the parse rather than supplying our own hardened parser via a {@link SAXSource}).
53+
*/
54+
private final boolean isMOXY;
55+
4156
private Schema schema;
4257
private boolean prettyOutput = false;
4358

@@ -62,6 +77,8 @@ private JAXBSerialiser(String contextPath)
6277
{
6378
throw new JAXBRuntimeException("Error creating JAXB Context: " + e.getMessage(), e);
6479
}
80+
81+
this.isMOXY = isMOXY(this.context);
6582
}
6683

6784

@@ -73,6 +90,7 @@ private JAXBSerialiser(String contextPath)
7390
private JAXBSerialiser(JAXBContext context)
7491
{
7592
this.context = context;
93+
this.isMOXY = isMOXY(context);
7694
}
7795

7896

@@ -91,6 +109,8 @@ private JAXBSerialiser(Class<?>... classes)
91109
{
92110
throw new JAXBRuntimeException("Error creating JAXB Context: " + e.getMessage(), e);
93111
}
112+
113+
this.isMOXY = isMOXY(this.context);
94114
}
95115

96116

@@ -282,6 +302,56 @@ private Unmarshaller getUnmarshaller()
282302
}
283303
}
284304

305+
306+
/**
307+
* Tests if this provider is eclipselink moxy.
308+
* @param context the JAXB context (may be null, defaults to system in this instance)
309+
*
310+
* @return true if the provider is from eclipselink
311+
*/
312+
private static boolean isMOXY(final JAXBContext context)
313+
{
314+
return context != null && context.getClass().getName().startsWith("org.eclipse.persistence.");
315+
}
316+
317+
318+
private static SAXParserFactory getHardenedSAXParserFactory()
319+
{
320+
if (SAX_PARSER_FACTORY == null)
321+
{
322+
try
323+
{
324+
final SAXParserFactory spf = SAXParserFactory.newInstance();
325+
326+
XMLSecurity.harden(spf);
327+
spf.setNamespaceAware(true);
328+
329+
SAX_PARSER_FACTORY = spf;
330+
}
331+
catch (ParserConfigurationException | SAXException e)
332+
{
333+
throw new JAXBRuntimeException("Unable to set up SAXParserFactory to securely decode XML inputs!", e);
334+
}
335+
}
336+
337+
return SAX_PARSER_FACTORY;
338+
}
339+
340+
341+
private static SAXSource toHardenedSource(final InputSource inputSource)
342+
{
343+
try
344+
{
345+
final XMLReader reader = getHardenedSAXParserFactory().newSAXParser().getXMLReader();
346+
347+
return new SAXSource(reader, inputSource);
348+
}
349+
catch (ParserConfigurationException | SAXException e)
350+
{
351+
throw new JAXBRuntimeException("Error setting up secure XML source for deserialisation", e);
352+
}
353+
}
354+
285355
//
286356
//
287357
// Deserialisers
@@ -428,7 +498,17 @@ public Object deserialise(final File file)
428498

429499
try
430500
{
431-
final Object obj = unmarshaller.unmarshal(file);
501+
final Object obj;
502+
if (isMOXY)
503+
{
504+
obj = unmarshaller.unmarshal(file);
505+
}
506+
else
507+
{
508+
final InputSource inputSource = new InputSource(file.toURI().toASCIIString());
509+
510+
obj = unmarshaller.unmarshal(toHardenedSource(inputSource));
511+
}
432512

433513
if (obj == null)
434514
throw new RuntimeException("Malformed XML from " + file);
@@ -451,7 +531,9 @@ public Object deserialise(final InputSource source)
451531

452532
try
453533
{
454-
final Object obj = unmarshaller.unmarshal(source);
534+
// MOXy disables DTDs and external entities by default when it owns the parse, so wrapping its input in a parser we build ourselves is unnecessary (MOXy also applies stricter type coercion on a {@link SAXSource} with an external reader)
535+
// A StreamSource carries raw, unparsed XML; other Source types are already parsed or carry their own reader.
536+
final Object obj = isMOXY ? unmarshaller.unmarshal(source) : unmarshaller.unmarshal(toHardenedSource(source));
455537

456538
if (obj == null)
457539
throw new RuntimeException("Malformed XML! JAXB returned null");
@@ -474,7 +556,24 @@ public Object deserialise(final Source source)
474556

475557
try
476558
{
477-
final Object obj = unmarshaller.unmarshal(source);
559+
// MOXy disables DTDs and external entities by default when it owns the parse, so wrapping its input in a parser we build ourselves is unnecessary (MOXy also applies stricter type coercion on a {@link SAXSource} with an external reader)
560+
// A StreamSource carries raw, unparsed XML; other Source types are already parsed or carry their own reader.
561+
final Source effectiveSource;
562+
if (!isMOXY && source instanceof javax.xml.transform.stream.StreamSource stream)
563+
{
564+
final InputSource inputSource = new InputSource();
565+
inputSource.setSystemId(stream.getSystemId());
566+
inputSource.setByteStream(stream.getInputStream());
567+
inputSource.setCharacterStream(stream.getReader());
568+
569+
effectiveSource = toHardenedSource(inputSource);
570+
}
571+
else
572+
{
573+
effectiveSource = source;
574+
}
575+
576+
final Object obj = unmarshaller.unmarshal(effectiveSource);
478577

479578
if (obj == null)
480579
throw new RuntimeException("Malformed XML! JAXB returned null");

0 commit comments

Comments
 (0)