Client-side JavaScript reads location.href, location.hash, or URL parameters and assigns them directly to window.location or document.location.
TL;DR
window.locationwindow.location, document.location.href, location.replace(), location.assign()location.search, location.hash (never logged), document.referrer, postMessage, window.namenew URL() constructor to extract hostname before any navigation assignmentDOM-based open redirect is a client-side variant of CWE-601 where the redirect logic executes entirely in the browser, driven by JavaScript reading user-controlled data from the DOM. Unlike server-side open redirect — which emits a 30x HTTP response — DOM-based redirect produces no observable HTTP response change. The browser navigates to the attacker-controlled URL directly from JavaScript without a server round-trip.
This variant falls under OWASP A01:2021 (Broken Access Control) and maps to DOM-based XSS precursor patterns in terms of data flow: source → taint propagation → sink. The OWASP Testing Guide covers it as WSTG-CLNT-004. Automated vulnerability scanners that do not execute JavaScript will consistently miss this class — coverage requires headless browser instrumentation or manual review of client-side code.
The attack surface is any JavaScript that reads from a URL-related source (query string, hash fragment, referrer, postMessage, window.name) and writes to a navigation sink without validating the destination host.
The taint flow structure mirrors server-side open redirect but operates in the browser's JavaScript engine:
The hash-based variant is particularly dangerous because the URL fragment is never sent to the server — it exists only in the browser and in client-side logs. Server-side detection, logging, and WAF rules cannot observe it.
// VULNERABLE — hash-based DOM redirect
// URL: https://trusted-app.com/dashboard#https://evil.com/capture
const hashValue = window.location.hash.slice(1); // "https://evil.com/capture"
if (hashValue) {
window.location = hashValue; // Open redirect — no validation
}// VULNERABLE — query parameter DOM redirect
const params = new URLSearchParams(window.location.search);
const next = params.get("next");
if (next) {
window.location.href = next; // No host check — direct navigation
}// VULNERABLE — postMessage-based redirect
window.addEventListener("message", (event) => {
// Missing: if (event.origin !== "https://trusted-origin.com") return;
const data = JSON.parse(event.data);
if (data.redirect) {
window.location.replace(data.redirect); // Attacker sends {redirect: "https://evil.com"}
}
});| Source | Sink | Bypass Notes | Detectability |
|---|---|---|---|
location.search (?next=) | window.location | Same as server-side bypass payloads | Easy (scanner) |
location.hash (#url) | location.href | Fragment never sent to server — invisible in logs | Hard |
document.referrer | location.replace() | Attacker controls Referer by navigating from their page | Medium |
window.name | location.assign() | Persists across navigations — set on attacker's page before redirecting | Hard |
postMessage data | window.location | Requires * wildcard or wrong origin check | Medium |
localStorage | document.location | Requires prior subdomain XSS to write the value | Hard |
webpack-dev-server included a development error overlay that read redirect destinations from URL parameters without validation. An attacker could craft a URL pointing to the development server (running on localhost:8080 or a CI/CD environment) that triggered a DOM-based redirect to a phishing page. This affected only development environments but targeted developers — a valuable credential-theft attack surface given developers' access to production secrets.
// Simplified representation of the webpack-dev-server error overlay vulnerability
// Reads redirect destination from URL parameter without WHATWG validation
const overlayParams = new URLSearchParams(window.location.search);
const redirectTarget = overlayParams.get("redirect");
if (redirectTarget) {
window.location.href = redirectTarget; // Vulnerable line
}CVE-2024-43788 — webpack-dev-server (CVSS 6.1) webpack-dev-server's development error overlay contained DOM-based open redirect via unvalidated URL parameters. Any development environment serving webpack-dev-server was vulnerable to developer-targeted phishing attacks. Fixed in webpack-dev-server 5.1.0 by validating redirect destinations against an allowlist.
Hash Fragment Redirect Pattern (Widespread)
Single-page applications that use location.hash for post-authentication redirect (/login#/intended-path) routinely contain DOM-based open redirects when the hash is not validated before being passed to the router or window.location. React and Vue applications that handle external redirects by checking if (next.startsWith("/")) are typically vulnerable to the //evil.com bypass — since //evil.com starts with /, the check passes.
postMessage Redirect in Embedded Widgets (Multiple HackerOne Reports)
Embedded chat widgets, payment iframes, and third-party integrations frequently use postMessage for cross-frame communication. When the message handler navigates based on message data without origin validation, any page that embeds the widget can trigger a redirect on the parent page. Bounties in the $500-$2,000 range for this pattern on major platforms.
window.location, document.location, location.href, location.hash, location.replace, location.assign, window.open.REDIRECT_CANARY) into each URL parameter and fragment. Use Burp DOM Invader to trace whether the canary reaches a navigation sink.https://target.com/page#REDIRECT_CANARY and check whether the browser navigates or whether window.location.hash is assigned to a navigation sink.window.opener.postMessage({redirect: "https://canary.example.com"}, "*") and check for navigation.# Burp Suite DOM Invader — inject canary string, trace to navigation sinks
# Enable DOM Invader via Burp's browser, navigate the application
# semgrep — static analysis for JavaScript navigation sinks
semgrep --config=auto --lang=javascript \
--pattern='window.location = $VAR' \
--pattern='location.href = $VAR' \
src/
# dalfox v3+ with DOM redirect mode
dalfox url "https://target.com/page" --open-redirect --mining-dom
# nuclei DOM-based redirect templates
nuclei -u https://target.com -t javascript/ -tags redirectThe key challenge for automated detection: the vulnerability requires JavaScript execution. Static analysis can find potential source-to-sink paths; confirming exploitability requires headless browser navigation with network monitoring.
BreachVex detects DOM-based redirects by executing JavaScript in an instrumented headless browser and monitoring navigation events. A confirmed finding requires the browser to actually navigate to the canary URL, not just string matching in source code.
// GOOD — validate using WHATWG URL constructor before any navigation
const ALLOWED_HOSTS = new Set(["app.example.com", "dashboard.example.com"]);
function safeNavigate(url) {
if (!url) return;
// Allow relative paths (single slash prefix, not protocol-relative)
if (url.startsWith("/") && !url.startsWith("//")) {
window.location.href = url;
return;
}
try {
const parsed = new URL(url); // WHATWG — same parser as browser
if (ALLOWED_HOSTS.has(parsed.hostname)) {
window.location.href = url;
} else {
window.location.href = "/dashboard"; // Safe fallback
}
} catch {
window.location.href = "/dashboard"; // Invalid URL — fallback
}
}
// Usage — replaces window.location = next
const next = new URLSearchParams(window.location.search).get("next");
safeNavigate(next);// GOOD — use hash only for same-origin SPA routing
const hash = window.location.hash.slice(1);
if (hash && hash.startsWith("/")) {
// Internal SPA route — safe
router.push(hash);
} else if (hash) {
// External URL in hash — reject
console.warn("Blocked external hash redirect:", hash);
}// BAD — accepts messages from any origin
window.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
window.location.href = data.redirect; // Vulnerable
});
// GOOD — validate origin before processing
const TRUSTED_ORIGINS = new Set(["https://partner.example.com"]);
window.addEventListener("message", (event) => {
if (!TRUSTED_ORIGINS.has(event.origin)) {
return; // Ignore messages from untrusted origins
}
const data = JSON.parse(event.data);
if (data.redirect) {
safeNavigate(data.redirect); // Use the validated safeNavigate function
}
});React Router v6's useNavigate() hook prevents external URL navigation by design — it only handles same-origin routes. However, code that uses window.location.href directly for external redirects (a common pattern for post-auth flows) bypasses React Router's safety guarantees entirely. Search for direct window.location assignments in your React codebase.
A DOM-based open redirect occurs entirely in the browser: JavaScript reads a URL from a controllable source (window.location.search, location.hash, document.referrer, postMessage data) and assigns it to a navigation sink (window.location, document.location.href, location.replace(), location.assign()) without host validation. No server-side 30x response is involved — the redirect is driven by client-side script execution.
Navigation sinks that trigger a redirect: window.location = url; document.location = url; window.location.href = url; window.location.replace(url); window.location.assign(url); location = url; window.open(url). Meta refresh is a near-equivalent: document.write('<meta http-equiv="refresh" content="0;url=' + url + '">''). All of these cause the browser to navigate to the supplied URL.
Attacker-controllable sources: window.location.search (query string), window.location.hash (fragment, never sent to server), document.referrer (referrer header), window.name (persists across pages), localStorage and sessionStorage (if attacker can write), postMessage data (cross-origin messaging), document.cookie (if attacker has subdomain XSS). The hash-based source (#fragment) is particularly interesting because it is not logged by servers — URL fragments are browser-only.
webpack-dev-server's development proxy had a DOM-based open redirect in its error overlay. When the dev server served an error page, client-side JavaScript read the redirect destination from the URL without validation. An attacker could craft a URL pointing to the dev server that redirected developers to a phishing page. CVSS 6.1. Only affected development environments.
Traditional scanners inject payloads into HTTP parameters and check HTTP responses. DOM-based redirects produce no server-side 30x response — the redirect is observable only by executing the JavaScript and watching the browser navigate. Detecting DOM-based redirects requires JavaScript rendering (headless Chrome/Firefox with Playwright or Puppeteer) plus navigation event monitoring.
Some applications use the URL fragment (location.hash) for single-page app routing: if (location.hash) window.location = location.hash.slice(1). An attacker sends a URL like https://trusted.com/page#https://evil.com — the server never sees the fragment (it is stripped before the HTTP request), but the client-side JavaScript reads it and navigates to evil.com. This technique is invisible in server logs.
Use Burp's DOM Invader tool with the canary injection option enabled. Navigate the application — DOM Invader traces data flows from sources (location.search, location.hash, referrer) to sinks (location.href, location.replace). When a source-to-sink flow reaches a navigation sink, DOM Invader flags it. For manual testing: inject a canary string into URL parameters and hash, then monitor the browser's navigation events for canary propagation.
Applications that use window.addEventListener('message', handler) and navigate based on message data are vulnerable if the handler does not validate message.origin. An attacker's page sends: window.opener.postMessage({redirect: 'https://evil.com'}, '*'). If the target page's message handler assigns message.data.redirect to window.location without validation, the user is redirected. The * wildcard in postMessage is the prerequisite for cross-origin exploitation.
React Router, Vue Router, and Angular Router use their own navigation APIs (navigate(), router.push(), router.navigate()). These are NOT directly equivalent to window.location assignment — they update the URL without full page navigation if the destination is same-origin. However, if the SPA passes the destination to window.location directly for external URLs (common pattern for post-auth redirects), the same open redirect risk applies. React Router v6's useNavigate() only navigates within the app — external URLs via useNavigate() are blocked by design.
DOM-based open redirect base CVSS is typically 4.3-6.1 (Medium). CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N (4.3) for pure phishing. When chained with token theft via Referer or cookie exfiltration, CVSS elevates to 6.1-7.4. The client-side nature means the impact is limited to the user who follows the crafted link — no server-side impact without additional chaining.