keycloak/keycloak
在 GitHub 查看NullPointerException in CertificateInfoHelper when "keyAlias" parameter is missing in multipart request. SAST
Open
#49,246 创建于 2026年5月22日
area/admin/apihelp wantedkind/bugpriority/lowstatus/auto-bumpstatus/auto-expireteam/core-protocolsteam/core-shared
描述
Before reporting an issue
- I have read and understood the above terms for submitting issues, and I understand that my issue may be closed without action if I do not follow them.
Area
admin/api
Describe the bug
When sending a multipart request with the required keyAlias parameter missing, the uploadForm.getFirst("keyAlias") call returns null, which results in a NullPointerException when invoking .asString(). The criticality is minor, because authorization is required to reach the endpoint.
Log:
2026-05-22 12:42:02,183 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (executor-thread-1) Uncaught server error: java.lang.NullPointerException: Cannot invoke "org.keycloak.http.FormPartValue.asString()" because the return value of "jakarta.ws.rs.core.MultivaluedMap.getFirst(Object)" is null
at org.keycloak.services.util.CertificateInfoHelper.getCertificateFromRequest(CertificateInfoHelper.java:242)
at org.keycloak.services.resources.admin.ClientAttributeCertificateResource.uploadJksCertificate(ClientAttributeCertificateResource.java:166)
at org.keycloak.services.resources.admin.ClientAttributeCertificateResource$quarkusrestinvoker$uploadJksCertificate_8dff09afc57f1e71a1d400026a52fbdbba3d2e83.invoke(Unknown Source)
at org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)
at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:190)
at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
at io.quarkus.vertx.core.runtime.VertxCoreRecorder$15.runWith(VertxCoreRecorder.java:677)
at org.jboss.threads.EnhancedQueueExecutor$Task.doRunWith(EnhancedQueueExecutor.java:2651)
at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2630)
at org.jboss.threads.EnhancedQueueExecutor.runThreadBody(EnhancedQueueExecutor.java:1622)
at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1589)
at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:11)
at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:11)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:1583)
Version
26.6.2
Regression
- The issue is a regression
Expected behavior
The server should validate the multipart form parameters and return an HTTP 400 Bad Request with a meaningful error message (e.g., "keyAlias cannot be null or empty") if the required keyAlias parameter is missing from the request payload.
Actual behavior
See the description
How to Reproduce?
- Start Keycloak in Docker:
docker run -p 127.0.0.1:8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:26.6.2 start-dev
- Generate a test JKS via
keytool:
keytool -genkeypair -alias my-test-alias -keyalg RSA -keysize 2048 -validity 365 -keystore test-keystore.jks -storepass password123 -keypass password123 -dname "CN=Test, O=Keycloak, C=EN"
- Send a multipart POST request to
/admin/realms/master/clients/{client-uuid}/certificates/jwt.credential/upload-certificate. To do this, I used AI to create the following script:
#!/bin/bash
# === CONFIGURATION SETTINGS ===
KC_URL="http://localhost:8080"
REALM="master"
USERNAME="admin"
PASSWORD="admin"
CLIENT_NAME="admin-cli" # Automatically searching for UUID using this name
KEYSTORE_FILE="test-keystore.jks"
# ==============================
echo "[*] Requesting a fresh access token..."
# 1. Automatically fetch the token
TKN=$(curl -s -X POST "${KC_URL}/realms/${REALM}/protocol/openid-connect/token" \
-d "client_id=admin-cli" \
-d "username=${USERNAME}" \
-d "password=${PASSWORD}" \
-d "grant_type=password" | grep -o '"access_token":"[^"]*' | grep -o '[^"]*$')
if [ -z "$TKN" ]; then
echo "[-] Error: Failed to obtain access token."
exit 1
fi
echo "[+] Access token successfully obtained!"
echo "[*] Automatically searching for UUID of client '${CLIENT_NAME}'..."
# 2. Query the API to find the internal client ID
CLIENT_UUID=$(curl -s -H "Authorization: Bearer ${TKN}" \
"${KC_URL}/admin/realms/${REALM}/clients?clientId=${CLIENT_NAME}" | grep -o '"id":"[^"]*' | head -n 1 | grep -o '[^"]*$')
if [ -z "$CLIENT_UUID" ]; then
echo "[-] Error: Could not find UUID for client ${CLIENT_NAME}."
exit 1
fi
echo "[+] Found! Internal Client UUID: ${CLIENT_UUID}"
echo "[*] Sending the vulnerability trigger to the endpoint..."
# 3. Execute the exploit request using the dynamically resolved UUID
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST \
"${KC_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}/certificates/jwt.credential/upload-certificate" \
-H "Authorization: Bearer ${TKN}" \
-F "keystoreFormat=JKS" \
-F "storePassword=password123" \
-F "keyPassword=password123" \
-F "file=@${KEYSTORE_FILE}")
BODY=$(echo "$RESPONSE" | sed '$d')
STATUS=$(echo "$RESPONSE" | tail -n1 | cut -d':' -f2)
echo -e "\n=== RESPONSE RESULT ==="
echo "HTTP Status: $STATUS"
echo "Response Body: $BODY"
if [ "$STATUS" -eq 500 ]; then
echo -e "\n[⚡] SUCCESS: Bug reproduced! The server crashed with 500 Internal Server Error (NullPointerException)."
elif [ "$STATUS" -eq 400 ]; then
echo -e "\n[✓] INFO: The server returned HTTP 400. The vulnerability might already be patched."
else
echo -e "\n[-] Something went wrong. Verify the endpoint or attribute type (jwt.credential)."
fi
The output should be:
$ bash nperepro.sh
[*] Requesting a fresh access token...
[+] Access token successfully obtained!
[*] Automatically searching for UUID of client 'admin-cli'...
[+] Found! Internal Client UUID: b0e4871a-eca9-43d4-8f3d-38b320475c62
[*] Sending the vulnerability trigger to the endpoint...
=== RESPONSE RESULT ===
HTTP Status: 500
Response Body: {"error":"unknown_error","error_description":"For more on this error consult the server log."}
[⚡] SUCCESS: Bug reproduced! The server crashed with 500 Internal Server Error (NullPointerException).
Anything else?
Found by Linux Verification Center with SVACE