Feb 16, 2026·6 min read·24 visits
esm.sh failed to properly sanitize file paths inside NPM tarballs. Attackers can upload a package containing filenames like `../../evil.js`, causing the server to write files outside the intended directory. This can poison the CDN cache or overwrite server configs.
A critical Path Traversal vulnerability (TarSlip) in the esm.sh CDN allows attackers to escape the extraction directory when processing NPM packages. By crafting a malicious tarball with relative path sequences, an attacker can overwrite arbitrary files on the server, leading to potential Remote Code Execution (RCE) via cache poisoning or configuration tampering.
We all love esm.sh. It’s the magic wand of the modern web—no build steps, just import and go. It acts as a bridge between the chaotic world of NPM and the browser, fetching packages, transpiling them, and serving them up on a silver platter. But here's the thing about magic: it usually relies on a lot of complex plumbing behind the scenes. And in this case, the plumbing involved untrusted tarballs.
At its core, esm.sh is a glorified unzipper. It grabs a compressed file from a registry (like NPM), extracts it to a temporary directory, and processes the contents. This is a classic setup for one of the oldest tricks in the book: the TarSlip attack. If you blindly trust the filenames inside a tar archive, you're asking for trouble. It's the digital equivalent of letting a delivery driver walk into your house and place a package directly into your safe, simply because the label said "To: Safe".
CVE-2026-23644 isn't just a file write bug; it's a potential supply chain nuke. Since esm.sh caches these files to serve to millions of users, poisoning the cache means poisoning every developer and application downstream that relies on that specific package version.
The vulnerability lies in the extractPackageTarball function. The developers made a classic mistake: confusing lexical cleaning with security boundaries. In Go, path.Clean (note the missing file prefix) is designed for URL-like paths. It simplifies strings like a/b/../c into a/c. It does not, however, check if the resulting path actually stays inside a specific directory on the host's filesystem.
Here is the logic flaw in a nutshell: The code attempted to split the filename by the first slash to remove the root folder (usually package/), and then joined it with the destination directory. But what happens if the tar entry is named package/../../../../etc/passwd? The split removes package/, leaving ../../../../etc/passwd. path.Join then happily concatenates this to your pkgDir.
Because the code wasn't verifying the final resolved path against the intended root directory, the operating system (or the filepath package if it were used correctly) would interpret those .. sequences effectively, walking right out of the sandbox. The developer locked the front door but left the walls completely missing.
Let’s look at the vulnerable code pattern found in commit 9d77b88. It uses path (generic) instead of filepath (OS-aware) and relies on weak string manipulation.
The Vulnerable Code:
// Danger: blindly joining paths
_, name := utils.SplitByFirstByte(h.Name, '/')
// path.Clean purely parses the string, it doesn't resolve OS paths
filename := path.Join(pkgDir, path.Clean(name))
file, err := os.Create(filename) // OOPS. We just wrote outside pkgDir.The Fix (Commit c62ab83c589e7b421a0e1376d2a00a4e48161093):
The patch introduces a robust defense-in-depth strategy. They switched to filepath, implemented a strict normalizer, and crucially, added an allowlist for extensions.
// 1. Normalize and strip leading slashes/dots
name := utils.NormalizePathname(h.Name)[1:]
// 2. Strict Extension Allowlist (Whitelisting > Blacklisting)
// Only allow web-safe assets (.js, .css, .json, etc.)
if !assetExts[ext] && !moduleExts[ext] {
continue
}
// 3. Prevent symbolic link attacks
if h.Typeflag != tar.TypeReg {
continue
}
// 4. Secure Join
targetPath := filepath.Join(pkgDir, name)
// (Implicit check: NormalizePathname prevents '..' traversal)This fix is aggressive. By rejecting anything that isn't a regular file (no symlinks!) and enforcing strict extensions, they killed an entire class of attacks, not just the path traversal.
Exploiting this requires a bit of creativity because we want to achieve something meaningful, not just write a junk file. Since esm.sh is a CDN, our goal is Cache Poisoning.
Attack Scenario:
evil-lib. Inside, we craft a tarball manually. We don't use npm pack; we use a script to insert a file header with the name package/../../installed_pkg/index.js.evil-lib to the NPM registry (or a proxy). Then, we curl https://esm.sh/evil-lib. The server pulls our tarball and triggers the extraction logic.../ sequences traverse out of the temp directory for evil-lib and overwrite the index.js of react or lodash stored in the adjacent directory.Now, anyone requesting react from esm.sh gets our backdoor. This is supply chain poisoning at the infrastructure level.
The CVSS score of 7.7 is respectable, but the business impact is catastrophic. For a service like esm.sh, integrity is the product. If users cannot trust that the code served matches the code published on NPM, the service is dead.
Potential Consequences:
.env file or a server config file (e.g., config.json), they can point the database credentials to their own server or change execution flags.If you are running a self-hosted instance of esm.sh, you need to update immediately. The vulnerability was patched in commit c62ab83c589e7b421a0e1376d2a00a4e48161093 (Jan 16, 2026).
For Developers (The Golden Rule of Archives):
Never, ever trust the file paths inside a zip, tar, or any archive format. They are user input. Treat them with the same hostility you treat SQL parameters.
filepath.Abs(destination_dir).// The Safe Way
dest := "/tmp/safe_zone"
pathInArchive := header.Name // e.g., "../../etc/passwd"
// Join them
fullPath := filepath.Join(dest, pathInArchive)
// Check if it starts with dest
if !strings.HasPrefix(fullPath, filepath.Clean(dest) + string(os.PathSeparator)) {
panic("Hacker detected!")
}CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:P| Product | Affected Versions | Fixed Version |
|---|---|---|
esm.sh esm-dev | < 0.0.0-20260116051925-c62ab83c589e | 0.0.0-20260116051925-c62ab83c589e |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) |
| Attack Vector | Network |
| CVSS v4.0 | 7.7 (High) |
| EPSS Score | 0.00061 (Low Probability) |
| Impact | Arbitrary File Write / RCE |
| Exploit Status | PoC Available (TarSlip Standard) |
The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the software does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
An authorization bypass vulnerability in the @better-auth/scim plugin allows authenticated attackers to hijack personal SCIM providers and subsequently perform full account takeovers.
A behavioral mismatch vulnerability (CWE-701) in the Rust-based uutils coreutils implementation of common command-line utilities allows silent data loss. When the --suffix argument is executed without explicit backup flags, the uucore library fails to enter backup mode, silently overwriting target files instead of creating preserving copies as expected under GNU standards.
Open WebUI versions prior to 0.6.6 contain a stored cross-site scripting (XSS) vulnerability that allows low-privileged users to upload malicious HTML files containing arbitrary JavaScript. When viewed by an administrator, the executed script can abuse administrative APIs to register malicious functions, leading to remote code execution on the underlying server host.
A critical Stored Cross-Site Scripting (XSS) vulnerability exists in Open WebUI versions prior to 0.6.6. The vulnerability resides in client-side Markdown rendering, where unvalidated iframe tags containing local API base URLs bypass DOMPurify sanitization. This flaw allows authenticated attackers to steal user session tokens. If an administrative session is compromised, the attacker can leverage the application's native Python execution capabilities ('Functions') to achieve arbitrary Remote Code Execution (RCE) on the hosting server.
CVE-2026-26192 is a high-severity Stored Cross-Site Scripting (XSS) vulnerability in Open WebUI prior to version 0.7.0. Authenticated users can modify chat history metadata to force document citations to render inside an HTML iframe configured with an insecure sandbox policy. By combining 'allow-scripts' and 'allow-same-origin', the sandbox boundary is neutralized. This allows scripts executing within the iframe to access the parent window's DOM, extract sensitive Web UI local storage keys (such as authentication JWTs), and perform state-changing actions on behalf of other users, including administrators.
CVE-2026-26193 is a stored Cross-Site Scripting (XSS) vulnerability in Open WebUI prior to version 0.6.44. The vulnerability arises because the rendering engine hardcodes insecure sandbox options on an iframe component used for response embeds, allowing attackers to execute JavaScript in the parent window origin.