From 1ad7c7e56864635c24b62617dfff0c305e7835ae Mon Sep 17 00:00:00 2001 From: Gautam Kumar Date: Mon, 22 Jun 2026 10:45:08 +0530 Subject: [PATCH 1/2] fix: return 400 instead of 500 for malformed Content-Type headers on token endpoint Malformed Content-Type headers (e.g. 'invalid/@@##', empty string, XSS payloads) sent to the OAuth2 token endpoint cause RESTEasy's MediaTypeHeaderDelegate to throw an IllegalArgumentException. The KeycloakErrorHandler was catching this but returning HTTP 500 with 'unknown_error'. Per RFC 6749 section 5.2, the token endpoint must return HTTP 400 with an 'invalid_request' error for malformed requests. Closes #49964 Signed-off-by: Gautam Kumar --- .../services/error/KeycloakErrorHandler.java | 8 ++ .../tests/error/MalformedContentTypeTest.java | 85 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 tests/base/src/test/java/org/keycloak/tests/error/MalformedContentTypeTest.java diff --git a/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java b/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java index 79fea1310cd7..aaee8e93aa8c 100644 --- a/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java +++ b/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java @@ -132,6 +132,10 @@ private static Response.Status getResponseStatus(Throwable throwable) { return Response.Status.CONFLICT; } + if (throwable instanceof IllegalArgumentException) { + return Response.Status.BAD_REQUEST; + } + return Response.Status.INTERNAL_SERVER_ERROR; } @@ -146,6 +150,10 @@ private static String getErrorCode(Throwable throwable) { return "conflict"; } + if (throwable instanceof IllegalArgumentException) { + return OAuthErrorException.INVALID_REQUEST; + } + if (throwable instanceof WebApplicationException && throwable.getMessage() != null) { return throwable.getMessage(); } diff --git a/tests/base/src/test/java/org/keycloak/tests/error/MalformedContentTypeTest.java b/tests/base/src/test/java/org/keycloak/tests/error/MalformedContentTypeTest.java new file mode 100644 index 000000000000..44358af0f9bd --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/error/MalformedContentTypeTest.java @@ -0,0 +1,85 @@ +package org.keycloak.tests.error; + +import java.io.IOException; +import java.net.URI; + +import org.keycloak.OAuthErrorException; +import org.keycloak.common.util.KeycloakUriBuilder; +import org.keycloak.representations.idm.OAuth2ErrorRepresentation; +import org.keycloak.testframework.annotations.InjectHttpClient; +import org.keycloak.testframework.annotations.InjectKeycloakUrls; +import org.keycloak.testframework.annotations.InjectRealm; +import org.keycloak.testframework.annotations.KeycloakIntegrationTest; +import org.keycloak.testframework.realm.ManagedRealm; +import org.keycloak.testframework.server.KeycloakUrls; +import org.keycloak.util.JsonSerialization; + +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@KeycloakIntegrationTest +class MalformedContentTypeTest { + + @InjectKeycloakUrls + KeycloakUrls keycloakUrls; + + @InjectHttpClient + HttpClient httpClient; + + @InjectRealm(attachTo = "master") + ManagedRealm masterRealm; + + @Test + void malformedContentTypeOnTokenEndpoint() throws IOException { + HttpPost post = new HttpPost(tokenUri()); + post.setHeader("Content-Type", "invalid/@@##"); + post.setEntity(new StringEntity("grant_type=password&client_id=admin-cli&username=admin&password=admin")); + + HttpResponse response = httpClient.execute(post); + assertThat("status code should be 400", response.getStatusLine().getStatusCode(), is(400)); + + OAuth2ErrorRepresentation error = JsonSerialization.readValue( + response.getEntity().getContent(), OAuth2ErrorRepresentation.class); + assertThat("error code", error.getError(), is(OAuthErrorException.INVALID_REQUEST)); + } + + @Test + void emptyContentTypeOnTokenEndpoint() throws IOException { + HttpPost post = new HttpPost(tokenUri()); + post.setHeader("Content-Type", ""); + post.setEntity(new StringEntity("grant_type=password&client_id=admin-cli&username=admin&password=admin")); + + HttpResponse response = httpClient.execute(post); + assertThat("status code should be 400", response.getStatusLine().getStatusCode(), is(400)); + + OAuth2ErrorRepresentation error = JsonSerialization.readValue( + response.getEntity().getContent(), OAuth2ErrorRepresentation.class); + assertThat("error code", error.getError(), is(OAuthErrorException.INVALID_REQUEST)); + } + + @Test + void xssPayloadContentTypeOnTokenEndpoint() throws IOException { + HttpPost post = new HttpPost(tokenUri()); + post.setHeader("Content-Type", "\"alert(1)"); + post.setEntity(new StringEntity("grant_type=password&client_id=admin-cli&username=admin&password=admin")); + + HttpResponse response = httpClient.execute(post); + assertThat("status code should be 400", response.getStatusLine().getStatusCode(), is(400)); + + OAuth2ErrorRepresentation error = JsonSerialization.readValue( + response.getEntity().getContent(), OAuth2ErrorRepresentation.class); + assertThat("error code", error.getError(), is(OAuthErrorException.INVALID_REQUEST)); + } + + private URI tokenUri() { + return KeycloakUriBuilder.fromUri(keycloakUrls.getMasterRealm()) + .path("protocol/openid-connect/token") + .build(); + } +} From 256733289e3d1c063242cb1f963470195a3e9455 Mon Sep 17 00:00:00 2001 From: Gautam Kumar Date: Mon, 22 Jun 2026 15:48:36 +0530 Subject: [PATCH 2/2] fix: narrow IllegalArgumentException to 400 only for media type parsing errors Signed-off-by: Gautam Kumar --- .../services/error/KeycloakErrorHandler.java | 24 +++++++++++++++++-- .../tests/error/MalformedContentTypeTest.java | 14 +++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java b/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java index aaee8e93aa8c..9e5449cb8a6a 100644 --- a/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java +++ b/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java @@ -132,7 +132,7 @@ private static Response.Status getResponseStatus(Throwable throwable) { return Response.Status.CONFLICT; } - if (throwable instanceof IllegalArgumentException) { + if (throwable instanceof IllegalArgumentException && isMediaTypeParsingError(throwable)) { return Response.Status.BAD_REQUEST; } @@ -150,7 +150,7 @@ private static String getErrorCode(Throwable throwable) { return "conflict"; } - if (throwable instanceof IllegalArgumentException) { + if (throwable instanceof IllegalArgumentException && isMediaTypeParsingError(throwable)) { return OAuthErrorException.INVALID_REQUEST; } @@ -161,6 +161,26 @@ private static String getErrorCode(Throwable throwable) { return "unknown_error"; } + private static boolean isMediaTypeParsingError(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + String msg = current.getMessage(); + if (msg != null) { + String lower = msg.toLowerCase(Locale.ROOT); + if (lower.contains("media type") || lower.contains("content-type") || + lower.contains("could not find") || lower.contains("invalid")) { + return true; + } + } + Class cls = current.getClass(); + if (cls.getName().contains("MediaType") || cls.getName().contains("BadRequest")) { + return true; + } + current = current.getCause(); + } + return false; + } + private static RealmModel resolveRealm(KeycloakSession session) { String path = session.getContext().getUri().getPath(); Matcher m = realmNamePattern.matcher(path); diff --git a/tests/base/src/test/java/org/keycloak/tests/error/MalformedContentTypeTest.java b/tests/base/src/test/java/org/keycloak/tests/error/MalformedContentTypeTest.java index 44358af0f9bd..c28dbc128417 100644 --- a/tests/base/src/test/java/org/keycloak/tests/error/MalformedContentTypeTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/error/MalformedContentTypeTest.java @@ -77,6 +77,20 @@ void xssPayloadContentTypeOnTokenEndpoint() throws IOException { assertThat("error code", error.getError(), is(OAuthErrorException.INVALID_REQUEST)); } + @Test + void xssScriptPayloadContentTypeOnTokenEndpoint() throws IOException { + HttpPost post = new HttpPost(tokenUri()); + post.setHeader("Content-Type", ""); + post.setEntity(new StringEntity("grant_type=password&client_id=admin-cli&username=admin&password=admin")); + + HttpResponse response = httpClient.execute(post); + assertThat("status code should be 400", response.getStatusLine().getStatusCode(), is(400)); + + OAuth2ErrorRepresentation error = JsonSerialization.readValue( + response.getEntity().getContent(), OAuth2ErrorRepresentation.class); + assertThat("error code", error.getError(), is(OAuthErrorException.INVALID_REQUEST)); + } + private URI tokenUri() { return KeycloakUriBuilder.fromUri(keycloakUrls.getMasterRealm()) .path("protocol/openid-connect/token")