# Better Auth has stored XSS in the auth-server origin via javascript: redirect_uri in oidc-provider and mcp

oidc-provider plugin stored unvalidated javascript: redirect_uris; consent page navigating to the returned redirectURI executes attacker JS in the auth-server origin, enabling account takeover.

- Payout: Research
- Severity: high
- Program: Better Auth
- Found by: hillalee
- Disclosed: 2026-07-07
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-86j7-9j95-vpqj
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-86j7-9j95-vpqj
- Tags: area:web domain:web impact:ato kind:research platform:ghsa sev:high tech:oauth technique:incomplete-fix technique:source-review vuln:open-redirect vuln:xss

**Target:** OAuth client registration endpoint and consent page of the oidc-provider/mcp plugins
**Root cause:** Client registration types redirect_uris as z.array(z.string()) with no scheme validation, so a javascript: URI is stored and later returned unchanged in the consent response's redirectURI field; the documented consent flow assigns it to window.location, executing script in the auth-server origin.
**Steps:**
1. Register a client with `POST /oauth2/register` and body containing `redirect_uris: ["javascript:fetch('/api/auth/get-session')//"]` (authenticated by default; unauthenticated if allowDynamicClientRegistration is true).
2. Send the victim to `GET /oauth2/authorize` with the matching `redirect_uri`; exact string equality matches the stored javascript: URI and it is written into the consent verification record.
3. Victim approves consent; handler runs `new URL(value.redirectURI)`, appends the code via `searchParams.set`, and returns it as JSON `redirectURI` (trailing `//` comments out the query).
4. Consent page assigns `res.data.redirectURI` to `window.location.href`, executing the javascript: body in the authorization-server origin.
5. Script calls `/api/auth/get-session` and other session-scoped endpoints to take over the account.
**How found:** Noticed the deprecated plugin's registration schema lacked the SafeUrlSchema scheme check that the sibling @better-auth/oauth-provider package already enforces, then traced the stored redirect_uri through the authorize/consent flow to the documented window.location assignment sink.
**Exploitation:** Attacker plants a malicious client (any authenticated user by default, anyone if dynamic registration is on), socially engineers the victim via the attacker-controlled client name on the consent screen, and on approval the javascript: payload runs in the auth-server origin, reading the session and calling session-scoped endpoints for account takeover.
**Insight:** When a newer sibling package validates a field (SafeUrlSchema rejecting javascript:/data:/vbscript:), diff the deprecated package's equivalent schema — legacy endpoints often shipped before the control existed and never received it.

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

**javascript: redirect_uri payload stored via registration**

```
`redirect_uris` as `z.array(z.string())` with no scheme check, so `POST /oauth2/register` stores a value such as `javascript:fetch('/api/auth/get-session')//`
```

