CVE Patch Review

GHSA-86J7-9J95-VPQJ: Root-cause Fix for Unsafe Redirect URI Scheme Handling

GHSA-86J7-9J95-VPQJ · Updated 2026-07-08 Root-cause

Summary

The patch addresses stored XSS by rejecting dangerous redirect URI schemes during plugin registration and by guarding client-side navigation sinks against javascript:, data:, and vbscript: URLs. It also centralizes redirect URI validation in core utilities and adds regression tests. The fix materially addresses the root cause for the affected plugins, though legacy plugins intentionally retain a narrower policy than the stricter oauth-provider package.

Analysis

Vulnerability

GHSA-86J7-9J95-VPQJ describes a stored cross-site scripting issue in Better Auth plugins where attacker-controlled redirect_uri values could be registered and later reflected into browser navigation flows. The patch summary states that the affected oidc-provider and mcp plugins previously accepted any string as a redirect URI at registration, including code-execution schemes such as javascript:, data:, and vbscript:. Because these values are stored and later used as redirect targets, the trust boundary failure becomes a stored XSS primitive in the authentication server origin.

The vulnerable pattern appears in both server-side validation and client-side navigation sinks. On the server side, the plugins used permissive schemas such as z.array(z.string()) for redirect_uris. On the client side, several helpers assigned unvalidated values into window.location.href or equivalent redirect paths. That combination means malicious redirect targets could survive registration and later trigger script execution when a browser follows the redirect.

// vulnerable registration validation
redirect_uris: z.array(z.string()),

// vulnerable navigation sink
window.location.href = opts?.callbackURL ?? "/";

Source references: official patch commit, GitHub Security Advisory.

Patch

The patch introduces a shared scheme-validation model in core and applies it to both registration-time validation and browser navigation sinks. The new DANGEROUS_URL_SCHEMES constant and isSafeUrlScheme() helper in @better-auth/core/utils/url reject absolute URLs using javascript:, data:, or vbscript:, while still allowing relative paths and custom application schemes. For stricter OAuth redirect validation, the patch adds a centralized SafeUrlSchema in @better-auth/core/utils/redirect-uri and re-exports it from @better-auth/oauth-provider so all OAuth provider implementations can share one policy.

export const DANGEROUS_URL_SCHEMES = ["javascript:", "data:", "vbscript:"];

export function isSafeUrlScheme(value: string): boolean {
	if (!URL.canParse(value)) {
		// Relative URLs carry no scheme to abuse.
		return true;
	}
	return !DANGEROUS_URL_SCHEMES.includes(new URL(value).protocol);
}

In the affected plugins, redirect_uris now use z.string().refine(isSafeUrlScheme) instead of accepting arbitrary strings. Client-side code in fetch-plugins.ts, one-tap/client.ts, and two-factor/client.ts now checks isSafeUrlScheme() before navigating. The patch also adds tests that explicitly reject javascript:, data:, and vbscript: redirect URIs and verifies that expected safe cases such as HTTPS and loopback HTTP still work.

Notably, the patch intentionally keeps oidc-provider and mcp on a narrower, non-breaking policy: they now block code-execution schemes, but they do not fully adopt the stricter HTTPS-or-loopback requirement that exists in the shared SafeUrlSchema. The commit message and inline comments explicitly frame this as migration-path behavior for older plugins rather than full parity hardening. See the official patch commit.

Review

Pros

  • The patch directly addresses the root trust failure by validating redirect URI schemes at registration time in the affected plugins, preventing persistence of obviously dangerous values.
  • It also hardens browser-side sinks, which is important defense in depth because unsafe navigation can arise from sources other than plugin registration.
  • Validation logic is centralized in core utilities, reducing policy drift. The re-export of SafeUrlSchema from @better-auth/oauth-provider removes duplicated logic and makes future maintenance less error-prone.
  • The tests are relevant and regression-oriented: they cover blocked schemes, mixed-case normalization, relative URLs, custom schemes, HTTPS, and loopback HTTP.
  • The implementation is intentionally narrow where needed: isSafeUrlScheme() blocks code-execution schemes without breaking relative paths or mobile deep links, which matches the stated non-breaking compatibility goal.

Cons

  • The legacy oidc-provider and mcp plugins do not adopt the full SafeUrlSchema policy. They only reject dangerous schemes, so insecure-but-non-XSS redirect targets such as non-loopback plain HTTP remain acceptable in those plugins.
  • The registration-time fix in those plugins uses refine(isSafeUrlScheme) rather than the stricter shared schema, so policy centralization is only partial for the vulnerable code paths.
  • isSafeUrlScheme() treats non-parseable values as safe because they are assumed to be relative URLs. That is reasonable for navigation helpers, but it is intentionally permissive and should not be confused with full redirect URI validation.
  • The patch mitigates the stored XSS vector described in the advisory, but it does not fully normalize all redirect handling semantics across old and new provider implementations.

Verdict

Root-cause.

For the vulnerability described in GHSA-86J7-9J95-VPQJ, the patch fixes the actual exploit mechanism: dangerous URI schemes were accepted, stored, and later used in browser navigation contexts. The update blocks those schemes at the point of registration in the affected plugins and adds sink-side guards where navigation occurs, which closes the stored XSS path rather than merely obscuring symptoms.

The main caveat is scope, not correctness. The older plugins intentionally receive only the non-breaking scheme blacklist instead of the stricter HTTPS-or-loopback redirect policy available through the shared SafeUrlSchema and the newer OAuth provider package. That means the patch is a root-cause fix for this XSS class, but not a complete redirect-security unification across the product line. Engineers maintaining these plugins should still prefer migration toward the shared stricter provider path documented in the official patch.

Sources