Skip to content

Commit 2af0323

Browse files
committed
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
1 parent fdab8fa commit 2af0323

File tree

5 files changed

+56
-6
lines changed

5 files changed

+56
-6
lines changed

spring-web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.http.converter.xml;
1818

19+
import java.nio.charset.Charset;
1920
import java.util.concurrent.ConcurrentHashMap;
2021
import java.util.concurrent.ConcurrentMap;
2122

@@ -24,7 +25,10 @@
2425
import jakarta.xml.bind.Marshaller;
2526
import jakarta.xml.bind.Unmarshaller;
2627

28+
import org.springframework.http.HttpHeaders;
29+
import org.springframework.http.MediaType;
2730
import org.springframework.http.converter.HttpMessageConversionException;
31+
import org.springframework.lang.Nullable;
2832

2933
/**
3034
* Abstract base class for {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters}
@@ -116,4 +120,20 @@ protected final JAXBContext getJaxbContext(Class<?> clazz) {
116120
});
117121
}
118122

123+
/**
124+
* Detect the charset from the given {@link HttpHeaders#getContentType()}.
125+
* @param httpHeaders the current HTTP headers
126+
* @return the charset defined in the content type header, or {@code null} if not found
127+
*/
128+
@Nullable
129+
protected Charset detectCharset(HttpHeaders httpHeaders) {
130+
MediaType contentType = httpHeaders.getContentType();
131+
if (contentType != null && contentType.getCharset() != null) {
132+
return contentType.getCharset();
133+
}
134+
else {
135+
return null;
136+
}
137+
}
138+
119139
}

spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.IOException;
2020
import java.lang.reflect.ParameterizedType;
2121
import java.lang.reflect.Type;
22+
import java.nio.charset.Charset;
2223
import java.util.ArrayList;
2324
import java.util.Collection;
2425
import java.util.LinkedHashSet;
@@ -148,7 +149,10 @@ public T read(Type type, @Nullable Class<?> contextClass, HttpInputMessage input
148149

149150
try {
150151
Unmarshaller unmarshaller = createUnmarshaller(elementClass);
151-
XMLStreamReader streamReader = this.inputFactory.createXMLStreamReader(inputMessage.getBody());
152+
Charset detectedCharset = detectCharset(inputMessage.getHeaders());
153+
XMLStreamReader streamReader = (detectedCharset != null) ?
154+
this.inputFactory.createXMLStreamReader(inputMessage.getBody(), detectedCharset.name()) :
155+
this.inputFactory.createXMLStreamReader(inputMessage.getBody());
152156
int event = moveToFirstChildOfRootElement(streamReader);
153157

154158
while (event != XMLStreamReader.END_DOCUMENT) {

spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.http.converter.xml;
1818

1919
import java.io.StringReader;
20+
import java.nio.charset.Charset;
2021

2122
import javax.xml.parsers.ParserConfigurationException;
2223
import javax.xml.parsers.SAXParser;
@@ -135,7 +136,7 @@ protected boolean supports(Class<?> clazz) {
135136
@Override
136137
protected Object readFromSource(Class<?> clazz, HttpHeaders headers, Source source) throws Exception {
137138
try {
138-
source = processSource(source);
139+
source = processSource(source, detectCharset(headers));
139140
Unmarshaller unmarshaller = createUnmarshaller(clazz);
140141
if (clazz.isAnnotationPresent(XmlRootElement.class)) {
141142
return unmarshaller.unmarshal(source);
@@ -160,9 +161,12 @@ protected Object readFromSource(Class<?> clazz, HttpHeaders headers, Source sour
160161
}
161162
}
162163

163-
protected Source processSource(Source source) {
164+
protected Source processSource(Source source, @Nullable Charset charset) {
164165
if (source instanceof StreamSource streamSource) {
165166
InputSource inputSource = new InputSource(streamSource.getInputStream());
167+
if (charset != null) {
168+
inputSource.setEncoding(charset.name());
169+
}
166170
try {
167171
// By default, Spring will prevent the processing of external entities.
168172
// This is a mitigation against XXE attacks.

spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -34,6 +34,7 @@
3434
import org.springframework.core.ParameterizedTypeReference;
3535
import org.springframework.core.io.ClassPathResource;
3636
import org.springframework.core.io.Resource;
37+
import org.springframework.http.MediaType;
3738
import org.springframework.http.converter.HttpMessageNotReadableException;
3839
import org.springframework.lang.Nullable;
3940
import org.springframework.web.testfixture.http.MockHttpInputMessage;
@@ -204,6 +205,18 @@ void testXmlBomb() {
204205
.withMessageContaining("\"lol9\"");
205206
}
206207

208+
@Test
209+
@SuppressWarnings("unchecked")
210+
public void readXmlRootElementListHeaderCharset() throws Exception {
211+
String content = "<list><rootElement><type s=\"Hellø Wørld\"/></rootElement></list>";
212+
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes(StandardCharsets.ISO_8859_1));
213+
inputMessage.getHeaders().setContentType(MediaType.parseMediaType("application/xml;charset=iso-8859-1"));
214+
List<RootElement> result = (List<RootElement>) converter.read(rootElementListType, null, inputMessage);
215+
216+
assertThat(result).as("Invalid result").hasSize(1);
217+
assertThat(result.get(0).type.s).as("Invalid result").isEqualTo("Hellø Wørld");
218+
}
219+
207220

208221
@XmlRootElement
209222
public static class RootElement {

spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTests.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -180,6 +180,15 @@ void testXmlBomb() throws Exception {
180180
.withMessageContaining("DOCTYPE");
181181
}
182182

183+
@Test
184+
void readXmlRootElementHeaderCharset() throws Exception {
185+
byte[] body = "<rootElement><type s=\"Hellø Wørld\"/></rootElement>".getBytes(StandardCharsets.ISO_8859_1);
186+
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body);
187+
inputMessage.getHeaders().setContentType(MediaType.parseMediaType("application/xml;charset=iso-8859-1"));
188+
RootElement result = (RootElement) converter.read(RootElement.class, inputMessage);
189+
assertThat(result.type.s).as("Invalid result").isEqualTo("Hellø Wørld");
190+
}
191+
183192
@Test
184193
void writeXmlRootElement() throws Exception {
185194
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();

0 commit comments

Comments
 (0)