Safeguard
Application Security

Safely Parsing Untrusted URLs in Node.js

Node's legacy url.parse() is deprecated (DEP0169), and parser mismatches between it, the WHATWG URL API, and fetchers are a documented root cause of SSRF and open redirects.

Safeguard Research Team
Research
6 min read

Node.js's legacy url.parse() has carried a deprecation warning, DEP0169, for several releases now, and the Node project's own documentation is blunt about why: the API is non-standardized, inconsistent with how browsers and other HTTP clients interpret the same string, and a frequent source of security bugs. Node's changelog and issue tracker show the warning still surfacing in 2025 and 2026 inside widely used tooling — pnpm, esbuild, and dependencies of Cypress and axios all triggered it, evidence that legacy parsing logic is still load-bearing across the ecosystem years after the WHATWG URL API became the recommended replacement. The danger isn't academic: when a validator parses a URL one way and the code that actually fetches or redirects to it parses the same string a different way, an attacker can craft input that sails through the check and lands somewhere the check never saw. Security researchers call this class "URL confusion," and it has been documented across many real products by both Snyk's application-security research team and Claroty's Team82, including a concrete backslash-based bypass technique that predates both write-ups and shows up in CVE-2021-32786. This piece walks through where Node's URL tooling diverges from what a fetch or redirect will actually do, and how to close that gap.

Why is url.parse() deprecated, and what should replace it?

Node deprecated the legacy url.parse() under code DEP0169 because it predates the WHATWG URL Standard and diverges from it in how it handles malformed input, relative references, and special characters — instead of throwing on invalid input, the legacy parser often returns a partially-populated object that looks usable but misrepresents the string. Node's documentation now directs developers to the WHATWG-compliant new URL() constructor, available globally since Node 10 and exposed via require( ""node:url"" ) for the URL and URLSearchParams classes. The practical risk with the legacy API is silent inconsistency: two different code paths in the same application — one using url.parse() for a validation check, another using fetch() (which is WHATWG-compliant internally) to make the request — can disagree about what host a string actually points to. That disagreement is exactly the gap attackers exploit in SSRF and open-redirect bypasses, so the fix is not just "avoid a deprecation warning" but "stop having two parsers with different opinions in the same request path."

What is a "URL confusion" vulnerability?

A URL confusion vulnerability is a bug where two different pieces of software parse the identical URL string into two different results, and an attacker exploits the gap between what a validator approved and what a downstream component actually acted on. Snyk's application-security research, published under the title "URL confusion vulnerabilities in the wild," and Claroty Team82's "Exploiting URL Parsing Confusion" research both catalog this as a recurring pattern across unrelated products, rooted in the fact that RFC 3986 (the general URI grammar) and the WHATWG URL Standard (what every modern browser, curl, and Node's URL class implement) are not fully interchangeable specifications. A validator built against RFC 3986 semantics can treat a component as inert punctuation that a WHATWG-compliant fetcher treats as a host or credential separator, and that mismatch is the entire vulnerability — no memory corruption or injection required, just two specs disagreeing.

How does the backslash-as-slash trick bypass hostname allowlists?

The WHATWG URL Standard specifies that browsers and WHATWG-compliant parsers must treat a backslash (\) the same as a forward slash (/) when it appears where the authority or path is expected — a normalization rule RFC 3986-based parsers do not share. That single divergence is enough to defeat naive hostname checks: a string like https://trusted-domain.com\@evil.com is read by an RFC 3986-style validator as a path or query fragment attached to trusted-domain.com, while a WHATWG-compliant browser or fetch client normalizes the backslash to a slash first, making evil.com the actual authority component. HackTricks' URL-format-bypass reference and independent researcher write-ups from dayzerosec document this as an active, still-effective bypass technique against allowlist filters that only inspect the string once (so-called "single-resolve" checks) rather than the fully normalized, WHATWG-parsed result. CVE-2021-32786, in mod_auth_openidc, is a real-world case of exactly this divergence: its RFC 3986-based apr_uri_parse disagreed with browser WHATWG parsing on backslash handling, enabling an open redirect.

What does the WHATWG URL API get right that developers still misuse?

Node's new URL() constructor is WHATWG-compliant, but it is not a security allowlist by itself, and treating its output naively still leaves gaps. The most common mistake is checking url.hostname (or worse, doing a String.prototype.includes() check against the raw input) instead of verifying the parsed origin against an exact, case-normalized allowlist — a substring check on hostname.includes( ""trusted.com"" ) is defeated trivially by evil-trusted.com or trusted.com.evil.net. Another frequent gap is validating a relative-looking string before resolving it against a base: new URL( userInput, baseUrl ) will happily produce an absolute URL pointing anywhere if userInput itself is already absolute, because the WHATWG algorithm lets an absolute reference override the base entirely. Developers relying on this constructor for SSRF defense also need to separately resolve DNS and check the resulting IP against private/link-local ranges, since URL parsing tells you nothing about where a hostname actually resolves — a validated, allowlisted hostname can still be re-pointed at an internal address via DNS at request time.

What does a defensible validation approach look like in practice?

A defensible approach parses once with the WHATWG URL constructor, validates the parsed object's protocol and hostname against strict, exact-match allowlists (not substring or prefix checks), and re-validates immediately before the network call is actually made — not earlier in the request pipeline, where a redirect or later transformation could change the target. Because fetch() and most modern HTTP clients follow redirects by default, an allowlist check on the initial URL alone doesn't account for a 3xx response steering the request to a disallowed host after the fact; disabling automatic redirect-following (or capping and re-validating each hop) is a documented mitigation for SSRF specifically because it closes that gap. Rejecting non-http:/https: protocols outright — blocking file:, data:, and gopher: — matters just as much as hostname checks, since protocol-based bypasses have appeared repeatedly in SSRF research as a way around hostname-only filters. None of this replaces network-layer controls: an application-layer allowlist is a defense-in-depth measure, and any service that fetches attacker-influenced URLs should also sit behind egress rules that block outbound access to internal address ranges regardless of what the parser decided.

Never miss an update

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