How to Validate URLs in Code
URL validation fails more often than it should because the rules are subtle. A string like "https://example.com" is obviously valid, but "https://user:@example.com/path" is also valid, and so is "file:///etc/hosts". Regex-based validation usually accepts strings that are not URLs and rejects strings that are. The correct approach is to attempt parsing with a standards-compliant URL parser and inspect the result.1
Different contexts require different levels of validation. Accepting a URL for a hyperlink requires less strictness than accepting a URL for a server-side HTTP request, which must also verify the scheme is http or https and the host is not a private IP address to prevent server-side request forgery.
Parse-and-check validation
The most reliable validation technique is to attempt parsing the URL and check that the result has a valid scheme and non-empty host. In JavaScript: try { const u = new URL(input); return ['http:', 'https:'].includes(u.protocol); } catch { return false; }. Consequently, this approach rejects strings with no scheme, invalid characters, or empty hostnames without writing any regex.2 In Python, the equivalent is wrapping urlparse() and checking that result.scheme and result.netloc are non-empty. Note that urlparse never throws, so invalid inputs silently produce empty fields rather than raising errors.3 Always check both the scheme and the netloc fields explicitly, because a string like "not-a-url" parses without error but produces empty values for both.
Scheme and host constraints
For most web applications, only http:// and https:// URLs are acceptable. Blocking other schemes prevents link injection attacks where an attacker passes javascript: or data: as a URL. Building on this, for server-side HTTP request URLs, check whether a hostname resolves to a private IP range (10.x.x.x, 172.16 to 31.x.x, 192.168.x.x) or loopback address (127.0.0.1, ::1) before the request goes out, since skipping that check is what makes SSRF possible. DNS rebinding attacks mean IP address blocking alone is not sufficient for high-security contexts, so consult your platform's SSRF mitigation documentation for the full list of blocked ranges.4 CapyToolkit provides a URL validation tool that checks scheme and structure locally in your browser.
Regex and library approaches
Custom regex for URL validation is almost always wrong, since RFCs permit far more variation than regexes account for. Yet for quick form input feedback where false negatives are acceptable, a simple pattern like /^https?:\/\/[^\s]+$/ gives reasonable results. For production validation in Node.js, the url module's URL constructor is sufficient. In Python, the validators library's validators.url() is a well-maintained option. Building on this, PHP's filter_var($url, FILTER_VALIDATE_URL) handles most cases but does not validate that the TLD exists or that the host is reachable, so treat it as syntax validation only.5 When building your own validation helper, always return a structured result object with separate fields for the parsed URL and any error message, rather than throwing an exception, so callers can handle invalid input gracefully.
IDN homograph attacks and Unicode normalization in URL validation
Internationalized domain names open a class of phishing attacks that ASCII-only validation cannot detect. The domain xn--80ak6aa92e.com uses Cyrillic characters that visually resemble Latin letters; rendered in most browsers, it looks like apple.com but resolves to a different server. This is the IDN homograph attack, and it exploits the fact that many Unicode scripts contain characters indistinguishable from Latin letters. Modern browsers display the Punycode (ASCII) form of suspected homograph domains in the address bar, but the protection is heuristic and not foolproof.
Defending against homograph attacks in URL validation
When your application accepts URLs that will be displayed to users, normalize the hostname to its Punycode form and compare against known-safe domains. The ICU library (used by most browsers and by Python's idna module) implements the IDNA 2008 standard and can detect mixed-script hostnames. For internal tools where users submit URLs for processing (not display), validate that the hostname is either an ASCII domain or a registered IDN, and log any mixed-script hostnames for security review. Chrome maintains lists of domains that trigger Punycode display; model your detection on their approach.6
Because the risk is highest exactly where URLs are rendered as clickable links, prioritize the display path over the processing path in your detection budget. A mixed-script hostname that reaches a user's screen can do damage even if the server never makes a request to it, so surface a warning or show the Punycode form whenever the script mix looks suspicious. Pair this with the scheme and host checks from parse-and-check validation so a homograph passes only when it is both well-formed and free of deceptive characters.
Validating URLs in TypeScript with branded types
TypeScript's type system cannot enforce URL validity at compile time, but branded types can prevent invalid strings from being passed to functions that expect validated URLs. Declare a brand: type ValidatedURL = string & { __brand: 'ValidatedURL' }. Your validation function returns ValidatedURL only after parsing succeeds and the scheme and hostname checks pass. From that point on, functions that accept ValidatedURL cannot accidentally receive an unvalidated string. This pattern catches misuse at compile time without runtime overhead beyond the initial validation.
Combining parse-and-check with schema validation
For APIs that receive URLs in request bodies, combine URL parsing with a JSON schema validator (Zod, Joi, or AJV). Define the schema as: z.string().url() in Zod, or Joi.string().uri() in Joi. These validators use the WHATWG URL Standard or a close approximation, catching most malformed URLs before your business logic runs. For stricter validation, chain a custom refinement that checks the protocol property of the parsed URL object, rejecting HTTP URLs at the schema level and enforcing HTTPS-only input without cluttering your route handlers with scattered validation logic.
URL validation in content security policies
Content Security Policy (CSP) headers control which URLs the browser may load resources from. A CSP directive like script-src https://cdn.example.com restricts script loading to that specific origin. The browser matches the full URL including the path, so https://cdn.example.com/script.js is allowed but https://cdn.example.com.evil.com/script.js is not (the dot makes it a completely different host). Wildcards in CSP follow specific rules: *.example.com matches any subdomain of example.com but not example.com itself. Failing to validate URLs before including them in CSP directives can inadvertently allow resources from attacker-controlled domains, undermining the entire policy.
Nonce-based CSP for inline scripts
For applications that must execute inline scripts (common in server-rendered pages), CSP nonces provide a secure alternative to allowing unsafe-inline. Generate a random nonce on each request, include it in the CSP header (script-src 'nonce-abc123'), and add the same nonce to each inline script tag. The browser executes only scripts whose nonce matches the header, blocking injected scripts that lack the correct nonce. This approach is supported in all modern browsers and is more flexible than hash-based CSP, which requires computing the SHA-256 hash of every allowed inline script.
When to use this
Validate URLs whenever your code accepts them from user input, configuration files, database records, or external APIs, especially before using them in HTTP requests, anchor hrefs, or image src attributes where invalid values cause errors or security issues.
Examples
Parse-and-check URL validation in JavaScript
// Regex approach — rejects valid URLs, accepts invalid ones const isValid = /^https?:\/\/.+/.test(input);
// Parse-and-check — correct and readable
function isValidHttpUrl(input) {
try {
const u = new URL(input);
return u.protocol === "http:" || u.protocol === "https:";
} catch {
return false;
}
} Parse-and-check URL validation in Python
from urllib.parse import urlparse def is_valid_http_url(url: str) -> bool: try: result = urlparse(url) return result.scheme in ("http", "https") and bool(result.netloc) except ValueError: return False
- 1.
WHATWG, "URL Standard," url.spec.whatwg.org, accessed June 2026. https://url.spec.whatwg.org/
- 2.
Mozilla Developer Network, "URL: URL() constructor," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/API/URL/URL
- 3.
Python Software Foundation, "urllib.parse — Parse URLs into components," docs.python.org, accessed June 2026. https://docs.python.org/3/library/urllib.parse.html
- 4.
OWASP, "Server-Side Request Forgery Prevention Cheat Sheet," owasp.org, accessed June 2026. https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
- 5.
PHP, "filter_var," php.net, accessed June 2026. https://www.php.net/manual/en/function.filter-var.php
- 6.
"IDN homograph attack," Wikipedia, accessed June 2026. https://en.wikipedia.org/wiki/IDN_homograph_attack