GuardVibe
News
· 6 min read

fast-uri Host Confusion, an undici Crash and Two Angular SSR Fixes

Two fast-uri bugs let URLs pass host checks but reach localhost, a bad WebSocket frame crashes undici, and Angular SSR fixes a DoS and an XSS.

Two new fast-uri advisories let a crafted URL point one part of your app at one host while your HTTP client connects to another. fast-uri is pulled in by Ajv and Fastify, so most Node projects have it somewhere in the tree. The same day brought a one-packet crash in undici's WebSocket client and two Angular SSR fixes, one of which was left open by the release that fixed last month's SSR bugs.

What shipped

fast-uri: authority injection through the port

  • Package: fast-uri below 2.4.6, 3.0.0–3.1.6 and 4.0.0–4.1.3. Fixed in 2.4.6, 3.1.7 and 4.1.4.
  • Severity: high, CVSS 7.5 (GHSA-qw65-cvwx-89v3, CVE-2026-84292).
  • Impact: serialize() escapes the user and host parts but copies port verbatim. A port value of @127.0.0.1:8124 turns trusted.example into userinfo, and both fast-uri and Node's URL read the result back as a request to 127.0.0.1. Re-validating the built string does not catch it.
  • Who is exposed: code that builds URLs from parts and takes the port from user input or a service record.
npm ls fast-uri
npm update fast-uri   # or bump ajv / fastify so the lockfile picks up 3.1.7+ / 4.1.4+

fast-uri: host confusion through an unclosed bracket

  • Package: fast-uri 2.4.5, 3.1.6 and 4.1.3 exactly. Fixed in the same 2.4.6 / 3.1.7 / 4.1.4.
  • Severity: high, CVSS 7.5 (GHSA-58mr-gqgx-xq4g, CVE-2026-84394).
  • Impact: a host such as [@127.0.0.1 passes parse() with no error, while Node's URL, http.get, axios and got resolve it to 127.0.0.1. An SSRF denylist or redirect allowlist that checks parse().host approves a request that lands on localhost.

undici: one malformed WebSocket frame kills the process

  • Package: undici 6.25.0–6.28.0, 7.28.0–7.29.0 and 8.1.0–8.10.1. Fixed in 6.28.1, 7.29.1 and 8.10.2.
  • Severity: medium, CVSS 5.9 (GHSA-3wwx-pv8p-q78v, CVE-2026-85024).
  • Impact: a WebSocket server sends a compressed message that crosses the size limit and then a bad DEFLATE block. The internal inflater errors with no listener attached, and Node treats that as fatal. Your error and close handlers never see it. The advisory says Node's built-in globalThis.WebSocket is affected too, since it is undici.
  • Who is exposed: any server-side code that opens a WebSocket to a peer it does not fully control, such as an AI streaming endpoint, a market data feed or a user-supplied webhook URL.
npm ls undici
npm install undici@latest

If you use the global WebSocket with no undici dependency, the fix arrives with a Node.js release that bundles the patched undici. We could not confirm which Node versions include it at the time of writing.

Angular SSR: infinite loop on a malformed DOCTYPE

  • Package: @angular/platform-server 20.0.0–20.3.30, 21.0.0–21.2.22 and 22.0.0–22.1.5. Fixed in 20.3.31, 21.2.23 and 22.1.6. The advisory lists 19.2.25 and earlier as affected with no fix.
  • Severity: high (GHSA-f67j-2jqw-jpq7, CVE-2026-101895).
  • Impact: untrusted HTML ending in <!DOCTYPE html puts the server-side DOM parser into an endless synchronous loop. One request freezes the Node process for every user.
  • Note: 22.1.4, 21.2.22 and 20.3.30 were last month's fix releases, and they are still affected by this one.

Angular SSR: XSS through processing instructions

  • Package: @angular/platform-server 20.0.0–20.3.29, 21.0.0–21.2.21 and 22.0.0–22.1.3. Fixed in 20.3.30, 21.2.22 and 22.1.4.
  • Severity: high (GHSA-j3r3-mxqp-r2p4, CVE-2026-88058).
  • Impact: a <?...?> node inside <noscript>, <iframe>, <noembed> or <noframes> can carry </noscript>. That closes the element early in the browser, so the markup after it runs as live HTML.

Upgrading to 22.1.6, 21.2.23 or 20.3.31 closes both Angular issues:

ng update @angular/core @angular/cli

URL parser differentials, explained

A parser differential happens when two pieces of code read the same input and disagree about what it means. With URLs, the dangerous disagreement is about the host. Your security check parses the URL one way and decides it points at api.partner.com. Your HTTP client parses it another way and connects to 127.0.0.1. Neither parser is necessarily wrong on its own. The bug is that the check and the request use different parsers.

This keeps showing up in AI-generated code for a simple reason. When you ask an agent to "block internal URLs", it reaches for whatever parser is already in the file, or a fast one it has seen in Fastify code, and writes the check. Then it passes the original string to fetch. Each line looks right, and the generated tests use well-formed URLs, so the gap never runs.

Here is the shape in a Next.js route handler:

// Vulnerable: the policy and the request parse the URL separately
import { parse } from "fast-uri";

export async function POST(req: Request) {
  const { target } = await req.json();
  const { host } = parse(target);            // "[@127.0.0.1" passes as-is
  if (!host || BLOCKED_HOSTS.has(host)) {
    return new Response("blocked", { status: 400 });
  }
  const res = await fetch(target);           // fetch resolves 127.0.0.1
  return new Response(await res.text());
}

The fix is to parse once, check the parsed result, and send exactly the object you checked:

// Fixed: one parser, one object, allowlist not denylist
export async function POST(req: Request) {
  const { target } = await req.json();
  let url: URL;
  try {
    url = new URL(target);
  } catch {
    return new Response("bad url", { status: 400 });
  }
  if (url.protocol !== "https:" || !ALLOWED_HOSTS.has(url.hostname)) {
    return new Response("blocked", { status: 400 });
  }
  const res = await fetch(url, { redirect: "manual" });
  return new Response(await res.text());
}

Three details carry the fix. The check and the request share one URL object, so they cannot disagree. The allowlist names hosts you trust instead of guessing every spelling of localhost. And redirect: "manual" stops a trusted host from bouncing the request somewhere else. For user-supplied destinations, also resolve DNS and reject private addresses, because an allowlisted name can still resolve to an internal IP.

The fast-uri port bug is the same idea from the other side. You build a URL from a trusted host plus an untrusted piece, and that piece changes where the authority ends. When you build URLs, set fields on a URL object instead of joining strings. Also validate the port as digits in range before you use it.

The review check: wherever a URL gets a security decision, find the line that makes the request. If that line does not use the same parsed object the check used, you have two parsers and one of them wins.

Check your own repo

npm ls fast-uri undici @angular/platform-server   # which versions are in your tree?
npm audit                                          # picks up all five advisories
grep -rn "fast-uri" --include=*.ts --include=*.js src/   # direct use of parse()/serialize()?
npx guardvibe@3.44.0 audit .   # 3.44.0 flags the Angular SSR releases still open to the DOCTYPE loop (VG1165)

Sources

Get the next one in your feed reader

Follow GuardVibe in your feed reader. No account, no email.