Skip to main content
interlace
Plugin: browser-securityRules

no-insecure-redirects

Detects open redirects: a navigation or HTTP redirect whose destination an attacker can choose.

CWE: CWE-601
OWASP: Unvalidated Redirects and Forwards

Detects open redirects: a navigation or HTTP redirect whose destination an attacker can choose. This rule is part of eslint-plugin-browser-security.

Quick Summary

AspectDetails
CWE ReferenceCWE-601 (Open Redirect)
SeverityMedium (CVSS 6.1)
Auto-Fix❌ No
CategorySecurity
Best ForWeb applications with redirection logic

Vulnerability and Risk

Vulnerability: An open redirect sends the user to a URL taken from untrusted input — the query string, the fragment, a request parameter — without proving where it points.

Risk: The link starts on your domain, so users and mail filters trust it, and it ends on a phishing or malware page the attacker chose. Redirects are also how OAuth tokens and session-bound links get exfiltrated.

Rule Details

The rule reports when an attacker-steerable value reaches a sink that moves the user, and no guard in front of it proves the destination.

Sinks this rule owns (by the navigation API that receives the URL, never by how the receiver is spelled):

SinkExample
Any write to the current document's Location or its .hrefwindow.location.href = x, top.location = x, window['location'].href = x, a bare global location = x
location.assign(x) / location.replace(x)location.assign(next)
.redirect(x)res.redirect(x), res['redirect'](x)

Attacker-steerable values: the inbound URL (location.search, location.hash, location.href, document.URL), values parsed out of it (new URLSearchParams(location.search).get('next'), new URL(location.href).searchParams), router hooks that return the query (useSearchParams() from Next.js / React Router), and request input (req.query.next, req.params, req.body.url). A TypeScript assertion (get('next')!, get('next') as string) narrows a type and validates nothing, so the value stays steerable. In a template URL the value decides the destination when it leads the URL (`${next}/x`) or lands in its host (`https://${host}/home`, `//${host}`).

Guards that prove the destination (all read from the AST): an anchored, dot-escaped host regexp (/^https:\/\/app\.example\.com\//.test(next)), a relative-path check that also rejects // (next.startsWith('/') && !next.startsWith('//')), and an equality against a parsed origin (new URL(next, location.origin).origin === location.origin). A prefix check such as next.startsWith('https://trusted.com') is not a guard: https://trusted.com.evil.test passes it.

Navigating to the page you are already on (location.assign(location.href)) is a reload, not a redirect, and is not reported.

Rule partition

SinkOwner
a Location write, location.assign/replace, .redirect(x)no-insecure-redirects
window.open(x), router.push/replace(x) on a resolved routerrequire-url-validation
Linking.openURL(x), navigation.navigate(x)no-unvalidated-deeplinks
a credential inside the URL stringno-password-in-url (complementary, not partitioned)

Examples

❌ Incorrect

const next = new URLSearchParams(location.search).get('next');
window.location.href = next;

// A non-null assertion is not validation
window.location.href = new URLSearchParams(location.search).get('next')!;

// The bare global is the same Location
location = new URLSearchParams(location.search).get('next');

// The attacker picks the host
window.location.href = `https://${new URLSearchParams(location.search).get('host')}/home`;

app.get('/login', (req, res) => {
  res.redirect(req.query.returnTo);
});

✅ Correct

const next = new URLSearchParams(location.search).get('next') ?? '/';

// Same-origin relative paths only (rejects `//evil.test`)
location.assign(next.startsWith('/') && !next.startsWith('//') ? next : '/');

// Parsed-origin equality
const target = new URL(next, location.origin);
if (target.origin === location.origin) window.location.href = target.href;

// A fixed host with a steerable path stays on that host
window.location.href = `https://app.example.com/${encodeURIComponent(page)}`;

res.redirect('/dashboard');

Configuration

// eslint.config.mjs
export default [
  {
    rules: {
      'browser-security/no-insecure-redirects': 'error',
    },
  },
];

Options

OptionTypeDefaultDescription
ignoreInTestsbooleantrueSkip this rule in *.test.* / *.spec.* files
allowedDomainsstring[][]Redirect target domains treated as safe

Known False Negatives

A Location reached through a local alias

Why: Proving that w in w.location.assign(x) is a Window needs type information, and matching any .location by name is the name inference this rule refuses.

// ❌ NOT DETECTED
const w = window;
w.location.assign(new URLSearchParams(location.search).get('next'));

Mitigation: Navigate through window / location directly.

Values passed through opaque functions

Why: An unknown function may validate its argument; only a local helper the rule can read to be the identity passes taint through.

// ❌ NOT DETECTED
location.assign(
  normalizeExternally(new URLSearchParams(location.search).get('next')),
);

Mitigation: Keep the validation visible at the redirect.

Further Reading

Use this rule

npm install -D oxlint-plugin-browser-security
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-browser-security"],  "rules": {    "browser-security/no-insecure-redirects": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-browser-security'],    rules: {      'browser-security/no-insecure-redirects': 'error',    },  },});

oxlint-plugin-browser-security re-exports eslint-plugin-browser-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.