decompress Symlink Escape, brace-expansion Crashes, undici TLS Gap
A symlink chain lets archives escape @xhmikosr/decompress; brace-expansion and joi stall Node; undici fixes a crash, a dropped TLS check and cache poisoning.
A critical path traversal in @xhmikosr/decompress lets a crafted archive write files outside the folder you extract into. The same day brought two stack-exhaustion crashes in brace-expansion, which sits under most glob tooling, three undici fixes including a dropped TLS check, and a slow regex in joi's isoDate(). All four have patched releases.
What shipped
@xhmikosr/decompress: path traversal via a symlink chain
- Affected:
@xhmikosr/decompressup to 10.2.1, and 11.0.0 through 11.1.3 - Fixed: 11.1.4 (latest) and 10.2.2 (backport on the
release-v10tag) - Severity: critical, CVSS 9.1 — GHSA-hrh2-vp3x-79xf (CVE-2026-101894)
An archive with a chain of symlink entries makes a later entry land outside the output directory. The path check passes because it looks at the string, but the operating system follows the planted links. The attacker can write and read files anywhere the process can, and overwriting a startup script or config file leads to code execution. This is a bypass of the earlier fix in GHSA-mp2f-45pm-3cg9. The unmaintained decompress package it was forked from has the same flaw and will not be patched.
npm install @xhmikosr/decompress@latest
If you still depend on plain decompress, the maintainers' advice is to switch to @xhmikosr/decompress.
brace-expansion: two ways to crash the process
- Affected: before 1.1.20, 2.0.0–2.1.5, 3.0.0–3.0.7, 4.0.0–5.0.10
- Fixed (both issues): 1.1.20, 2.1.6, 3.0.8, 5.0.11
- Severity: high, CVSS 7.5 — GHSA-6j4f-fj2g-mc7p (CVE-2026-102276), GHSA-qhr7-859c-m2p7 (CVE-2026-102278)
Two separate recursion bugs: one in the comma-list parser, one in the expander that runs once per level of nesting. A single pattern of roughly 15–30 KB exhausts the stack and the process dies with a RangeError. The first fixed releases (5.0.10 and its siblings) close only the parser bug, so check you are on the second set of versions. This package is almost never a direct dependency — it comes in through minimatch and glob libraries — so the fix is usually a lockfile refresh:
npm ls brace-expansion
npm update brace-expansion
It only matters if user input can reach a glob pattern, but that happens more often than people expect (file filters, search endpoints, config-driven include lists).
undici: WebSocket crash, dropped TLS check, cache poisoning
- Fixed: 6.28.1, 7.29.1, 8.10.2
- Severity: high — GHSA-rfgv-xxqx-mfg5 (CVE-2026-19534, CVSS 7.5), GHSA-w293-vg96-wgc3 (CVE-2026-84961, CVSS 7.4), GHSA-vp8m-p9jh-q5pm (CVE-2026-85152, CVSS 7.4)
Three separate problems. From 6.7.0, a WebSocket server that answers with a subprotocol the client never asked for triggers an uncaught exception that kills the Node.js process, even with a plain new WebSocket(url). From 7.24.1, BalancedPool deep-clones its options through JSON, which silently drops a custom checkServerIdentity callback, so certificates you meant to reject are accepted. In 8.10.0–8.10.1, interceptors.cache() and interceptors.deduplicate() build keys without the destination origin, so a response from one origin can be served for another. The advisory gives JWKS poisoning as an example. This is a different WebSocket bug from the undici crash we covered yesterday.
npm install undici@latest
joi: quadratic regex in isoDate()
- Affected: 17.2.0–17.13.6 and 18.0.0–18.2.5
- Fixed: 17.13.7 and 18.2.6
- Severity: high, CVSS 7.5 — GHSA-6h2x-m376-mqjq
Joi.string().isoDate() ran an unanchored regex. A valid date followed by a long run of fractional-second digits takes time proportional to the square of its length: about 1.4 s for 64 KB and 22 s for 256 KB, blocking the event loop the whole time. The only workaround is capping the string length before joi sees it.
npm install joi@latest
Archive path traversal, explained
"Zip slip" is the name for an archive entry that writes outside the directory you extract into. The classic version is an entry named ../../.bashrc. Most extractors now reject names containing .., so attackers use symlinks instead. The archive first creates a link that points outside the target, then writes a file through that link. Every name in the archive looks clean, but the final write lands wherever the link points. The decompress bypass goes one step further: a chain of links, each one relative to the last.
AI-generated code runs into this in two ways. Asked to "unzip the uploaded file", a model usually calls the library with defaults and adds no checks of its own. When it does add a check, it is usually a string check such as path.join(dest, entry.path).startsWith(dest), which is exactly the check that symlinks get around.
A vulnerable handler in a Next.js route:
import decompress from "@xhmikosr/decompress";
export async function POST(req: Request) {
const buf = Buffer.from(await req.arrayBuffer());
await decompress(buf, "/tmp/uploads/job-1"); // trusts every entry
return Response.json({ ok: true });
}
A safer version drops link entries and confirms the real path of each file after writing:
import decompress from "@xhmikosr/decompress";
import { realpath } from "node:fs/promises";
import path from "node:path";
const dest = "/tmp/uploads/job-1";
const files = await decompress(buf, dest, {
filter: (f) => f.type === "file" || f.type === "directory", // no symlinks or hardlinks
});
const root = await realpath(dest);
for (const f of files) {
const real = await realpath(path.join(dest, f.path));
if (!real.startsWith(root + path.sep)) throw new Error("entry escaped the output dir");
}
Filtering out link entries does most of the work. The realpath check is the backstop, because it looks at where the file actually ended up rather than what its name says. Extracting as a user that cannot write anywhere important limits the damage from anything you missed.
The review check: wherever an archive from a user is extracted, ask what the library does with symlinks, and whether anything checks the resolved path rather than the name. If the answer is "the defaults" and "a string compare", treat it as vulnerable. This applies to zip, tar and any other archive format.
Check your own repo
npm ls @xhmikosr/decompress decompress brace-expansion undici joi # which of these are in your tree?
npm audit # advisories against the installed versions
npx guardvibe@3.45.0 audit . # 3.45.0 flags the affected decompress, brace-expansion, undici and joi pins (VG1171–VG1174)
Most of these packages arrive as transitive dependencies, so npm ls tells you more than your package.json does. Upgrading is usually a matter of refreshing the lockfile.
Sources
- GHSA-hrh2-vp3x-79xf — @xhmikosr/decompress path traversal via symlink chain
- GHSA-6j4f-fj2g-mc7p — brace-expansion recursion in parseCommaParts
- GHSA-qhr7-859c-m2p7 — brace-expansion recursion on nested brace groups
- GHSA-rfgv-xxqx-mfg5 — undici DoS via unrequested WebSocket subprotocol
- GHSA-w293-vg96-wgc3 — undici BalancedPool drops connect/tls options
- GHSA-vp8m-p9jh-q5pm — undici cross-origin cache poisoning in interceptors
- GHSA-6h2x-m376-mqjq — joi isoDate quadratic backtracking