Unvalidated server-side URL redirects

Unvalidated redirects and forwards (open redirect)

Description

An open redirect occurs when a server uses user input as a redirect destination without adequate validation. An attacker can make a link appear to belong to a legitimate domain, then use a parameter to redirect the visitor to a malicious site. This may support credential phishing or, in a vulnerable OAuth/SSO flow, interception of authorization codes or tokens.

Potential impact

  • Phishing that uses a trusted-looking link to collect credentials
  • Interception of codes or tokens when an authentication flow accepts an attacker-controlled return destination
  • Redirection to malicious downloads or other attack pages
  • Bypass of domain allow-lists or messaging filters that rely on the initial URL

Remediation

  • Restrict internal navigation to predefined local paths. Use a fixed mapping for required external destinations.
  • Select internal paths from an allow-list, and external URLs through predefined keys.
  • Redirect to a safe default such as / when validation fails.
  • Compare normalized values exactly with the allow-list; do not rely on blocking suspicious substrings.
  • Validate inputs to Next.js NextResponse.redirect(new URL(..., request.url)) and App Router's redirect(...) using the same criteria.

Examples

Before

javascript
const express = require("express");
const app = express();

// Before: redirect directly to user input.
app.get("/go", (req, res) => {
  const target = req.query.u; // User-controlled input.
  return res.redirect(target); // No validation (open redirect).
});

app.listen(3000);

After

javascript
const express = require("express");
const app = express();

// After: allowlisted internal paths and keyed external URLs.
const allowedPaths = new Set(["/home", "/dashboard", "/help"]);
const externalMap = new Map([
  ["docs", "https://docs.example.com"],
  ["status", "https://status.example.com"],
]);

app.get("/go", (req, res) => {
  const next = typeof req.query.next === "string" ? req.query.next.trim() : "";
  const siteKey = typeof req.query.site === "string" ? req.query.site.trim() : "";

  // 1) Allow only predefined internal paths.
  if (allowedPaths.has(next)) {
    return res.redirect(next);
  }

  // 2) Select external destinations only through the key-to-URL map.
  const extUrl = externalMap.get(siteKey);
  if (extUrl) {
    return res.redirect(extUrl);
  }

  // 3) Use a safe default when validation fails.
  return res.redirect("/");
});

app.listen(3000);

Explanation:

  • Before: Unvalidated input u becomes the Location header, allowing redirection to an arbitrary domain for phishing.
  • After: Internal destinations must belong to allowedPaths. Query values must be strings, and external destinations must be registered site keys in the Map. Arrays and inherited properties cannot become redirect targets. Invalid values lead to /.

References