high · 7.5CVE-2026-76172Sep 2, 2026

CVE-2026-76172: fast-uri Host Confusion via Percent-Encoded Scheme Normalization

Shubham Kandhare
Security Engagement Manager, SecureLayer7

fast-uri decodes percent-encoded characters in the URI scheme using the legacy unescape() function and writes the decoded result back verbatim, letting an attacker craft a scheme that becomes…

Packagefast-uri
Ecosystemnpm
Affected>= 2.3.1, < 2.4.5
Fixed in2.4.5
CVE-2026-76172: fast-uri Host Confusion via Percent-Encoded Scheme Normalization

The problem

fast-uri calls the legacy global unescape() on the scheme component and serializes the decoded result back as raw characters, with no re-validation against the RFC 3986 scheme grammar and no re-escaping.

A URI like %2f%2fevil.example:/pwn parses with no authority (host is undefined), but after resolve() or normalize() the output is //evil.example:/pwn, which reparses with host evil.example. The %uXXXX form supported by unescape() triggers the same transformation, and a scheme containing %0d%0a reaches the serialized output as a raw CR LF sequence, enabling header injection.

Proof of concept

A working proof-of-concept for CVE-2026-76172 in fast-uri, with the exact payload below.

javascript
const uri = require('fast-uri')

// Before patch: host is undefined on parse, but resolve/normalize surfaces evil.example
console.log(uri.parse('%2f%2fevil.example:/pwn').host)
// => undefined  (looks safe)

console.log(uri.normalize('%2f%2fevil.example:/pwn'))
// => //evil.example:/pwn  (host is now evil.example)

// Legacy %uXXXX form (also decoded by unescape()) produces the same result
console.log(uri.normalize('%u002f%u002fevil.example:/pwn'))
// => //evil.example:/pwn

// CRLF injection via scheme
console.log(uri.normalize('%0d%0aX-Injected: yes\r\nexample:/path'))
// => raw CR LF lands in serialized output

The root cause is a single call: scheme = unescape(parsed.scheme) in index.js. The legacy unescape() decodes every percent-sequence, including %2F (slash), %0D%0A (CR LF), and the non-standard %uXXXX Unicode form. The decoded value is written straight back into the serialized URI without re-escaping or checking that the result still conforms to the RFC 3986 scheme grammar (which permits only ALPHA *( ALPHA / DIGIT / '+' / '-' / '.' )).

This means an input that looks like an opaque scheme on parse becomes an authority introducer after normalization, flipping host from undefined to an attacker-controlled value. Any middleware that checks the parsed host before calling normalize() or resolve() sees a safe value, while the downstream consumer sees an entirely different host.

The fix

Upgrade to fast-uri >= 2.4.5 (v2.x line), >= 3.1.6 (v3.x line), or >= 4.1.3 (v4.x line). The patch validates scheme characters after decoding and rejects or re-escapes any scheme that contains characters outside the RFC 3986 allowed set, preventing decoded slashes or control characters from reaching the serialized output.

Reported by OpenJS Foundation CNA.

References: [1][2][3][4][5][6]

Related research