Use Content-Type charset in JAXB message converters
Backport Bot / build (push) Has been cancelled Details
Build and Deploy Snapshot / Build and Deploy Snapshot (push) Has been cancelled Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:false version:17], map[id:ubuntu-latest name:Linux]) (push) Has been cancelled Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:21], map[id:ubuntu-latest name:Linux]) (push) Has been cancelled Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:23], map[id:ubuntu-latest name:Linux]) (push) Has been cancelled Details
Deploy Docs / Dispatch docs deployment (push) Has been cancelled Details
Build and Deploy Snapshot / Verify (push) Has been cancelled Details

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
This commit is contained in:
Brian Clozel 2025-05-19 16:57:57 +02:00
parent fdab8fabd2
commit 2af0323c21
5 changed files with 56 additions and 6 deletions

View File

@ -16,6 +16,7 @@
package org.springframework.http.converter.xml; package org.springframework.http.converter.xml;
import java.nio.charset.Charset;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
@ -24,7 +25,10 @@ import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller; import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller; import jakarta.xml.bind.Unmarshaller;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConversionException; import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.lang.Nullable;
/** /**
* Abstract base class for {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters} * Abstract base class for {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters}
@ -116,4 +120,20 @@ public abstract class AbstractJaxb2HttpMessageConverter<T> 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;
}
}
} }

View File

@ -19,6 +19,7 @@ package org.springframework.http.converter.xml;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@ -148,7 +149,10 @@ public class Jaxb2CollectionHttpMessageConverter<T extends Collection>
try { try {
Unmarshaller unmarshaller = createUnmarshaller(elementClass); 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); int event = moveToFirstChildOfRootElement(streamReader);
while (event != XMLStreamReader.END_DOCUMENT) { while (event != XMLStreamReader.END_DOCUMENT) {

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,6 +17,7 @@
package org.springframework.http.converter.xml; package org.springframework.http.converter.xml;
import java.io.StringReader; import java.io.StringReader;
import java.nio.charset.Charset;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParser;
@ -135,7 +136,7 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
@Override @Override
protected Object readFromSource(Class<?> clazz, HttpHeaders headers, Source source) throws Exception { protected Object readFromSource(Class<?> clazz, HttpHeaders headers, Source source) throws Exception {
try { try {
source = processSource(source); source = processSource(source, detectCharset(headers));
Unmarshaller unmarshaller = createUnmarshaller(clazz); Unmarshaller unmarshaller = createUnmarshaller(clazz);
if (clazz.isAnnotationPresent(XmlRootElement.class)) { if (clazz.isAnnotationPresent(XmlRootElement.class)) {
return unmarshaller.unmarshal(source); 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) { if (source instanceof StreamSource streamSource) {
InputSource inputSource = new InputSource(streamSource.getInputStream()); InputSource inputSource = new InputSource(streamSource.getInputStream());
if (charset != null) {
inputSource.setEncoding(charset.name());
}
try { try {
// By default, Spring will prevent the processing of external entities. // By default, Spring will prevent the processing of external entities.
// This is a mitigation against XXE attacks. // This is a mitigation against XXE attacks.

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.ParameterizedTypeReference;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.web.testfixture.http.MockHttpInputMessage; import org.springframework.web.testfixture.http.MockHttpInputMessage;
@ -204,6 +205,18 @@ class Jaxb2CollectionHttpMessageConverterTests {
.withMessageContaining("\"lol9\""); .withMessageContaining("\"lol9\"");
} }
@Test
@SuppressWarnings("unchecked")
public void readXmlRootElementListHeaderCharset() throws Exception {
String content = "<list><rootElement><type s=\"Hellø Wørld\"/></rootElement></list>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes(StandardCharsets.ISO_8859_1));
inputMessage.getHeaders().setContentType(MediaType.parseMediaType("application/xml;charset=iso-8859-1"));
List<RootElement> result = (List<RootElement>) 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 @XmlRootElement
public static class RootElement { public static class RootElement {

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -180,6 +180,15 @@ class Jaxb2RootElementHttpMessageConverterTests {
.withMessageContaining("DOCTYPE"); .withMessageContaining("DOCTYPE");
} }
@Test
void readXmlRootElementHeaderCharset() throws Exception {
byte[] body = "<rootElement><type s=\"Hellø Wørld\"/></rootElement>".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 @Test
void writeXmlRootElement() throws Exception { void writeXmlRootElement() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();