From 2af0323c21375da5c3de97e62e25f4de1cdd3b8e Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 19 May 2025 16:57:57 +0200 Subject: [PATCH] Use Content-Type charset in JAXB message converters Prior to this commit, the JAXB message converters would only rely on the encoding declaration inside the XML document for reading the document. This would then use the default UTF-8 encoding, even if the HTTP message has the `"application/xml;charset=iso-8859-1"` Content-Type. This commit ensures that both `Jaxb2CollectionHttpMessageConverter` and `Jaxb2RootElementHttpMessageConverter` use the encoding declared in the HTTP Content-Type, if present. Fixes gh-34745 --- .../AbstractJaxb2HttpMessageConverter.java | 20 +++++++++++++++++++ .../Jaxb2CollectionHttpMessageConverter.java | 6 +++++- .../Jaxb2RootElementHttpMessageConverter.java | 10 +++++++--- ...b2CollectionHttpMessageConverterTests.java | 15 +++++++++++++- ...2RootElementHttpMessageConverterTests.java | 11 +++++++++- 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java index 11890fa419b..da74365382d 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java @@ -16,6 +16,7 @@ package org.springframework.http.converter.xml; +import java.nio.charset.Charset; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -24,7 +25,10 @@ import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Marshaller; import jakarta.xml.bind.Unmarshaller; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.lang.Nullable; /** * Abstract base class for {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters} @@ -116,4 +120,20 @@ public abstract class AbstractJaxb2HttpMessageConverter extends AbstractXmlHt }); } + /** + * Detect the charset from the given {@link HttpHeaders#getContentType()}. + * @param httpHeaders the current HTTP headers + * @return the charset defined in the content type header, or {@code null} if not found + */ + @Nullable + protected Charset detectCharset(HttpHeaders httpHeaders) { + MediaType contentType = httpHeaders.getContentType(); + if (contentType != null && contentType.getCharset() != null) { + return contentType.getCharset(); + } + else { + return null; + } + } + } diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java index 5730286bafd..d494fcc93af 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java @@ -19,6 +19,7 @@ package org.springframework.http.converter.xml; import java.io.IOException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashSet; @@ -148,7 +149,10 @@ public class Jaxb2CollectionHttpMessageConverter try { Unmarshaller unmarshaller = createUnmarshaller(elementClass); - XMLStreamReader streamReader = this.inputFactory.createXMLStreamReader(inputMessage.getBody()); + Charset detectedCharset = detectCharset(inputMessage.getHeaders()); + XMLStreamReader streamReader = (detectedCharset != null) ? + this.inputFactory.createXMLStreamReader(inputMessage.getBody(), detectedCharset.name()) : + this.inputFactory.createXMLStreamReader(inputMessage.getBody()); int event = moveToFirstChildOfRootElement(streamReader); while (event != XMLStreamReader.END_DOCUMENT) { diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java index 8cc89318184..5417b28500c 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -17,6 +17,7 @@ package org.springframework.http.converter.xml; import java.io.StringReader; +import java.nio.charset.Charset; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; @@ -135,7 +136,7 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa @Override protected Object readFromSource(Class clazz, HttpHeaders headers, Source source) throws Exception { try { - source = processSource(source); + source = processSource(source, detectCharset(headers)); Unmarshaller unmarshaller = createUnmarshaller(clazz); if (clazz.isAnnotationPresent(XmlRootElement.class)) { return unmarshaller.unmarshal(source); @@ -160,9 +161,12 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa } } - protected Source processSource(Source source) { + protected Source processSource(Source source, @Nullable Charset charset) { if (source instanceof StreamSource streamSource) { InputSource inputSource = new InputSource(streamSource.getInputStream()); + if (charset != null) { + inputSource.setEncoding(charset.name()); + } try { // By default, Spring will prevent the processing of external entities. // This is a mitigation against XXE attacks. diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java index 19c277a9b7d..be5b42854b1 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.lang.Nullable; import org.springframework.web.testfixture.http.MockHttpInputMessage; @@ -204,6 +205,18 @@ class Jaxb2CollectionHttpMessageConverterTests { .withMessageContaining("\"lol9\""); } + @Test + @SuppressWarnings("unchecked") + public void readXmlRootElementListHeaderCharset() throws Exception { + String content = ""; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes(StandardCharsets.ISO_8859_1)); + inputMessage.getHeaders().setContentType(MediaType.parseMediaType("application/xml;charset=iso-8859-1")); + List result = (List) converter.read(rootElementListType, null, inputMessage); + + assertThat(result).as("Invalid result").hasSize(1); + assertThat(result.get(0).type.s).as("Invalid result").isEqualTo("Hellø Wørld"); + } + @XmlRootElement public static class RootElement { diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTests.java index 6c839401533..054f84b7fb2 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -180,6 +180,15 @@ class Jaxb2RootElementHttpMessageConverterTests { .withMessageContaining("DOCTYPE"); } + @Test + void readXmlRootElementHeaderCharset() throws Exception { + byte[] body = "".getBytes(StandardCharsets.ISO_8859_1); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); + inputMessage.getHeaders().setContentType(MediaType.parseMediaType("application/xml;charset=iso-8859-1")); + RootElement result = (RootElement) converter.read(RootElement.class, inputMessage); + assertThat(result.type.s).as("Invalid result").isEqualTo("Hellø Wørld"); + } + @Test void writeXmlRootElement() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();