CVE-2014-0054
Description
The Jaxb2RootElementHttpMessageConverter in Spring MVC in Spring Framework before 3.2.8 and 4.0.0 before 4.0.2 does not disable external entity resolution, which allows remote attackers to read arbitrary files, cause a denial of service, and conduct CSRF attacks via crafted XML, aka an XML External Entity (XXE) issue. NOTE: this vulnerability exists because of an incomplete fix for CVE-2013-4152, CVE-2013-7315, and CVE-2013-6429.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.springframework:spring-webmvcMaven | < 3.2.8 | 3.2.8 |
org.springframework:spring-webmvcMaven | >= 4.0.0, < 4.0.2 | 4.0.2 |
Affected products
34cpe:2.3:a:springsource:spring_framework:3.0.0:*:*:*:*:*:*:*+ 18 more
- cpe:2.3:a:springsource:spring_framework:3.0.0:*:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.0.0.m1:*:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.0.0:m1:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.0.0.m2:*:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.0.0:m2:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.0.0:m3:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.0.0:m4:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.0.0:rc1:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.0.0:rc2:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.0.0:rc3:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.0.1:*:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.0.2:*:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.0.3:*:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.0.4:*:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.0.5:*:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.2.5:*:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:3.2.6:*:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:4.0.0:rc1:*:*:*:*:*:*
- cpe:2.3:a:springsource:spring_framework:4.0.1:*:*:*:*:*:*:*
cpe:2.3:a:vmware:spring_framework:*:*:*:*:*:*:*:*+ 14 more
- cpe:2.3:a:vmware:spring_framework:*:*:*:*:*:*:*:*range: <=3.2.7
- cpe:2.3:a:vmware:spring_framework:3.0.6:*:*:*:*:*:*:*
- cpe:2.3:a:vmware:spring_framework:3.0.7:*:*:*:*:*:*:*
- cpe:2.3:a:vmware:spring_framework:3.1.0:*:*:*:*:*:*:*
- cpe:2.3:a:vmware:spring_framework:3.1.1:*:*:*:*:*:*:*
- cpe:2.3:a:vmware:spring_framework:3.1.2:*:*:*:*:*:*:*
- cpe:2.3:a:vmware:spring_framework:3.1.3:*:*:*:*:*:*:*
- cpe:2.3:a:vmware:spring_framework:3.1.4:*:*:*:*:*:*:*
- cpe:2.3:a:vmware:spring_framework:3.2.0:*:*:*:*:*:*:*
- cpe:2.3:a:vmware:spring_framework:3.2.1:*:*:*:*:*:*:*
- cpe:2.3:a:vmware:spring_framework:3.2.2:*:*:*:*:*:*:*
- cpe:2.3:a:vmware:spring_framework:3.2.3:*:*:*:*:*:*:*
- cpe:2.3:a:vmware:spring_framework:3.2.4:*:*:*:*:*:*:*
- cpe:2.3:a:vmware:spring_framework:4.0.0:milestone1:*:*:*:*:*:*
- cpe:2.3:a:vmware:spring_framework:4.0.0:milestone2:*:*:*:*:*:*
Patches
4fb0683c066e7Add processExternalEntities support to OXM
12 files changed · +327 −17
spring-oxm/src/main/java/org/springframework/oxm/castor/CastorMarshaller.java+7 −2 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -162,6 +162,11 @@ public void setEncoding(String encoding) { this.encoding = encoding; } + @Override + protected String getDefaultEncoding() { + return this.encoding; + } + /** * Set the locations of the Castor XML mapping files. */ @@ -604,7 +609,7 @@ protected final Object unmarshalReader(Reader reader) throws XmlMappingException } @Override - protected final Object unmarshalSaxReader(XMLReader xmlReader, InputSource inputSource) + protected Object unmarshalSaxReader(XMLReader xmlReader, InputSource inputSource) throws XmlMappingException, IOException { UnmarshalHandler unmarshalHandler = createUnmarshaller().createHandler();
spring-oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java+8 −1 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -401,6 +401,13 @@ public void setProcessExternalEntities(boolean processExternalEntities) { this.processExternalEntities = processExternalEntities; } + /** + * @return the configured value for whether XML external entities are allowed. + */ + public boolean isProcessExternalEntities() { + return this.processExternalEntities; + } + @Override public void setBeanClassLoader(ClassLoader classLoader) { this.beanClassLoader = classLoader;
spring-oxm/src/main/java/org/springframework/oxm/jibx/JibxMarshaller.java+14 −6 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamWriter; +import javax.xml.transform.OutputKeys; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; @@ -148,6 +149,11 @@ public void setEncoding(String encoding) { this.encoding = encoding; } + @Override + protected String getDefaultEncoding() { + return this.encoding; + } + /** * Set the document standalone flag for marshalling. By default, this flag is not present. */ @@ -389,13 +395,12 @@ protected Object unmarshalReader(Reader reader) throws XmlMappingException, IOEx } } - // Unsupported Unmarshalling @Override protected Object unmarshalDomNode(Node node) throws XmlMappingException { try { - return transformAndUnmarshal(new DOMSource(node)); + return transformAndUnmarshal(new DOMSource(node), null); } catch (IOException ex) { throw new UnmarshallingFailureException("JiBX unmarshalling exception", ex); @@ -406,20 +411,23 @@ protected Object unmarshalDomNode(Node node) throws XmlMappingException { protected Object unmarshalSaxReader(XMLReader xmlReader, InputSource inputSource) throws XmlMappingException, IOException { - return transformAndUnmarshal(new SAXSource(xmlReader, inputSource)); + return transformAndUnmarshal(new SAXSource(xmlReader, inputSource), inputSource.getEncoding()); } - private Object transformAndUnmarshal(Source source) throws IOException { + private Object transformAndUnmarshal(Source source, String encoding) throws IOException { try { Transformer transformer = this.transformerFactory.newTransformer(); + if (encoding != null) { + transformer.setOutputProperty(OutputKeys.ENCODING, encoding); + } ByteArrayOutputStream os = new ByteArrayOutputStream(); transformer.transform(source, new StreamResult(os)); ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()); return unmarshalInputStream(is); } catch (TransformerException ex) { throw new MarshallingFailureException( - "Could not transform from [" + ClassUtils.getShortName(source.getClass()) + "]"); + "Could not transform from [" + ClassUtils.getShortName(source.getClass()) + "]", ex); } }
spring-oxm/src/main/java/org/springframework/oxm/support/AbstractMarshaller.java+68 −4 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,6 +73,33 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller { private final Object documentBuilderFactoryMonitor = new Object(); + private boolean processExternalEntities = false; + + + /** + * Indicates whether external XML entities are processed when unmarshalling. + * <p>Default is {@code false}, meaning that external entities are not resolved. + * Note that processing of external entities will only be enabled/disabled when the + * {@code Source} passed to {@link #unmarshal(Source)} is a {@link SAXSource} or + * {@link StreamSource}. It has no effect for {@link DOMSource} or {@link StAXSource} + * instances. + */ + public void setProcessExternalEntities(boolean processExternalEntities) { + this.processExternalEntities = processExternalEntities; + } + + /** + * @return the configured value for whether XML external entities are allowed. + */ + public boolean isProcessExternalEntities() { + return this.processExternalEntities; + } + + /** + * @return the default encoding to use for marshalling or unmarshalling from + * a byte stream, or {@code null}. + */ + abstract protected String getDefaultEncoding(); /** * Marshals the object graph with the given root into the provided {@code javax.xml.transform.Result}. @@ -131,7 +158,7 @@ else if (source instanceof SAXSource) { return unmarshalSaxSource((SAXSource) source); } else if (source instanceof StreamSource) { - return unmarshalStreamSource((StreamSource) source); + return unmarshalStreamSourceNoExternalEntitities((StreamSource) source); } else { throw new IllegalArgumentException("Unknown Source type: " + source.getClass()); @@ -173,7 +200,9 @@ protected DocumentBuilderFactory createDocumentBuilderFactory() throws ParserCon * @throws SAXException if thrown by JAXP methods */ protected XMLReader createXmlReader() throws SAXException { - return XMLReaderFactory.createXMLReader(); + XMLReader xmlReader = XMLReaderFactory.createXMLReader(); + xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities()); + return xmlReader; } @@ -355,9 +384,44 @@ protected Object unmarshalSaxSource(SAXSource saxSource) throws XmlMappingExcept return unmarshalSaxReader(saxSource.getXMLReader(), saxSource.getInputSource()); } + /** + * Template method for handling {@code StreamSource}s with protection against + * the XML External Entity (XXE) processing vulnerability taking into account + * the value of the {@link #setProcessExternalEntities(boolean)} property. + * <p> + * The default implementation wraps the StreamSource as a SAXSource and delegates + * to {@link #unmarshalSaxSource(javax.xml.transform.sax.SAXSource)}. + * + * @param streamSource the {@code StreamSource} + * @return the object graph + * @throws IOException if an I/O exception occurs + * @throws XmlMappingException if the given source cannot be mapped to an object + * + * @see <a href="https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing">XML_External_Entity_(XXE)_Processing</a> + */ + protected Object unmarshalStreamSourceNoExternalEntitities(StreamSource streamSource) + throws XmlMappingException, IOException { + + InputSource inputSource; + if (streamSource.getInputStream() != null) { + inputSource = new InputSource(streamSource.getInputStream()); + inputSource.setEncoding(getDefaultEncoding()); + } + else if (streamSource.getReader() != null) { + inputSource = new InputSource(streamSource.getReader()); + } + else { + inputSource = new InputSource(streamSource.getSystemId()); + } + return unmarshalSaxSource(new SAXSource(inputSource)); + } + /** * Template method for handling {@code StreamSource}s. - * <p>This implementation defers to {@code unmarshalInputStream} or {@code unmarshalReader}. + * <p>As of 3.2.8 and 4.0.2 this method is no longer invoked from + * {@link #unmarshal(javax.xml.transform.Source)}. The method invoked instead is + * {@link #unmarshalStreamSourceNoExternalEntitities(javax.xml.transform.stream.StreamSource)}. + * * @param streamSource the {@code StreamSource} * @return the object graph * @throws IOException if an I/O exception occurs
spring-oxm/src/main/java/org/springframework/oxm/xmlbeans/XmlBeansMarshaller.java+4 −0 modified@@ -113,6 +113,10 @@ public boolean isValidating() { return this.validating; } + @Override + protected String getDefaultEncoding() { + return null; + } /** * This implementation returns true if the given class is an implementation of {@link XmlObject}.
spring-oxm/src/main/java/org/springframework/oxm/xstream/XStreamMarshaller.java+12 −1 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamWriter; +import javax.xml.transform.stream.StreamSource; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.ConversionException; @@ -353,6 +354,11 @@ public void setEncoding(String encoding) { this.encoding = encoding; } + @Override + protected String getDefaultEncoding() { + return this.encoding; + } + /** * Set the classes supported by this marshaller. * <p>If this property is empty (the default), all classes are supported. @@ -482,6 +488,11 @@ private void marshal(Object graph, HierarchicalStreamWriter streamWriter) { // Unmarshalling + @Override + protected Object unmarshalStreamSourceNoExternalEntitities(StreamSource streamSource) throws XmlMappingException, IOException { + return super.unmarshalStreamSource(streamSource); + } + @Override protected Object unmarshalDomNode(Node node) throws XmlMappingException { HierarchicalStreamReader streamReader;
spring-oxm/src/test/java/org/springframework/oxm/castor/CastorUnmarshallerTests.java+60 −1 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringReader; +import java.util.concurrent.atomic.AtomicReference; +import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamSource; import org.junit.Ignore; @@ -28,6 +30,8 @@ import org.springframework.oxm.AbstractUnmarshallerTests; import org.springframework.oxm.MarshallingException; import org.springframework.oxm.Unmarshaller; +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; @@ -203,4 +207,59 @@ private Object unmarshal(String xml) throws Exception { StreamSource source = new StreamSource(new StringReader(xml)); return unmarshaller.unmarshal(source); } + + @Test + public void unmarshalStreamSourceExternalEntities() throws Exception { + + final AtomicReference<XMLReader> result = new AtomicReference<XMLReader>(); + CastorMarshaller marshaller = new CastorMarshaller() { + @Override + protected Object unmarshalSaxReader(XMLReader xmlReader, InputSource inputSource) { + result.set(xmlReader); + return null; + } + }; + + // 1. external-general-entities disabled (default) + + marshaller.unmarshal(new StreamSource("1")); + assertNotNull(result.get()); + assertEquals(false, result.get().getFeature("http://xml.org/sax/features/external-general-entities")); + + // 2. external-general-entities disabled (default) + + result.set(null); + marshaller.setProcessExternalEntities(true); + marshaller.unmarshal(new StreamSource("1")); + assertNotNull(result.get()); + assertEquals(true, result.get().getFeature("http://xml.org/sax/features/external-general-entities")); + } + + @Test + public void unmarshalSaxSourceExternalEntities() throws Exception { + + final AtomicReference<XMLReader> result = new AtomicReference<XMLReader>(); + CastorMarshaller marshaller = new CastorMarshaller() { + @Override + protected Object unmarshalSaxReader(XMLReader xmlReader, InputSource inputSource) { + result.set(xmlReader); + return null; + } + }; + + // 1. external-general-entities disabled (default) + + marshaller.unmarshal(new SAXSource(new InputSource("1"))); + assertNotNull(result.get()); + assertEquals(false, result.get().getFeature("http://xml.org/sax/features/external-general-entities")); + + // 2. external-general-entities disabled (default) + + result.set(null); + marshaller.setProcessExternalEntities(true); + marshaller.unmarshal(new SAXSource(new InputSource("1"))); + assertNotNull(result.get()); + assertEquals(true, result.get().getFeature("http://xml.org/sax/features/external-general-entities")); + } + }
spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java+74 −1 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,9 +31,12 @@ import javax.xml.namespace.QName; import javax.xml.transform.Result; import javax.xml.transform.sax.SAXResult; +import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -49,6 +52,7 @@ import org.springframework.util.ReflectionUtils; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; import org.xml.sax.Locator; @@ -301,6 +305,75 @@ public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Excepti writer.toString(), "<airplane><name>test</name></airplane>"); } + // SPR-10806 + + @Test + public void unmarshalStreamSourceExternalEntities() throws Exception { + + final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); + Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { + @Override + protected javax.xml.bind.Unmarshaller createUnmarshaller() { + return unmarshaller; + } + }; + + // 1. external-general-entities disabled (default) + + marshaller.unmarshal(new StreamSource("1")); + ArgumentCaptor<SAXSource> sourceCaptor = ArgumentCaptor.forClass(SAXSource.class); + verify(unmarshaller).unmarshal(sourceCaptor.capture()); + + SAXSource result = sourceCaptor.getValue(); + assertEquals(false, result.getXMLReader().getFeature("http://xml.org/sax/features/external-general-entities")); + + // 2. external-general-entities enabled + + reset(unmarshaller); + marshaller.setProcessExternalEntities(true); + + marshaller.unmarshal(new StreamSource("1")); + verify(unmarshaller).unmarshal(sourceCaptor.capture()); + + result = sourceCaptor.getValue(); + assertEquals(true, result.getXMLReader().getFeature("http://xml.org/sax/features/external-general-entities")); + } + + // SPR-10806 + + @Test + public void unmarshalSaxSourceExternalEntities() throws Exception { + + final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); + Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { + @Override + protected javax.xml.bind.Unmarshaller createUnmarshaller() { + return unmarshaller; + } + }; + + // 1. external-general-entities disabled (default) + + marshaller.unmarshal(new SAXSource(new InputSource("1"))); + ArgumentCaptor<SAXSource> sourceCaptor = ArgumentCaptor.forClass(SAXSource.class); + verify(unmarshaller).unmarshal(sourceCaptor.capture()); + + SAXSource result = sourceCaptor.getValue(); + assertEquals(false, result.getXMLReader().getFeature("http://xml.org/sax/features/external-general-entities")); + + // 2. external-general-entities enabled + + reset(unmarshaller); + marshaller.setProcessExternalEntities(true); + + marshaller.unmarshal(new SAXSource(new InputSource("1"))); + verify(unmarshaller).unmarshal(sourceCaptor.capture()); + + result = sourceCaptor.getValue(); + assertEquals(true, result.getXMLReader().getFeature("http://xml.org/sax/features/external-general-entities")); + } + + @XmlRootElement @SuppressWarnings("unused") public static class DummyRootElement {
spring-oxm/src/test/java/org/springframework/oxm/jibx/JibxUnmarshallerTests.java+3 −1 modified@@ -25,7 +25,9 @@ import org.springframework.oxm.AbstractUnmarshallerTests; import org.springframework.oxm.Unmarshaller; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + /** * @author Arjen Poutsma
spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java+39 −0 modified@@ -28,6 +28,8 @@ import javax.xml.bind.annotation.XmlType; import javax.xml.transform.Result; import javax.xml.transform.Source; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpHeaders; @@ -36,6 +38,10 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.util.ClassUtils; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; /** * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} that can read @@ -49,6 +55,18 @@ */ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessageConverter<Object> { + private boolean processExternalEntities = false; + + + /** + * Indicates whether external XML entities are processed when converting to a Source. + * <p>Default is {@code false}, meaning that external entities are not resolved. + */ + public void setProcessExternalEntities(boolean processExternalEntities) { + this.processExternalEntities = processExternalEntities; + } + + @Override public boolean canRead(Class<?> clazz, MediaType mediaType) { return (clazz.isAnnotationPresent(XmlRootElement.class) || clazz.isAnnotationPresent(XmlType.class)) && @@ -69,6 +87,7 @@ protected boolean supports(Class<?> clazz) { @Override protected Object readFromSource(Class<?> clazz, HttpHeaders headers, Source source) throws IOException { try { + source = processSource(source); Unmarshaller unmarshaller = createUnmarshaller(clazz); if (clazz.isAnnotationPresent(XmlRootElement.class)) { return unmarshaller.unmarshal(source); @@ -87,6 +106,26 @@ protected Object readFromSource(Class<?> clazz, HttpHeaders headers, Source sour } } + protected Source processSource(Source source) { + if (source instanceof StreamSource) { + StreamSource streamSource = (StreamSource) source; + InputSource inputSource = new InputSource(streamSource.getInputStream()); + try { + XMLReader xmlReader = XMLReaderFactory.createXMLReader(); + String featureName = "http://xml.org/sax/features/external-general-entities"; + xmlReader.setFeature(featureName, this.processExternalEntities); + return new SAXSource(xmlReader, inputSource); + } + catch (SAXException ex) { + logger.warn("Processing of external entities could not be disabled", ex); + return source; + } + } + else { + return source; + } + } + @Override protected void writeToResult(Object o, HttpHeaders headers, Result result) throws IOException { try {
spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java+6 −0 modified@@ -90,6 +90,12 @@ public void setProcessExternalEntities(boolean processExternalEntities) { this.processExternalEntities = processExternalEntities; } + /** + * @return the configured value for whether XML external entities are allowed. + */ + public boolean isProcessExternalEntities() { + return this.processExternalEntities; + } @Override public boolean supports(Class<?> clazz) {
spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTest.java+32 −0 modified@@ -32,6 +32,8 @@ import org.springframework.aop.framework.AdvisedSupport; import org.springframework.aop.framework.AopProxy; import org.springframework.aop.framework.DefaultAopProxyFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpOutputMessage; @@ -95,6 +97,33 @@ public void readXmlType() throws Exception { assertEquals("Invalid result", "Hello World", result.s); } + @Test + public void readXmlRootElementExternalEntityDisabled() throws Exception { + Resource external = new ClassPathResource("external.txt", getClass()); + String content = "<!DOCTYPE root [" + + " <!ELEMENT external ANY >\n" + + " <!ENTITY ext SYSTEM \"" + external.getURI() + "\" >]>" + + " <rootElement><external>&ext;</external></rootElement>"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); + RootElement rootElement = (RootElement) converter.read(RootElement.class, inputMessage); + + assertEquals("", rootElement.external); + } + + @Test + public void readXmlRootElementExternalEntityEnabled() throws Exception { + Resource external = new ClassPathResource("external.txt", getClass()); + String content = "<!DOCTYPE root [" + + " <!ELEMENT external ANY >\n" + + " <!ENTITY ext SYSTEM \"" + external.getURI() + "\" >]>" + + " <rootElement><external>&ext;</external></rootElement>"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); + this.converter.setProcessExternalEntities(true); + RootElement rootElement = (RootElement) converter.read(RootElement.class, inputMessage); + + assertEquals("Foo Bar", rootElement.external); + } + @Test public void writeXmlRootElement() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); @@ -121,6 +150,9 @@ public static class RootElement { @XmlElement public Type type = new Type(); + @XmlElement(required=false) + public String external; + } @XmlType
edba32b30937Add processExternalEntities support to OXM
13 files changed · +349 −26
spring-oxm/src/main/java/org/springframework/oxm/castor/CastorMarshaller.java+5 −0 modified@@ -162,6 +162,11 @@ public void setEncoding(String encoding) { this.encoding = encoding; } + @Override + protected String getDefaultEncoding() { + return this.encoding; + } + /** * Set the locations of the Castor XML mapping files. */
spring-oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java+7 −0 modified@@ -400,6 +400,13 @@ public void setProcessExternalEntities(boolean processExternalEntities) { this.processExternalEntities = processExternalEntities; } + /** + * @return the configured value for whether XML external entities are allowed. + */ + public boolean isProcessExternalEntities() { + return this.processExternalEntities; + } + @Override public void setBeanClassLoader(ClassLoader classLoader) { this.beanClassLoader = classLoader;
spring-oxm/src/main/java/org/springframework/oxm/jibx/JibxMarshaller.java+15 −6 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamWriter; +import javax.xml.transform.OutputKeys; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; @@ -149,6 +150,11 @@ public void setEncoding(String encoding) { this.encoding = encoding; } + @Override + protected String getDefaultEncoding() { + return this.encoding; + } + /** * Set the document standalone flag for marshalling. By default, this flag is not present. */ @@ -338,7 +344,7 @@ private void transformAndMarshal(Object graph, Result result) throws IOException } catch (TransformerException ex) { throw new MarshallingFailureException( - "Could not transform to [" + ClassUtils.getShortName(result.getClass()) + "]"); + "Could not transform to [" + ClassUtils.getShortName(result.getClass()) + "]", ex); } } @@ -398,7 +404,7 @@ protected Object unmarshalReader(Reader reader) throws XmlMappingException, IOEx @Override protected Object unmarshalDomNode(Node node) throws XmlMappingException { try { - return transformAndUnmarshal(new DOMSource(node)); + return transformAndUnmarshal(new DOMSource(node), null); } catch (IOException ex) { throw new UnmarshallingFailureException("JiBX unmarshalling exception", ex); @@ -409,20 +415,23 @@ protected Object unmarshalDomNode(Node node) throws XmlMappingException { protected Object unmarshalSaxReader(XMLReader xmlReader, InputSource inputSource) throws XmlMappingException, IOException { - return transformAndUnmarshal(new SAXSource(xmlReader, inputSource)); + return transformAndUnmarshal(new SAXSource(xmlReader, inputSource), inputSource.getEncoding()); } - private Object transformAndUnmarshal(Source source) throws IOException { + private Object transformAndUnmarshal(Source source, String encoding) throws IOException { try { Transformer transformer = this.transformerFactory.newTransformer(); + if (encoding != null) { + transformer.setOutputProperty(OutputKeys.ENCODING, encoding); + } ByteArrayOutputStream os = new ByteArrayOutputStream(); transformer.transform(source, new StreamResult(os)); ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()); return unmarshalInputStream(is); } catch (TransformerException ex) { throw new MarshallingFailureException( - "Could not transform from [" + ClassUtils.getShortName(source.getClass()) + "]"); + "Could not transform from [" + ClassUtils.getShortName(source.getClass()) + "]", ex); } }
spring-oxm/src/main/java/org/springframework/oxm/support/AbstractMarshaller.java+67 −3 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,6 +73,34 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller { private final Object documentBuilderFactoryMonitor = new Object(); + private boolean processExternalEntities = false; + + + /** + * Indicates whether external XML entities are processed when unmarshalling. + * <p>Default is {@code false}, meaning that external entities are not resolved. + * Note that processing of external entities will only be enabled/disabled when the + * {@code Source} passed to {@link #unmarshal(Source)} is a {@link SAXSource} or + * {@link StreamSource}. It has no effect for {@link DOMSource} or {@link StAXSource} + * instances. + */ + public void setProcessExternalEntities(boolean processExternalEntities) { + this.processExternalEntities = processExternalEntities; + } + + /** + * @return the configured value for whether XML external entities are allowed. + */ + public boolean isProcessExternalEntities() { + return this.processExternalEntities; + } + + /** + * @return the default encoding to use for marshalling or unmarshalling from + * a byte stream, or {@code null}. + */ + abstract protected String getDefaultEncoding(); + /** * Marshals the object graph with the given root into the provided {@code javax.xml.transform.Result}. @@ -133,7 +161,7 @@ else if (source instanceof SAXSource) { return unmarshalSaxSource((SAXSource) source); } else if (source instanceof StreamSource) { - return unmarshalStreamSource((StreamSource) source); + return unmarshalStreamSourceNoExternalEntitities((StreamSource) source); } else { throw new IllegalArgumentException("Unknown Source type: " + source.getClass()); @@ -175,7 +203,9 @@ protected DocumentBuilderFactory createDocumentBuilderFactory() throws ParserCon * @throws SAXException if thrown by JAXP methods */ protected XMLReader createXmlReader() throws SAXException { - return XMLReaderFactory.createXMLReader(); + XMLReader xmlReader = XMLReaderFactory.createXMLReader(); + xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities()); + return xmlReader; } @@ -357,9 +387,43 @@ protected Object unmarshalSaxSource(SAXSource saxSource) throws XmlMappingExcept return unmarshalSaxReader(saxSource.getXMLReader(), saxSource.getInputSource()); } + /** + * Template method for handling {@code StreamSource}s with protection against + * the XML External Entity (XXE) processing vulnerability taking into account + * the value of the {@link #setProcessExternalEntities(boolean)} property. + * <p> + * The default implementation wraps the StreamSource as a SAXSource and delegates + * to {@link #unmarshalSaxSource(javax.xml.transform.sax.SAXSource)}. + * + * @param streamSource the {@code StreamSource} + * @return the object graph + * @throws IOException if an I/O exception occurs + * @throws XmlMappingException if the given source cannot be mapped to an object + * + * @see <a href="https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing">XML_External_Entity_(XXE)_Processing</a> + */ + protected Object unmarshalStreamSourceNoExternalEntitities(StreamSource streamSource) throws XmlMappingException, IOException { + InputSource inputSource; + if (streamSource.getInputStream() != null) { + inputSource = new InputSource(streamSource.getInputStream()); + inputSource.setEncoding(getDefaultEncoding()); + } + else if (streamSource.getReader() != null) { + inputSource = new InputSource(streamSource.getReader()); + } + else { + inputSource = new InputSource(streamSource.getSystemId()); + } + return unmarshalSaxSource(new SAXSource(inputSource)); + } + /** * Template method for handling {@code StreamSource}s. * <p>This implementation defers to {@code unmarshalInputStream} or {@code unmarshalReader}. + * <p>As of 3.2.8 and 4.0.2 this method is no longer invoked from + * {@link #unmarshal(javax.xml.transform.Source)}. The method invoked instead is + * {@link #unmarshalStreamSourceNoExternalEntitities(javax.xml.transform.stream.StreamSource)}. + * * @param streamSource the {@code StreamSource} * @return the object graph * @throws IOException if an I/O exception occurs
spring-oxm/src/main/java/org/springframework/oxm/xmlbeans/XmlBeansMarshaller.java+5 −1 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -113,6 +113,10 @@ public boolean isValidating() { return this.validating; } + @Override + protected String getDefaultEncoding() { + return null; + } /** * This implementation returns true if the given class is an implementation of {@link XmlObject}.
spring-oxm/src/main/java/org/springframework/oxm/xstream/XStreamMarshaller.java+15 −5 modified@@ -27,11 +27,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import javax.xml.stream.XMLEventReader; -import javax.xml.stream.XMLEventWriter; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamReader; -import javax.xml.stream.XMLStreamWriter; +import javax.xml.stream.*; +import javax.xml.transform.stax.StAXSource; +import javax.xml.transform.stream.StreamSource; import com.thoughtworks.xstream.MarshallingStrategy; import com.thoughtworks.xstream.XStream; @@ -342,6 +340,11 @@ public void setEncoding(String encoding) { this.encoding = encoding; } + @Override + protected String getDefaultEncoding() { + return this.encoding; + } + /** * Set the classes supported by this marshaller. * <p>If this property is empty (the default), all classes are supported. @@ -700,6 +703,13 @@ private void doMarshal(Object graph, HierarchicalStreamWriter streamWriter, Data // Unmarshalling + @Override + protected Object unmarshalStreamSourceNoExternalEntitities(StreamSource streamSource) + throws XmlMappingException, IOException { + + return super.unmarshalStreamSource(streamSource); + } + @Override protected Object unmarshalDomNode(Node node) throws XmlMappingException { HierarchicalStreamReader streamReader;
spring-oxm/src/test/java/org/springframework/oxm/castor/CastorUnmarshallerTests.java+62 −1 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringReader; +import java.util.concurrent.atomic.AtomicReference; +import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamSource; import org.junit.Ignore; @@ -28,9 +30,13 @@ import org.springframework.oxm.AbstractUnmarshallerTests; import org.springframework.oxm.MarshallingException; import org.springframework.oxm.Unmarshaller; +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; +import static junit.framework.Assert.assertNotNull; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; /** * @author Arjen Poutsma @@ -203,4 +209,59 @@ private Object unmarshal(String xml) throws Exception { StreamSource source = new StreamSource(new StringReader(xml)); return unmarshaller.unmarshal(source); } + + @Test + public void unmarshalStreamSourceExternalEntities() throws Exception { + + final AtomicReference<XMLReader> result = new AtomicReference<XMLReader>(); + CastorMarshaller marshaller = new CastorMarshaller() { + @Override + protected Object unmarshalSaxReader(XMLReader xmlReader, InputSource inputSource) { + result.set(xmlReader); + return null; + } + }; + + // 1. external-general-entities disabled (default) + + marshaller.unmarshal(new StreamSource("1")); + assertNotNull(result.get()); + assertEquals(false, result.get().getFeature("http://xml.org/sax/features/external-general-entities")); + + // 2. external-general-entities disabled (default) + + result.set(null); + marshaller.setProcessExternalEntities(true); + marshaller.unmarshal(new StreamSource("1")); + assertNotNull(result.get()); + assertEquals(true, result.get().getFeature("http://xml.org/sax/features/external-general-entities")); + } + + @Test + public void unmarshalSaxSourceExternalEntities() throws Exception { + + final AtomicReference<XMLReader> result = new AtomicReference<XMLReader>(); + CastorMarshaller marshaller = new CastorMarshaller() { + @Override + protected Object unmarshalSaxReader(XMLReader xmlReader, InputSource inputSource) { + result.set(xmlReader); + return null; + } + }; + + // 1. external-general-entities disabled (default) + + marshaller.unmarshal(new SAXSource(new InputSource("1"))); + assertNotNull(result.get()); + assertEquals(false, result.get().getFeature("http://xml.org/sax/features/external-general-entities")); + + // 2. external-general-entities disabled (default) + + result.set(null); + marshaller.setProcessExternalEntities(true); + marshaller.unmarshal(new SAXSource(new InputSource("1"))); + assertNotNull(result.get()); + assertEquals(true, result.get().getFeature("http://xml.org/sax/features/external-general-entities")); + } + }
spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java+75 −5 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,9 +31,12 @@ import javax.xml.namespace.QName; import javax.xml.transform.Result; import javax.xml.transform.sax.SAXResult; +import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -47,9 +50,7 @@ import org.springframework.oxm.mime.MimeContainer; import org.springframework.util.FileCopyUtils; import org.springframework.util.ReflectionUtils; -import org.xml.sax.Attributes; -import org.xml.sax.ContentHandler; -import org.xml.sax.Locator; +import org.xml.sax.*; import static org.junit.Assert.*; import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; @@ -289,7 +290,7 @@ public void marshalAttachments() throws Exception { public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { // SPR-10714 marshaller = new Jaxb2Marshaller(); - marshaller.setPackagesToScan(new String[] { "org.springframework.oxm.jaxb" }); + marshaller.setPackagesToScan(new String[]{"org.springframework.oxm.jaxb"}); marshaller.afterPropertiesSet(); Airplane airplane = new Airplane(); airplane.setName("test"); @@ -300,6 +301,75 @@ public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Excepti writer.toString(), "<airplane><name>test</name></airplane>"); } + // SPR-10806 + + @Test + public void unmarshalStreamSourceExternalEntities() throws Exception { + + final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); + Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { + @Override + protected javax.xml.bind.Unmarshaller createUnmarshaller() { + return unmarshaller; + } + }; + + // 1. external-general-entities disabled (default) + + marshaller.unmarshal(new StreamSource("1")); + ArgumentCaptor<SAXSource> sourceCaptor = ArgumentCaptor.forClass(SAXSource.class); + verify(unmarshaller).unmarshal(sourceCaptor.capture()); + + SAXSource result = sourceCaptor.getValue(); + assertEquals(false, result.getXMLReader().getFeature("http://xml.org/sax/features/external-general-entities")); + + // 2. external-general-entities enabled + + reset(unmarshaller); + marshaller.setProcessExternalEntities(true); + + marshaller.unmarshal(new StreamSource("1")); + verify(unmarshaller).unmarshal(sourceCaptor.capture()); + + result = sourceCaptor.getValue(); + assertEquals(true, result.getXMLReader().getFeature("http://xml.org/sax/features/external-general-entities")); + } + + // SPR-10806 + + @Test + public void unmarshalSaxSourceExternalEntities() throws Exception { + + final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); + Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { + @Override + protected javax.xml.bind.Unmarshaller createUnmarshaller() { + return unmarshaller; + } + }; + + // 1. external-general-entities disabled (default) + + marshaller.unmarshal(new SAXSource(new InputSource("1"))); + ArgumentCaptor<SAXSource> sourceCaptor = ArgumentCaptor.forClass(SAXSource.class); + verify(unmarshaller).unmarshal(sourceCaptor.capture()); + + SAXSource result = sourceCaptor.getValue(); + assertEquals(false, result.getXMLReader().getFeature("http://xml.org/sax/features/external-general-entities")); + + // 2. external-general-entities enabled + + reset(unmarshaller); + marshaller.setProcessExternalEntities(true); + + marshaller.unmarshal(new SAXSource(new InputSource("1"))); + verify(unmarshaller).unmarshal(sourceCaptor.capture()); + + result = sourceCaptor.getValue(); + assertEquals(true, result.getXMLReader().getFeature("http://xml.org/sax/features/external-general-entities")); + } + + @XmlRootElement @SuppressWarnings("unused") public static class DummyRootElement {
spring-oxm/src/test/java/org/springframework/oxm/jibx/JibxMarshallerTests.java+13 −1 modified@@ -16,21 +16,34 @@ package org.springframework.oxm.jibx; +import java.io.IOException; import java.io.StringWriter; +import java.util.concurrent.atomic.AtomicReference; +import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; import org.custommonkey.xmlunit.XMLUnit; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.springframework.oxm.AbstractMarshallerTests; import org.springframework.oxm.Marshaller; +import org.springframework.oxm.XmlMappingException; +import org.springframework.oxm.jaxb.Jaxb2Marshaller; import org.springframework.tests.Assume; import org.springframework.tests.TestGroup; +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; import static org.custommonkey.xmlunit.XMLAssert.*; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; /** * @author Arjen Poutsma @@ -107,5 +120,4 @@ public void testSupports() throws Exception { assertFalse("JibxMarshaller supports illegal type", marshaller.supports(getClass())); } - }
spring-oxm/src/test/java/org/springframework/oxm/jibx/JibxUnmarshallerTests.java+3 −1 modified@@ -28,7 +28,9 @@ import org.springframework.tests.Assume; import org.springframework.tests.TestGroup; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + /** * @author Arjen Poutsma
spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java+41 −1 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,9 @@ import javax.xml.bind.annotation.XmlType; import javax.xml.transform.Result; import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpHeaders; @@ -36,6 +39,11 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.util.ClassUtils; +import org.springframework.util.xml.StaxUtils; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; /** * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} that can read @@ -49,6 +57,17 @@ */ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessageConverter<Object> { + private boolean processExternalEntities = false; + + + /** + * Indicates whether external XML entities are processed when converting to a Source. + * <p>Default is {@code false}, meaning that external entities are not resolved. + */ + public void setProcessExternalEntities(boolean processExternalEntities) { + this.processExternalEntities = processExternalEntities; + } + @Override public boolean canRead(Class<?> clazz, MediaType mediaType) { return (clazz.isAnnotationPresent(XmlRootElement.class) || clazz.isAnnotationPresent(XmlType.class)) && @@ -69,6 +88,7 @@ protected boolean supports(Class<?> clazz) { @Override protected Object readFromSource(Class<?> clazz, HttpHeaders headers, Source source) throws IOException { try { + source = processSource(source); Unmarshaller unmarshaller = createUnmarshaller(clazz); if (clazz.isAnnotationPresent(XmlRootElement.class)) { return unmarshaller.unmarshal(source); @@ -87,6 +107,26 @@ protected Object readFromSource(Class<?> clazz, HttpHeaders headers, Source sour } } + protected Source processSource(Source source) { + if (source instanceof StreamSource) { + StreamSource streamSource = (StreamSource) source; + InputSource inputSource = new InputSource(streamSource.getInputStream()); + try { + XMLReader xmlReader = XMLReaderFactory.createXMLReader(); + String featureName = "http://xml.org/sax/features/external-general-entities"; + xmlReader.setFeature(featureName, this.processExternalEntities); + return new SAXSource(xmlReader, inputSource); + } + catch (SAXException ex) { + logger.warn("Processing of external entities could not be disabled", ex); + return source; + } + } + else { + return source; + } + } + @Override protected void writeToResult(Object o, HttpHeaders headers, Result result) throws IOException { try {
spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java+7 −2 modified@@ -95,6 +95,12 @@ public void setProcessExternalEntities(boolean processExternalEntities) { this.processExternalEntities = processExternalEntities; } + /** + * @return the configured value for whether XML external entities are allowed. + */ + public boolean isProcessExternalEntities() { + return this.processExternalEntities; + } @Override public boolean supports(Class<?> clazz) { @@ -159,8 +165,7 @@ private SAXSource readSAXSource(InputStream body) throws IOException { private Source readStAXSource(InputStream body) { try { XMLInputFactory inputFactory = XMLInputFactory.newFactory(); - inputFactory.setProperty( - "javax.xml.stream.isSupportingExternalEntities", this.processExternalEntities); + inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, this.processExternalEntities); XMLStreamReader streamReader = inputFactory.createXMLStreamReader(body); return new StAXSource(streamReader); }
spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTests.java+34 −0 modified@@ -32,9 +32,13 @@ import org.springframework.aop.framework.AdvisedSupport; import org.springframework.aop.framework.AopProxy; import org.springframework.aop.framework.DefaultAopProxyFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpOutputMessage; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.xml.sax.SAXParseException; /** @author Arjen Poutsma */ public class Jaxb2RootElementHttpMessageConverterTests { @@ -95,6 +99,33 @@ public void readXmlType() throws Exception { assertEquals("Invalid result", "Hello World", result.s); } + @Test + public void readXmlRootElementExternalEntityDisabled() throws Exception { + Resource external = new ClassPathResource("external.txt", getClass()); + String content = "<!DOCTYPE root [" + + " <!ELEMENT external ANY >\n" + + " <!ENTITY ext SYSTEM \"" + external.getURI() + "\" >]>" + + " <rootElement><external>&ext;</external></rootElement>"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); + RootElement rootElement = (RootElement) converter.read(RootElement.class, inputMessage); + + assertEquals("", rootElement.external); + } + + @Test + public void readXmlRootElementExternalEntityEnabled() throws Exception { + Resource external = new ClassPathResource("external.txt", getClass()); + String content = "<!DOCTYPE root [" + + " <!ELEMENT external ANY >\n" + + " <!ENTITY ext SYSTEM \"" + external.getURI() + "\" >]>" + + " <rootElement><external>&ext;</external></rootElement>"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); + this.converter.setProcessExternalEntities(true); + RootElement rootElement = (RootElement) converter.read(RootElement.class, inputMessage); + + assertEquals("Foo Bar", rootElement.external); + } + @Test public void writeXmlRootElement() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); @@ -120,6 +151,9 @@ public static class RootElement { private Type type = new Type(); + @XmlElement(required=false) + public String external; + public Type getType() { return this.type; }
1a8629d40825Fix failing test
2 files changed · +2 −3
spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java+1 −1 modified@@ -229,7 +229,7 @@ protected void writeToResult(T t, HttpHeaders headers, Result result) throws IOE */ protected XMLInputFactory createXmlInputFactory() { XMLInputFactory inputFactory = XMLInputFactory.newInstance(); - inputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false); + inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); return inputFactory; }
spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java+1 −2 modified@@ -126,7 +126,7 @@ public void readXmlTypeSet() throws Exception { @Test @SuppressWarnings("unchecked") - public void readXmlRootElementWithExternalEntity() throws Exception { + public void readXmlRootElementExternalEntityDisabled() throws Exception { Resource external = new ClassPathResource("external.txt", getClass()); String content = "<!DOCTYPE root [" + @@ -151,7 +151,6 @@ public void readXmlRootElementExternalEntityEnabled() throws Exception { " <list><rootElement><type s=\"1\"/><external>&ext;</external></rootElement></list>"; MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); - // Now read with Jaxb2CollectionHttpMessageConverter<?> c = new Jaxb2CollectionHttpMessageConverter<Collection<Object>>() { @Override protected XMLInputFactory createXmlInputFactory() {
1c5cab2a4069Add external entity test
1 file changed · +49 −3
spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java+49 −3 modified@@ -28,11 +28,15 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; +import javax.xml.stream.XMLInputFactory; import org.junit.Before; import org.junit.Test; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.http.MockHttpInputMessage; +import org.springframework.http.converter.HttpMessageConverter; /** * Test fixture for {@link Jaxb2CollectionHttpMessageConverter}. @@ -120,6 +124,48 @@ public void readXmlTypeSet() throws Exception { assertTrue("Invalid result", result.contains(new TestType("2"))); } + @Test + @SuppressWarnings("unchecked") + public void readXmlRootElementWithExternalEntity() throws Exception { + + Resource external = new ClassPathResource("external.txt", getClass()); + String content = "<!DOCTYPE root [" + + " <!ELEMENT external ANY >\n" + + " <!ENTITY ext SYSTEM \"" + external.getURI() + "\" >]>" + + " <list><rootElement><type s=\"1\"/><external>&ext;</external></rootElement></list>"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); + + Collection<RootElement> result = converter.read(rootElementListType, null, inputMessage); + assertEquals(1, result.size()); + assertEquals("", result.iterator().next().external); + } + + @Test + @SuppressWarnings("unchecked") + public void readXmlRootElementExternalEntityEnabled() throws Exception { + + Resource external = new ClassPathResource("external.txt", getClass()); + String content = "<!DOCTYPE root [" + + " <!ELEMENT external ANY >\n" + + " <!ENTITY ext SYSTEM \"" + external.getURI() + "\" >]>" + + " <list><rootElement><type s=\"1\"/><external>&ext;</external></rootElement></list>"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); + + // Now read with + Jaxb2CollectionHttpMessageConverter<?> c = new Jaxb2CollectionHttpMessageConverter<Collection<Object>>() { + @Override + protected XMLInputFactory createXmlInputFactory() { + XMLInputFactory inputFactory = XMLInputFactory.newInstance(); + inputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, true); + return inputFactory; + } + }; + + Collection<RootElement> result = c.read(rootElementListType, null, inputMessage); + assertEquals(1, result.size()); + assertEquals("Foo Bar", result.iterator().next().external); + } + @XmlRootElement public static class RootElement { @@ -134,6 +180,9 @@ public RootElement(String s) { @XmlElement public TestType type = new TestType(); + @XmlElement(required=false) + public String external; + @Override public boolean equals(Object o) { if (this == o) { @@ -181,9 +230,6 @@ public boolean equals(Object o) { public int hashCode() { return s.hashCode(); } - - - } }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
12- secunia.com/advisories/57915nvdVendor Advisory
- github.com/advisories/GHSA-8cmm-qj8g-fcp6ghsaADVISORY
- jira.spring.io/browse/SPR-11376nvdVendor Advisory
- nvd.nist.gov/vuln/detail/CVE-2014-0054ghsaADVISORY
- rhn.redhat.com/errata/RHSA-2014-0400.htmlnvdWEB
- www.oracle.com/technetwork/security-advisory/cpuapr2018-3678067.htmlnvdWEB
- github.com/spring-projects/spring-framework/commit/1a8629d40825c073b6863ae2ab748b647b28506aghsaWEB
- github.com/spring-projects/spring-framework/commit/1c5cab2a4069ec3239c531d741aeb07a434f521bghsaWEB
- github.com/spring-projects/spring-framework/commit/edba32b3093703d5e9ed42b5b8ec23ecc1998398ghsaWEB
- github.com/spring-projects/spring-framework/commit/fb0683c066e74e9667d6cd8c5fa01f674c68c3beghsaWEB
- github.com/spring-projects/spring-framework/issues/16003ghsaWEB
- www.securityfocus.com/bid/66148nvd
News mentions
0No linked articles in our index yet.