DO10IIndependent
security research
DO10I / LAB STUDY

One URL. Two parsers. Different boundaries.

A reproducible comparison of URL interpretation in Node.js: encoded dot segments, default ports and inputs that do not mean the same thing to both APIs.

PARSER DIFFERENTIALS / LOCAL EXPERIMENT / NODE.JS

A security check and the component that acts on its decision need to agree on what the input means. This local experiment compares Node.js’s WHATWG URL API with its legacy parser. Several short inputs produce different host or path representations. The result is a study of interpretation, not a vulnerability claim against Node.js or a deployed service.

The question

When one part of a system checks a URL and another part uses it, what exactly has been approved?

The original string is only the beginning. A parser gives that string structure: scheme, credentials, host, port, path and query. A later component may interpret or normalise the input differently. A comparison between raw text and a parsed destination can therefore answer a different question from the one the developer intended.

This note examines a narrow, reproducible example: two URL APIs provided by the same Node.js runtime. There is no remote target, no network traffic and no claim of a newly discovered vulnerability. The purpose is to make a potential source of disagreement concrete.

Test environment

The experiment ran locally using Node.js v24.22.1. The corpus consists of six fixed strings using example.com. No requests are made to that domain.

For each input, the test records host and pathname from:

  • new URL(input), the WHATWG-compatible API;
  • parse(input) from node:url, the legacy API.

The legacy parser is included for comparison, not recommended for new security-sensitive code. Node.js documents it as a legacy API and warns about its security-sensitive parsing behaviour. Consult the versioned documentation for the runtime being assessed.

Reproduction

Download the complete experiment. Inspect it, then run:

node url-parsers.mjs

The script prints the runtime version, records that it makes zero network requests, and produces structured results. Assertions check the differences discussed below. An assertion failure means the recorded interpretation should be revisited for that runtime.

The core comparison is small:

import { parse } from 'node:url';

const input = 'https://example.com/%2e%2e/admin';
const modern = new URL(input);
const legacy = parse(input);

console.log(modern.pathname); // /admin
console.log(legacy.pathname); // /%2e%2e/admin

The legacy API may emit a deprecation warning. That warning is expected and does not invalidate the observed output.

Observed results

The following values were obtained in the stated local runtime. Paths are shown separately from hosts so that the source of disagreement remains visible.

Input suffix after https:WHATWG hostLegacy hostWHATWG pathLegacy path
//example.com/pathexample.comexample.com/path/path
//example.com/%2e%2e/adminexample.comexample.com/admin/%2e%2e/admin
//example.com:443/aexample.comexample.com:443/a/a
//example.com/a/../bexample.comexample.com/b/a/../b
example.com/pathexample.comnull/pathexample.com/path
//example.com/%2Fadminexample.comexample.com/%2Fadmin/%2Fadmin

Encoded dot segments

For /%2e%2e/admin, the WHATWG API returns /admin. The legacy parser preserves the encoded segment in its pathname.

This distinction matters when reviewing a system that checks a path representation before a downstream component normalises it. The experiment does not show that such a system exists or that an access control has been bypassed. It identifies the exact representation difference that would need to be traced through a real application.

Default ports

For an HTTPS URL containing :443, the WHATWG API returns example.com as host; the legacy API returns example.com:443.

The representations differ even though the port is the default for the scheme. A string inequality is not automatically a different security destination. Equally, a normalised match is not proof that an entire destination policy is correct.

Literal dot segments

The input /a/../b becomes /b in the WHATWG pathname, while the legacy pathname retains /a/../b.

This is a useful control alongside the encoded example. It separates a general normalisation difference from an explanation that depends only on percent-encoding.

An input without the familiar double slash

For https:example.com/path, the WHATWG constructor identifies example.com as the host. With the legacy parser’s default arguments, host is null and the pathname is example.com/path.

The familiar appearance of an input is not a sufficient description of its parse result. Parser choice and invocation are part of the evidence.

A useful non-difference

Both APIs preserve /%2Fadmin as the pathname in this corpus. It would be incorrect to conclude that every percent-encoded character is simply decoded during URL construction.

Negative controls are valuable: they limit the explanation and stop a real observation becoming an overbroad claim.

What the experiment establishes

The same runtime exposes APIs that can assign different structured representations to the same URL text. The differences are observable without an application, a proxy or a remote service.

The security consequence is conditional. A relevant finding would need to demonstrate a complete path from an attacker-controlled input, through a particular check, to a later action that violates the intended boundary. Parser disagreement alone does not establish that outcome.

In a real investigation, the next step would be to identify the exact check and consumer, preserve their inputs and outputs, and test whether a policy decision changes. A rejection, a harmless canonicalisation or a downstream re-check may prevent any security impact.

Limits and interpretation

This is a small corpus, not a fuzzing campaign. It covers one runtime version and two specific APIs with their default invocation patterns. It does not measure browser behaviour, HTTP-client destinations, DNS resolution, proxy normalisation or operating-system path handling.

The observations are consistent with a documented difference between the modern URL API and the legacy API. They are not presented as novel Node.js vulnerabilities, vendor disclosures or bounty-eligible results.

The general lesson is precise: when a policy depends on the meaning of an input, preserve the interpretation used for the decision and verify what the eventual consumer actually does. A familiar-looking string is not a substitute for that evidence.

References

  1. Node.js v24 URL API documentation
  2. WHATWG URL Standard