remix-run/react-router · critical

Invalid redirect location

Error message

Invalid redirect location

What it means

Before honoring a redirect target, normalizeRedirectLocation validates its protocol; hasInvalidProtocol rejects dangerous schemes (e.g. javascript:) and the function throws 'Invalid redirect location' (lib/router/router.ts:6920). This is a security guard: following or echoing a javascript:/data: Location would let untrusted input become script execution.

Solutions

  1. Whitelist redirect targets: only allow values starting with '/' (no '//') or absolute URLs on your own origin
  2. Return 400 for suspicious or scheme-carrying redirect inputs instead of passing them to redirect()
  3. If proxying upstream responses, validate/sanitize the Location header before returning it
  4. Add tests covering 'javascript:', 'data:', and '//' protocol-relative payloads

Example fix

// before
return redirect(request.url.searchParams.get('redirectTo') ?? '/')
// after
function safeRedirect(to: string | null) {
  if (to && to.startsWith('/') && !to.startsWith('//')) return to
  return '/'
}
return redirect(safeRedirect(request.url.searchParams.get('redirectTo')))
Defensive patterns

Strategy: validation

Validate before calling

function isSafeRedirectTarget(to: string | null | undefined): boolean {
  if (!to) return false;
  if (to.startsWith('/') && !to.startsWith('//')) return true;
  try {
    const url = new URL(to, 'https://example.invalid');
    return url.protocol === 'https:' || url.protocol === 'http:';
  } catch {
    return false;
  }
}
if (!isSafeRedirectTarget(redirectTo)) throw new Response('Bad redirect', { status: 400 });

Type guard

type SafeRedirect = `/${string}`;
function asSafeRedirect(to: string): SafeRedirect | null {
  return to.startsWith('/') && !to.startsWith('//') && !to.includes(':') ? (to as SafeRedirect) : null;
}

Try / catch

try { return redirect(target) } catch (e) { if (e instanceof Error && e.message === 'Invalid redirect location') throw new Response('Bad redirect target', { status: 400 }) throw e }

Prevention

When it happens

Trigger: A loader/action returns redirect(input) where input came from a query param, DB field, or upstream service and equals something like 'javascript:alert(1)'; a proxied backend returns a Location header with a non-http(s) scheme; protocol-relative or malformed URLs that parse to an invalid protocol.

Common situations: Open redirect targets taken from ?redirectTo= or ?next= stored and replayed; user profile fields used as post-login redirects; untrusted upstream APIs behind a proxy whose Location headers are forwarded.

Related errors


AI-assisted analysis of remix-run/react-router@7aea711dd1 (2026-08-18). Data as JSON: /api/errors/8257a93da2dbf452. Report an issue: GitHub.

Appendix: source

Thrown at packages/react-router/lib/router/router.ts:6899

  }
}

function normalizeRedirectLocation(
  location: string,
  currentUrl: URL,
  basename: string,
  historyInstance: History,
): string {
  if (isAbsoluteUrl(location)) {
    // Strip off the protocol+origin for same-origin + same-basename absolute redirects
    let normalizedLocation = location;
    let url = PROTOCOL_RELATIVE_URL_REGEX.test(normalizedLocation)
      ? new URL(
          normalizeProtocolRelativeUrl(normalizedLocation, currentUrl.protocol),
        )
      : new URL(normalizedLocation);
    if (hasInvalidProtocol(url.toString())) {
      throw new Error("Invalid redirect location");
    }
    let isSameBasename = stripBasename(url.pathname, basename) != null;
    if (url.origin === currentUrl.origin && isSameBasename) {
      return removeDoubleSlashes(url.pathname) + url.search + url.hash;
    }
  }

  try {
    let url = historyInstance.createURL(location);
    if (hasInvalidProtocol(url.toString())) {
      throw new Error("Invalid redirect location");
    }
  } catch {}

  return location;
}

// Utility method for creating the Request instances for loaders/actions during

View on GitHub (pinned to 7aea711dd1)