Unvalidated client-side URL redirection

Unvalidated client-side URL redirection

Description

A client-side open redirect occurs when a user-controlled value, such as a query parameter, fragment or DOM value, is passed directly to location.assign, replace or href. Allowing an unvalidated external destination can send a user from a trusted site to an attacker's site for phishing or token theft. For example, a manipulated to value can cause automatic navigation after the trusted page loads.

Potential impact

  • An attacker may lure users into entering credentials or MFA codes on a phishing site.
  • An open-redirect chain may help bypass OAuth redirect validation and expose authorization codes or access tokens.
  • A trusted link may lead to malicious downloads and harm trust in the service.
  • Navigation may bypass referrer-based filtering or other destination checks.

Remediation

  • Do not use user input directly as a redirect destination.
  • Map allow-listed keys such as to=home to internal paths, and send unknown values to a default path.
  • Accept only internal destinations. Reject absolute URLs with a scheme or host, and protocol-relative URLs such as //evil.com.
  • If parsing a value with new URL(value, location.origin), check the resulting origin against the current site and require a pathname beginning with /.
  • Apply the same allow-list on the server when it performs the final redirect.

Examples

Before

javascript
// Use the fragment directly as an external redirect destination
function unsafeRedirect() {
  // URL: https://trusted.example/app#to=https://evil.example
  const raw = location.hash.replace(/^#to=/, '');
  if (raw) {
    // A user-controlled absolute URL can navigate outside the site
    top.location = decodeURIComponent(raw);
  }
}

After

javascript
// Map allow-listed keys only to internal redirect paths
const ROUTE_MAP = Object.freeze({
  home: '/home',
  dashboard: '/dashboard',
  help: '/help'
});

function safeRedirect() {
  const params = new URLSearchParams(window.location.search);
  const key = params.get('to');
  const path = Object.hasOwn(ROUTE_MAP, key) ? ROUTE_MAP[key] : undefined;

  if (path) {
    // Navigate only to an internal path, without a supplied scheme or host
    window.location.assign(path);
  } else {
    // Use the default path for unsupported values
    window.location.replace('/home');
  }
}

Explanation:

  • Before: The fragment becomes a navigation destination, including absolute or protocol-relative external URLs. An attacker can place #to= and an external URL in a trusted site's link to redirect the user.
  • After: Input selects only an own key in the route allow-list, which maps to an internal path. Inherited properties such as toString are excluded. External URLs are not available as destinations on this path.

References