Skip to content

fix(auth): validate mobile OAuth redirect URI against scheme allowlist#390

Open
Ridanshi wants to merge 1 commit into
Dev-Card:mainfrom
Ridanshi:fix/oauth-mobile-redirect-validation
Open

fix(auth): validate mobile OAuth redirect URI against scheme allowlist#390
Ridanshi wants to merge 1 commit into
Dev-Card:mainfrom
Ridanshi:fix/oauth-mobile-redirect-validation

Conversation

@Ridanshi
Copy link
Copy Markdown
Contributor

@Ridanshi Ridanshi commented May 29, 2026

Closes #185

Problem

The GitHub and Google OAuth initiation endpoints accept a mobile_redirect_uri query parameter and embed it into the OAuth state without validation. During the callback flow, the value is decoded and used as the redirect destination for the user's JWT.

This allows an attacker to supply an arbitrary redirect URI and receive a victim's authentication token after a successful OAuth login flow.

Example:

/auth/github?state=mobile_x&mobile_redirect_uri=https://attacker.com/steal

In this scenario, a victim who completes authentication could be redirected to an attacker-controlled destination containing their JWT.

Root Cause

mobile_redirect_uri was accepted directly from user-controlled input and embedded into the OAuth state. The callback flow later trusted and used the decoded value without validating whether it was a legitimate mobile application redirect target.

Solution

Introduce explicit allowlist validation for mobile redirect URIs.

Allowed schemes:

  • devcard://
  • exp://

The validation is applied in two places for defense in depth:

  1. During OAuth state generation before a redirect URI is embedded.
  2. During callback processing when a redirect URI is extracted from the OAuth state.

If a supplied URI does not match the allowlist, it is ignored and the flow falls back to MOBILE_REDIRECT_URI.

Changes

  • Added isSafeMobileRedirectUri() helper.
  • Added scheme allowlist validation for mobile redirect URIs.
  • Prevented unsafe redirect URIs from being embedded into OAuth state.
  • Revalidated decoded redirect URIs before redirecting users after OAuth completion.
  • Added comprehensive unit test coverage.

Tests

Added 19 unit tests covering:

  • Allowed redirect schemes
  • Disallowed redirect schemes (http, https, javascript, malformed values)
  • OAuth state generation with valid redirect URIs
  • OAuth state generation with invalid redirect URIs
  • Redirect URI extraction from OAuth state
  • Rejection of tampered OAuth state values
  • Malformed state handling
  • Fallback behavior

All existing and new tests pass successfully.

Impact

Low risk.

The change only affects untrusted redirect URIs supplied through mobile_redirect_uri. Existing valid mobile authentication flows continue to function normally, while unsafe redirect destinations are rejected.

Security Benefit

Prevents authentication token leakage through attacker-controlled redirect destinations and ensures OAuth tokens are only delivered to trusted mobile application schemes.

…mbedding in OAuth state

The GitHub and Google OAuth initiation endpoints accepted an arbitrary
mobile_redirect_uri query parameter and embedded it — base64url-encoded —
into the OAuth state string without any validation.  At callback time
getMobileRedirectUri() decoded it and the handler used it verbatim as the
redirect destination for a freshly-issued 30-day JWT:

  return reply.redirect(`${mobileRedirect}#token=${token}`);

An attacker who crafts a link such as

  /auth/github?state=mobile_x&mobile_redirect_uri=https://attacker.com/steal

and convinces a victim to click it receives the victim's JWT via the
Location header fragment.  The existing CSRF cookie check does not prevent
this because the attacker initiates a legitimate flow — they do not forge a
request on behalf of the victim.

Fix:

- Add ALLOWED_MOBILE_SCHEMES constant in authService.ts listing the only
  permitted custom schemes: devcard:// (production) and exp:// (Expo Go
  local development), matching the scheme registered in apps/mobile/app.json
  and the MOBILE_REDIRECT_URI example in .env.example.

- Export isSafeMobileRedirectUri() for use in both buildOAuthState() and
  getMobileRedirectUri() so validation is enforced at embed time and again
  at decode time (defence in depth).

- buildOAuthState() now silently drops an unsafe URI rather than embedding
  it; the callback falls back to the server-configured MOBILE_REDIRECT_URI.

- getMobileRedirectUri() re-validates the decoded URI on the way out so that
  a tampered state string constructed outside buildOAuthState() cannot slip
  a forbidden https:// URI past the initial check.

Add 19 unit tests in authService.test.ts covering:
- isSafeMobileRedirectUri: allowed schemes, forbidden schemes (https, http,
  javascript:, embedded-scheme-in-path), empty string
- buildOAuthState: safe URI embedded correctly, unsafe URI dropped, empty
  mobile URI, uniqueness of nonce across calls
- getMobileRedirectUri: non-mobile state, safe round-trip, tampered state
  with https:// URI rejected, malformed base64 rejected, exp:// round-trip
@Harxhit Harxhit added the gssoc:approved Required label for every approved PR. Gives the base +50 points and enables contribution tracking. label May 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gssoc:approved Required label for every approved PR. Gives the base +50 points and enables contribution tracking.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OAuth state parameter is never validated, enabling login CSRF attacks

2 participants