URLs that come from users or from data (a profile website, a “return to” parameter, an API field) can be used in unsafe ways. Validate them before you put them into a link, an image or a redirect.
Problem 1: dangerous protocols
A link’s href does not have to start with http. Some schemes, such as javascript:, can run code when clicked.
<!-- If userUrl is "javascript:..." this link runs script -->
<a href="${userUrl}">Visit my site</a>
The fix: parse it and allow only what you expect
Use the URL constructor, and check the protocol against an allow list (a list of permitted values), instead of trying to block bad ones.
function safeUrl(input, allowedProtocols = ["https:", "http:"]) {
try {
const url = new URL(input);
return allowedProtocols.includes(url.protocol) ? url.href : null;
} catch {
return null; // not a valid absolute URL
}
}
console.log(safeUrl("https://example.com/page")); // → https://example.com/page
console.log(safeUrl("javascript:doSomething()")); // → null
console.log(safeUrl("data:text/html,hello")); // → null
console.log(safeUrl("not a url")); // → null
An allow list is safer than a block list, because you cannot predict every tricky variation.
Using it
<a id="link">Visit</a>
<script>
const link = document.querySelector("#link");
const userSupplied = "https://example.com/profile";
const href = safeUrl(userSupplied);
if (href) {
link.href = href;
link.textContent = "Visit the website";
} else {
link.removeAttribute("href");
link.textContent = "(invalid website)";
}
function safeUrl(input) {
try {
const url = new URL(input);
return ["https:", "http:"].includes(url.protocol) ? url.href : null;
} catch {
return null;
}
}
</script>
Problem 2: open redirects
A page that takes a “where to go next” URL, such as /login?next=..., and redirects there, can be abused to send users to a malicious site that looks like yours. Only redirect to addresses on your own site.
function safeRedirectTarget(next, siteOrigin = "https://example.com") {
try {
const url = new URL(next, siteOrigin); // relative paths are resolved against your origin
return url.origin === siteOrigin ? url.pathname + url.search + url.hash : "/";
} catch {
return "/";
}
}
console.log(safeRedirectTarget("/account/orders")); // → /account/orders
console.log(safeRedirectTarget("https://evil.example/login")); // → /
console.log(safeRedirectTarget("//evil.example/x")); // → /
Comparing the parsed origin handles tricks that simple string checks miss. Redirect checks belong on the server too.
Problem 3: links that open new tabs
Use rel="noopener" on links with target="_blank" so the new page cannot control yours through window.opener. Modern browsers already treat _blank links as noopener by default, but writing it is a clear habit. Add noreferrer if you also do not want to send the referrer.
<a href="https://other.example" target="_blank" rel="noopener noreferrer">External link</a>
Images and other resources
URLs set as src for images, frames or scripts should also come from trusted sources. Never load a script URL from user input.
Encoding when building URLs
Use URL and URLSearchParams to add user text as parameters (URL and URLSearchParams). They encode characters correctly.
The principle
URL, compare against an allow list, and fall back to a safe default. Never test URLs with a few startsWith checks.