TidyURLs
Go to Tools
BLOG
The TidyURLsJournal

Practical guides, tutorials, and insights about URLs, SEO, web development, and online marketing. Learn, troubleshoot, and get the most of the web.

Back to the Journal
TidyURLs Journal

How to Detect Open Redirects in a JavaScript Application

Practical insights, clear explanations, and useful ideas for understanding the web.

October 26, 20267 min read5 topics
How to Detect Open Redirects in a JavaScript Application
Explore the details
#JavaScript security#open redirect#OWASP#Express#web security
October 26, 20267 min read

An open redirect occurs when an application sends a visitor to a destination controlled by untrusted input. In a JavaScript application, that input might be a query parameter such as next, returnTo, or redirect. This guide shows how to find redirect flows, reproduce one safely on your own computer, and replace it with allowlist-based validation.

Key Takeaways

  • Trace untrusted input from its source—such as a query parameter—to a redirect sink, such as res.redirect() or window.location.assign().
  • Test suspected issues only in an application you own or are authorized to assess.
  • Prefer an exact allowlist of permitted destinations over trying to block suspicious-looking URLs.
  • Reject missing, repeated, or unexpected parameter values, and use a safe fallback.

What makes a redirect vulnerable?

A redirect is risky when an application accepts a destination from a request and uses it without sufficient validation. For example, a server might read next from the URL and pass it directly to a redirect function. A visitor can then be sent somewhere the application owner did not intend.

The relevant pattern is untrusted source → redirect sink:

  • Sources: query parameters (next, returnTo, url), form fields, cookies, or values stored and later reused.
  • Sinks: server-side redirect APIs such as Express res.redirect(), or client-side navigation APIs such as window.location.assign() and location.href.

A parameter name alone does not prove a vulnerability. Follow the value through the code and establish whether it can influence the destination without an effective allowlist or equivalent restriction.

Find and reproduce the redirect locally

Search the application for redirect calls and likely destination parameters. For example, from the project directory:

rg -n 'redirect\(|location\.(href|assign|replace)|window\.open|next|returnTo|redirect' .

This search is a starting point, not proof of a vulnerability. Review matches in context: a variable may already be validated, or a redirect may use a fixed destination. Also check middleware, helper functions, and client-side routing code.

The following minimal Express example demonstrates the risky pattern. Run it only on your own computer or in an authorized isolated test environment.

const express = require('express');

const app = express();

app.get('/unsafe', (req, res) => {
  // Vulnerable pattern: request input controls the redirect destination.
  res.redirect(req.query.next);
});

app.listen(3000, '127.0.0.1', () => {
  console.log('Local test app listening at http://127.0.0.1:3000');
});

For a local test, install Express in a disposable project with npm install express, save the code as app.js, and start it with node app.js. Then inspect the response headers:

curl -i 'http://127.0.0.1:3000/unsafe?next=https%3A%2F%2Fdestination.invalid%2F'

In this deliberately vulnerable example, the response can include a Location header containing the supplied destination. The .invalid domain is reserved for examples; do not replace it with a real third-party target when testing. The important question is whether an attacker-controlled value reaches the redirect sink—not whether a particular external site loads.

Use an exact allowlist

If the application only needs to send users to a few internal pages, accept only those exact paths. Reject values that are missing, repeated, or not present in the allowlist, and send the user to a fixed fallback.

const express = require('express');

const app = express();
const allowedDestinations = new Set(['/dashboard', '/account']);

app.get('/safe', (req, res) => {
  const requested = req.query.next;
  const destination =
    typeof requested === 'string' && allowedDestinations.has(requested)
      ? requested
      : '/';

  res.redirect(303, destination);
});

app.listen(3000, '127.0.0.1', () => {
  console.log('Local test app listening at http://127.0.0.1:3000');
});

Here, the query parser must produce one string that exactly matches an approved path. An absent value, an array produced by repeated query parameters, an absolute URL, or any other value falls back to /. The 303 status makes the redirect response explicit; the security control is the destination allowlist, not the status code.

