eclipse-ee4j / jersey

Eclipse Jersey Project - Read our Wiki:
https://github.com/eclipse-ee4j/jersey/wiki
Other
692 stars 353 forks source link

[Security] Unicode normalization can lead to open redirect #5777

Open chmodxxx opened 1 week ago

chmodxxx commented 1 week ago

Description

We are facing the following behaviour , we have the following endpoint that returns a redirect response via :

return Response.seeOther("https://www.google.com☣@example.org"); Normally this should redirect the user to example.org, however it is not the case the redirection goes to www.google.com .

You can use any unicode character that ends with 23 (e.g \u2523), jakarta will do some sort of normalization in the response headers and convert the unicode character to a literal # causing the legitimate domain to be interpreted as fragment.

Problem

This is a big problem when having an Oauth endpoint using jakarta, the normal implementation would be to take redirect_uri from User input and validate the host, and then redirect to the target domain. The problem here is when parsing this redirect uri in java "https://www.malicious.com☣@legitimate.com" the host will be legitimate.com but the redirection via seeOther() will send the user to malicious.com, which will result in an open redirect in oauth flows.

jansupol commented 1 week ago

@chmodxxx This looks serious. Can you specify how exactly you parse the redirect uri? What client connector do you use (default, netty...)? Do you use some custom logic that includes the work with UriBuilder?

jansupol commented 1 week ago

The Location header for return Response.seeOther(URI.create("https://www.google.com\u2523@example.org")).build(); is https://www.google.com%E2%94%A3@example.org and for return Response.seeOther(URI.create("https://www.google.com☣@example.org")).build(); it is https://www.google.com%E2%98%A3@example.org for me.

What container does Jersey run at in your case?

chmodxxx commented 6 days ago

@jansupol Thanks for looking into this, I don't think our custom logic to validate/parse is relevant for this case, I have created an endpoint that has this only logic :

 @Override
    public Response handleRequest() {
            return Response.seeOther("https://www.google.com☣@example.org");
    }

We have this custom Response interface and the logic roughly looks like this :

import jakarta.ws.rs.core.Response.ResponseBuilder;

public interface Response {

    @Value.Parameter
    int statusCode();

    Map<HttpString, String> headers();

    List<Cookie> cookies();

    Optional<Body> body();

    default jakarta.ws.rs.core.Response toJaxrs() {
        checkState(cookies().isEmpty());

        ResponseBuilder builder = jakarta.ws.rs.core.Response.status(statusCode());

        headers().forEach((name, value) -> builder.header(name.toString(), value));

        if (body().isPresent()) {
            builder.entity(body().get());
        }

        return builder.build();
    }

    static Response seeOther(String location) {
        return builder().seeOther(location).build();
    }

    static Builder builder() {
        return new Builder();
    }

    static Builder ok() {
        return builder().statusCode(StatusCodes.OK);
    }

    final class Builder extends ImmutableResponse.Builder {

        public Builder seeOther(String location) {
            statusCode(StatusCodes.SEE_OTHER);
            putHeaders(Headers.LOCATION, location);
            return this;
        }
}
}