Client-side JavaScript copies location.href, location.hash or URL parameters into window.location, turning a DOM sink into an open redirect.
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:2025 (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:
sequenceDiagram
participant A as Attacker
participant V as Victim Browser
participant S as trusted-app.com
participant M as attacker.com
A->>V: Sends URL: trusted-app.com/callback#https://attacker.com/phish
V->>S: GET /callback (fragment not sent to server)
S-->>V: 200 OK + JavaScript bundle
Note over V: JS executes: const dest = location.hash.slice(1)
Note over V: window.location = dest (no validation)
V->>M: Browser navigates to https://attacker.com/phish
Note over A: Server logs show only GET /callback — no redirect recordedThe 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 |
CVE-2024-43788 — webpack DOM Clobbering (CVSS 3.1 base 6.1 (NVD), 6.4 (CNA GitHub), 2024) — cited to correct a mis-citation, not as an instance of this class
webpack's AutoPublicPathRuntimeModule read the public path from DOM elements it did not validate. A page able to inject HTML could clobber the element the runtime reads and point the bundle at a script of the attacker's choice — DOM Clobbering leading to XSS (CWE-79). There is no navigation sink and no unvalidated window.location assignment anywhere in that flow, and no webpack-dev-server involvement: the flaw is in webpack's runtime module. It is not an open redirect.
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.
No. CVE-2024-43788 is DOM Clobbering in webpack's AutoPublicPathRuntimeModule: a page able to inject HTML can clobber the element the runtime reads its public path from and point the bundle at a script of the attacker's choice, which is XSS (CWE-79). It sits in the same DOM source-to-sink family, but there is no navigation sink and no unvalidated window.location assignment, so nothing is redirected. CVSS 3.1 base 6.1 (NVD), 6.4 (CNA GitHub).
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.7) 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.