Issue
I have a Spring Boot with REST API:
@Operation(summary = "Update your user")
@PatchMapping(consumes = {MediaType.APPLICATION_JSON_VALUE})
@PreAuthorize("hasAuthority('MANAGE_MY_ACCOUNT')")
@ApiResponse(responseCode = "204", description = "Successfully updated user")
@ApiResponse(responseCode = "400", description = "Cannot update user",
content = @Content(schema = @Schema(implementation = ApiErrorResponse.class)))
@ApiResponse(responseCode = "404", description = "Unknown User Id",
content = @Content(schema = @Schema(implementation = ApiErrorResponse.class)))
ResponseEntity<Void> updateUser(@RequestBody UpdateUserRequest request);
Which accepts UpdateUserRequest
as a POJO in the request body.
@Getter
@NoArgsConstructor
public class UpdateUserRequest {
@Length(max = 100)
private String name;
@Length(max = 100)
private String givenName;
@Length(max = 100)
private String familyName;
}
When I invoke the API with following request:
{
"name": "Rafał Laskowski",
"givenName": "Rafał",
"familyName": "Laskowski"
}
The name
and givenName
are not encoded properly. What I get is RafaÅ
. When debugging the @RestController
i noticed that these names are incorrectly transformed into a byte array which looks like this:
What I have tried so far:
- Set the
consumes
argument of@PatchMapping
toMediaType.APPLICATION_JSON_VALUE
- The above as a string adding
;charset=UTF-8
- Setting JVM -Dfile.encoding=UTF-8
- Setting -Dsun.jnu.encoding=UTF-8
- Setting
sping.mandatory-file-encoding: UTF-8
in application.yml - Setting
server.servlet.encoding.charset: UTF-8
in application.yml log.info(System.getProperty("file.encoding"));
returned UTF-8
I thought it might be the Jackson's Object Mapper configuration but I read it's not the case as JSON only accepts UTF-x encodings.
I have no idea why the character ł
is not properly read from the request.
Solution
Looks like the problem is within security of our application. We've implemented XSS security with following class:
public class XSSRequestWrapper extends HttpServletRequestWrapper {
private final HttpServletRequest request;
private final ResettableServletInputStream servletStream;
private byte[] rawData;
public XSSRequestWrapper(HttpServletRequest request) {
super(request);
this.request = request;
this.servletStream = new ResettableServletInputStream();
}
public static String stripXSS(String value) {
if (value == null) {
return null;
}
value = ESAPI.encoder()
.canonicalize(value)
.replaceAll("\0", "");
return Jsoup.clean(value, Whitelist.none());
}
public void resetInputStream(byte[] newRawData) {
rawData = newRawData;
servletStream.stream = new ByteArrayInputStream(newRawData);
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), Charsets.UTF_8);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return servletStream;
}
@Override
public BufferedReader getReader() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), Charsets.UTF_8);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return new BufferedReader(new InputStreamReader(servletStream));
}
@Override
public String[] getParameterValues(String parameter) {
String[] values = super.getParameterValues(parameter);
if (values == null) {
return null;
}
int count = values.length;
String[] encodedValues = new String[count];
for (int i = 0; i < count; i++) {
encodedValues[i] = stripXSS(values[i]);
}
return encodedValues;
}
@Override
public String getParameter(String parameter) {
String value = super.getParameter(parameter);
return stripXSS(value);
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
return stripXSS(value);
}
@Override
public Enumeration<String> getHeaders(String name) {
List<String> result = new ArrayList<>();
Enumeration<String> headers = super.getHeaders(name);
while (headers.hasMoreElements()) {
String header = headers.nextElement();
String[] tokens = header.split(",");
for (String token : tokens) {
result.add(stripXSS(token));
}
}
return Collections.enumeration(result);
}
private class ResettableServletInputStream extends ServletInputStream {
private InputStream stream;
@Override
public int read() throws IOException {
return stream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
}
}
What solved the issue, was actually changing how getInputStream()
method works.
Changed:
@Override
public ServletInputStream getInputStream() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), Charsets.UTF_8);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return servletStream;
}
to
@Override
public ServletInputStream getInputStream() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), Charsets.ISO_8859_1);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return servletStream;
}
It solved the problem. Disclaimer: I don't know why :)
Answered By - Fenio