diff --git a/jollyday-jackson/src/main/java/de/focus_shift/jollyday/jackson/JacksonXMLMapper.java b/jollyday-jackson/src/main/java/de/focus_shift/jollyday/jackson/JacksonXMLMapper.java index 30602ebbb..7e58784c7 100644 --- a/jollyday-jackson/src/main/java/de/focus_shift/jollyday/jackson/JacksonXMLMapper.java +++ b/jollyday-jackson/src/main/java/de/focus_shift/jollyday/jackson/JacksonXMLMapper.java @@ -2,8 +2,10 @@ import de.focus_shift.jollyday.jackson.mapping.Configuration; import org.jspecify.annotations.NonNull; +import tools.jackson.dataformat.xml.XmlFactory; import tools.jackson.dataformat.xml.XmlMapper; +import javax.xml.stream.XMLInputFactory; import java.io.InputStream; import static tools.jackson.databind.PropertyNamingStrategies.UPPER_CAMEL_CASE; @@ -29,9 +31,23 @@ public class JacksonXMLMapper { private static class JacksonMapperCreator { private @NonNull XmlMapper create() { - return XmlMapper.builder() + return XmlMapper.builder(XmlFactory.builder().xmlInputFactory(createHardenedXmlInputFactory()).build()) .propertyNamingStrategy(UPPER_CAMEL_CASE) .build(); } + + /** + * StAX factory hardened against XXE. DTD processing and external entity resolution are + * disabled, so a {@code } declaration (and any external or parameter entity it + * might carry) is rejected rather than resolved. The protection is configured explicitly here + * instead of relying on the defaults of whichever StAX provider happens to be on the classpath, + * which could change if a consumer swaps or downgrades that provider. + */ + private @NonNull XMLInputFactory createHardenedXmlInputFactory() { + final XMLInputFactory factory = XMLInputFactory.newFactory(); + factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + return factory; + } } } diff --git a/jollyday-jackson/src/test/java/de/focus_shift/jollyday/jackson/JacksonXMLMapperTest.java b/jollyday-jackson/src/test/java/de/focus_shift/jollyday/jackson/JacksonXMLMapperTest.java index 2a1b5d1a1..5d7b3cdd8 100644 --- a/jollyday-jackson/src/test/java/de/focus_shift/jollyday/jackson/JacksonXMLMapperTest.java +++ b/jollyday-jackson/src/test/java/de/focus_shift/jollyday/jackson/JacksonXMLMapperTest.java @@ -1,12 +1,16 @@ package de.focus_shift.jollyday.jackson; import de.focus_shift.jollyday.jackson.mapping.Configuration; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class JacksonXMLMapperTest { @@ -18,4 +22,17 @@ void unmarshalRealResource(String holidayFileName) { final Configuration configuration = sut.unmarshallConfiguration(inputStream); assertThat(configuration.getHolidays()).isNotNull(); } + + @Test + void rejectsXmlWithDoctypeToPreventXxe() { + final JacksonXMLMapper sut = new JacksonXMLMapper(); + final String maliciousXml = + "\n" + + "]>\n" + + ""; + final InputStream inputStream = new ByteArrayInputStream(maliciousXml.getBytes(StandardCharsets.UTF_8)); + + assertThatThrownBy(() -> sut.unmarshallConfiguration(inputStream)) + .isInstanceOf(IllegalStateException.class); + } }