Try these requests against the local /safe endpoint:

curl -i 'http://127.0.0.1:3000/safe?next=%2Fdashboard'
curl -i 'http://127.0.0.1:3000/safe?next=https%3A%2F%2Fdestination.invalid%2F'
curl -i 'http://127.0.0.1:3000/safe?next=%2Fdashboard&next=%2Faccount'

The first request should redirect to /dashboard. The other requests should redirect to /, because neither supplies exactly one allowlisted string. Check the Location header to verify the result.

If the application has many destinations, use stable identifiers instead of accepting arbitrary URLs. For example, map a value such as settings to a hard-coded path such as /account. This keeps the user’s choice separate from the actual redirect destination.

Validation edge cases

  • Repeated parameters: A request can include next more than once. Depending on the framework and query parser, the result may be an array or another non-string value. Reject unexpected types rather than coercing them.
  • Protocol-relative URLs: Values beginning with // can be interpreted as pointing to another host by browsers. Do not treat a leading slash as sufficient validation; compare against exact permitted paths.
  • Backslashes and encoded characters: Browsers and URL parsers can interpret unusual separators or encoded characters differently. Avoid hand-written checks that only look for http, //, or a leading slash. An exact allowlist avoids needing to normalize arbitrary input.
  • Decoding: Query parsers normally decode query values. Avoid repeatedly decoding the value in an attempt to find a “clean” URL; different layers may interpret encoded input differently.
  • External destinations: If external redirects are a real product requirement, maintain an explicit allowlist of destinations and parse candidates with a standards-based URL parser. Check the complete hostname, scheme, and any permitted port. A check such as hostname.endsWith('trusted.example') can accept a different hostname such as not-trusted.example if implemented carelessly; substring checks are not a safe substitute for exact rules.
  • Client-side navigation: The same data-flow review applies to window.location and other navigation sinks. Validate the destination against the same kind of fixed mapping before assigning it.

Confirm the fix

Repeat the same local tests against the protected route. Verify that:

  1. Each approved value produces only its intended Location header.
  2. An absolute URL, a protocol-relative value, a missing value, a repeated parameter, and an unknown path all use the fixed fallback or are rejected.
  3. No other route or client-side code path redirects using the same untrusted parameter without validation.
  4. The allowed destinations are appropriate for the current user and application flow; destination validation does not replace authorization checks.

OWASP’s guidance on unvalidated redirects recommends avoiding redirects based directly on untrusted input and using a safe, controlled destination strategy. An allowlist helps constrain where the application can send users, but it does not authorize access to the destination itself.

Common Mistakes

  • Blocking only strings that contain http: This misses other URL forms and can behave differently after parsing or decoding.
  • Allowing any path that starts with /: A prefix check does not reliably distinguish a local path from browser-interpreted URL forms.
  • Checking only the hostname with a substring: Hostnames can contain trusted-looking text without being the trusted host.
  • Validating in the browser but not on the server: Client-side checks can be bypassed. Enforce the redirect policy at the server-side redirect point as well.
  • Treating a successful redirect as proof of a bug: Confirm that the destination is actually controlled by untrusted input and not constrained elsewhere.
  • Testing real services unnecessarily: Use a local application and reserved example domains; do not probe or redirect users to systems without authorization.

Best Practices

  • Prefer fixed destinations or a mapping from short identifiers to approved paths.
  • Validate at the point where the redirect is issued, even if earlier layers also validate input.
  • Reject unexpected types and values; do not silently coerce them into strings.
  • Use a safe fixed fallback when the requested destination is invalid.
  • Include redirect behavior in tests for missing, repeated, encoded, and unrecognized parameters.
  • Review both server-side and client-side navigation sinks during code review.

Conclusion

To detect a JavaScript open redirect vulnerability, trace destination input to each redirect sink and reproduce the behavior in an authorized local environment. The simplest dependable fix for a limited set of destinations is an exact allowlist with a fixed fallback—not a blacklist of suspicious URL strings.

References