Scharfes S, Sharp Claws: Breaking Node-Tar with Unicode Ligatures
Jan 21, 2026·6 min read·1 visit
Executive Summary (TL;DR)
Node-tar's locking mechanism failed to account for macOS filesystem quirks where characters like 'ß' and 'ss' are identical. This allowed attackers to bypass path reservations, leading to a race condition where a malicious symlink could be swapped in during file extraction, causing arbitrary file overwrites.
A high-severity race condition in the popular `node-tar` library allows arbitrary file overwrites on macOS systems. The vulnerability stems from a mismatch between how the library normalizes Unicode path names and how the APFS filesystem handles character ligatures (like the German 'ß'), bypassing concurrency safeguards.
The Hook: When File Systems Gaslight Developers
In the world of secure software development, the filesystem is not your friend. It is a chaotic, legacy-ridden beast that lies to you about what files exist, where they are, and what they are named. This is especially true when dealing with the unholy trinity of file extraction: symlinks, concurrency, and Unicode.
node-tar is the backbone of the Node.js ecosystem. It’s the engine that unpacks your node_modules every time you run npm install. Because installing dependencies is slow, node-tar tries to be fast. It uses a concurrency model controlled by the jobs option to extract multiple files at once. To prevent the obvious chaos of multiple threads writing to the same file, it implements a system called PathReservations. Ideally, this acts like a bouncer, ensuring that if Thread A is working on /app/config, Thread B has to wait.
But here's the catch: Logic that works perfectly on Linux (ext4) often falls apart on macOS (APFS/HFS+). While Linux is case-sensitive and literal, macOS is "helpful." It treats Hello.txt and hello.txt as the same file. Even worse, it treats certain Unicode ligatures—combined characters like the German Eszett (ß) or the ligature ff—as identical to their expanded ASCII counterparts (ss and ff). CVE-2026-23950 is the story of how that "helpfulness" allowed attackers to walk right past the bouncer.
The Flaw: A Tale of Two Normalizations
The root cause of this vulnerability is a classic Time-of-Check Time-of-Use (TOCTOU) race condition, enabled by a logic error in string normalization. The PathReservations system relies on a Map to track which file paths are currently being written to. To check if a path is busy, node-tar generates a cache key from the filename.
Prior to version 7.5.4, the library generated this key by stripping the path of relative dots (..) and applying Unicode Normalization Form D (NFD). The code looked something like this: path.normalize('NFD').toLowerCase(). The developer's assumption was that this would catch all variations of a filename. If you try to extract Foo.txt and foo.txt concurrently, they normalize to the same key, and the second job waits.
However, the NFD normalization form does not decompose the German ß into ss. In the eyes of node-tar, ß-secret.txt and ss-secret.txt were two completely different files. They generated different reservation keys (locks). Consequently, node-tar allowed operations on both files to proceed in parallel.
But the macOS kernel (XNU) disagrees. To APFS, ß and ss are the same inode. By creating a tarball with both names, an attacker could trigger two parallel threads targeting the exact same physical location on the disk, completely bypassing the library's internal locking mechanism.
The Code: shaking It Off
The fix required a fundamental change in how node-tar views strings. Simply lowercasing a string isn't enough when dealing with aggressive filesystem normalization. The patch introduces a "shake out" technique—forcing the string to expand and contract to catch these edge cases.
Here is the diff that closed the hole. Note specifically the sequence of operations:
// src/normalize-unicode.ts
export const normalizeUnicode = (s: string): string => {
return s
.normalize('NFD')
.toLocaleLowerCase('en')
// The magic fix: round-tripping through UpperCase forces expansion.
// 'ß'.toLocaleUpperCase() becomes 'SS'.
.toLocaleUpperCase('en')
}[!NOTE] Why this works: The
.toLocaleUpperCase('en')method is aggressive. It knows that the uppercase version of ß is SS. By converting everything to uppercase after decomposition, the library ensures that both the ligatureßand the ASCII sequencessresolve to the genericSS. The collision is detected, the lock is shared, and the race condition is neutralized.
The Exploit: Symlink Poisoning
To exploit this, we don't need memory corruption; we just need precise timing. The goal is Arbitrary File Overwrite. We want to trick node-tar into writing a file to a sensitive location (e.g., ~/.ssh/authorized_keys) by abusing a symlink.
Here is the attack chain:
-
Craft the Archive: Create a tarball with two entries.
- Entry A: A Symlink named
ßpointing to~/.ssh/. - Entry B: A File named
sscontaining the attacker's public key.
- Entry A: A Symlink named
-
The Setup: The victim runs
tar.extractwithjobs: 2(or higher) on a Mac. -
The Race:
node-tarseesßandssas different. It grants locks for both immediately.- Thread 1 (Entry A) begins creating the symlink
ß->~/.ssh/. - Thread 2 (Entry B) performs directory traversal checks for
ss. It checks: "Issssafe? Does the parent exist?" Since the symlink hasn't been flushed to disk yet (or the check logic ignores the parallel operation), the check passes.
-
The Switch:
- Thread 1 finishes. The filesystem now contains a symlink at
./ß(which is also./ss). - Thread 2 proceeds to the write phase. It opens
./ssfor writing.
- Thread 1 finishes. The filesystem now contains a symlink at
-
The Impact: Because APFS resolves
./ssto the symlink created by Thread 1, the write operation follows the link. The attacker's public key is written to~/.ssh/authorized_keys(or the file inside the directory depending on exact path construction). Game over.
The Impact: Why Should We Panic?
While this vulnerability requires specific conditions (macOS, jobs > 1, extracting untrusted tarballs), the context of node-tar makes it critical. This library isn't just a utility; it is the installer for the Node.js universe.
If a package manager or build tool uses a vulnerable version of node-tar to unpack a malicious npm package (a supply chain attack vector), the attacker gains the ability to overwrite any file the user has write access to. In a CI/CD environment, this often means overwriting build scripts, injecting backdoors into the resulting artifacts, or stealing secrets by overwriting configuration files to point to external loggers.
Because the attack relies on filesystem behavior (APFS/HFS+), it is invisible to standard static analysis tools that look for buffer overflows or prototype pollution. It is a logic flaw pure and simple, and those are always the hardest to catch.
The Fix: Mitigation Strategies
The immediate fix is to upgrade node-tar to version 7.5.4. This version includes the improved Unicode normalization logic that correctly identifies ligatures as collisions.
If you cannot upgrade immediately, you must disable concurrent extraction or filter out symlinks. Since the race condition depends on two threads operating simultaneously, setting jobs: 1 effectively mitigates the issue, albeit at the cost of performance.
Alternatively, enforce strict input validation. If your application extracts tarballs from untrusted sources, use the filter option to reject SymbolicLink entries entirely until the patch is applied. Remember: trusting user input is bad, but trusting user input that interacts with the filesystem is suicidal.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:H/A:LAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
node-tar isaacs | <= 7.5.3 | 7.5.4 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-367 (TOCTOU Race Condition) |
| Secondary CWE | CWE-176 (Improper Handling of Unicode Encoding) |
| CVSS | 8.8 (High) |
| Attack Vector | Network (Malicious Tarball) |
| Platform | macOS / Node.js |
| EPSS Score | 0.00014 (Low Probability) |
MITRE ATT&CK Mapping
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.