Spring Boot Request Body (POJO) doesn't handle special characters

Viewed 1078

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:

Request evaluation

What I have tried so far:

  • Set the consumes argument of @PatchMapping to MediaType.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.

1 Answers

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 :)

Related