Safeguard
Security

How to Validate a URL in JavaScript Without Opening a Hole

The modern way to validate a URL in JavaScript is the built-in URL constructor, not a regex. Here's how to use it safely on both the client and the server.

Aisha Rahman
Security Analyst
5 min read

To validate a URL in JavaScript, use the built-in URL constructor and enforce a scheme allowlist — a regular expression is the wrong tool for this job. The new URL() API parses according to the WHATWG URL standard, handles the edge cases you would otherwise get wrong, and throws on genuinely malformed input. What it does not do on its own is decide whether a parseable URL is safe, and that gap is where cross-site scripting and server-side request forgery bugs get in.

The one-liner that works

Every browser and every modern Node.js runtime ships the URL constructor. The simplest way to validate url in javascript is to try to construct one:

function isValidUrl(input) {
  try {
    new URL(input);
    return true;
  } catch {
    return false;
  }
}

If input is not a parseable absolute URL, the constructor throws and you return false. This already beats most hand-rolled regexes: it correctly accepts internationalized domains, ports, and percent-encoded paths, and correctly rejects garbage. For older environments there is also URL.canParse(input), a static method that returns a boolean without the try/catch, though you should confirm your target runtimes support it before relying on it.

Why not a regex?

People reach for a regex because it feels self-contained, but URL grammar is defined by a specification that a short regex cannot capture. The regexes that actually get pasted into projects reject valid URLs like http://localhost:3000 and accept dangerous ones like javascript:alert(document.cookie). That second case is the whole problem. If you take a "validated" URL and drop it into an href or window.location, a javascript: scheme executes script in your page — a classic DOM XSS. A regex that only checks "does this look URL-ish" waves it straight through.

The URL constructor does not save you here either, because new URL("javascript:alert(1)") parses successfully — javascript is a legitimate URL scheme. Parsing tells you the string is structured; it does not tell you the scheme is one you want to allow.

Adding a scheme allowlist

The fix is an explicit allowlist. Decide which schemes your feature needs and reject everything else:

const ALLOWED = new Set(["http:", "https:"]);

function isSafeUrl(input) {
  let url;
  try {
    url = new URL(input);
  } catch {
    return false;
  }
  return ALLOWED.has(url.protocol);
}

Note that url.protocol includes the trailing colon ("https:"), which trips people up. With this in place, javascript:, data:, vbscript:, and file: are all rejected in one check without you enumerating each dangerous scheme by hand. When you validate URL in javascript specifically to render a link or perform a redirect, this scheme gate is the single most important line in the function.

Rendering user URLs safely

Validation is necessary but not sufficient when the URL ends up in the DOM. Two rules keep you out of trouble.

First, for links, set the property rather than building HTML strings, and let the framework escape it. In plain DOM:

if (isSafeUrl(userInput)) {
  anchor.href = userInput;
} else {
  anchor.removeAttribute("href");
}

Second, for links that open in a new tab, add rel="noopener noreferrer" so the destination page cannot reach back into your window via window.opener. Modern browsers imply noopener for target="_blank", but setting it explicitly is cheap insurance.

Server-side: the SSRF angle

If your JavaScript runs on the server — a Node.js API that fetches a user-supplied URL for a webhook, link preview, or image import — scheme allowlisting is not enough. You also have to check where the host resolves. An attacker who submits http://169.254.169.254/ is not attacking the browser; they are trying to make your server request its own cloud metadata endpoint.

import dns from "node:dns/promises";
import net from "node:net";

async function isPublicUrl(input) {
  let url;
  try {
    url = new URL(input);
  } catch {
    return false;
  }
  if (!["http:", "https:"].includes(url.protocol)) return false;

  const { address } = await dns.lookup(url.hostname);
  if (net.isIP(address) === 0) return false;
  // reject loopback, private, and link-local ranges
  return !/^(127\.|10\.|192\.168\.|169\.254\.|::1)/.test(address)
    && !/^172\.(1[6-9]|2\d|3[01])\./.test(address);
}

That range check is illustrative rather than exhaustive; production code should use a maintained IP-range library and guard against DNS rebinding by connecting to the resolved IP directly. The principle is what matters: on the server you validate the destination, not just the string.

Watch your dependencies too

Plenty of projects pull in a validation package like validator or a URL-parsing helper rather than using the platform API. Those are fine, but they are dependencies with their own advisory history — URL and query-string parsers have shipped ReDoS and parsing-confusion bugs before. Keeping them current matters, and an SCA tool such as Safeguard can flag when a validation library you depend on has a known issue. For the wider set of injection and XSS patterns, the Safeguard Academy has worked examples.

FAQ

Should I use the URL constructor or a library to validate URLs?

Start with the built-in URL constructor — it is standards-compliant, dependency-free, and available everywhere. Reach for a library only when you need extra checks it doesn't cover, and then keep that library patched.

Does new URL() prevent XSS?

Not by itself. It parses javascript: and data: URLs happily. You prevent XSS by adding a scheme allowlist (http:/https: only) and by assigning URLs to DOM properties rather than concatenating them into HTML.

How do I validate a relative URL?

The URL constructor needs a base for relative inputs: new URL(input, window.location.origin). This resolves the relative path against a known origin, which also lets you confirm the result stays on your own domain.

Is client-side URL validation enough?

No. Client-side checks improve UX but can be bypassed. Any URL your server acts on — fetches, redirects to, or stores — must be validated again server-side, including the SSRF destination check.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.