Serious path traversal hits the SCP middleware in charm.land/wish/v2. Attackers using a malicious SCP client read arbitrary files, write files anywhere, and create directories outside your configured root. This affects all versions up to commit 72d67e6 on main branch, released as recently as this month. The older github.com/charmbracelet/wish v1 likely suffers the same flaw due to identical code.
Wish powers SSH servers and apps in Go, built by Charmbracelet—a team known for polished TUI tools like Bubble Tea. Their libraries rack up thousands of GitHub stars: soft-serve (2.5k+), wish itself around 1k. Developers use it for custom SSH services, including file transfer demos. If your server runs wish’s scp.Middleware with scp.NewFileSystemHandler, assume compromise if exposed to untrusted SCP clients.
Why this matters: SCP often runs with elevated privileges on fileservers or internal tools. An attacker with network access pwns the host filesystem—no auth bypass needed, just a crafted scp command. In production, this escalates to data theft, ransomware drop, or persistence. We’ve seen similar bugs burn orgs before; OpenSSH patched SCP traversals years ago for a reason.
Root Cause
The bug lives in fileSystemHandler.prefixed() at scp/filesystem.go:42-48. It cleans paths with filepath.Clean() but skips checking if the result stays inside the root directory. Here’s the flawed code:
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)
}
Clean() resolves ../ but doesn’t block them. Join() then lets attackers like “../../../../etc/passwd” slip out. Fair point: Go’s filepath package prioritizes usability over security by default—developers must add chroot-like checks themselves.
Attack Vectors
Vector 1: Arbitrary writes via scp -t (receive mode). Client sends filenames parsed by lax regexes:
reNewFile = regexp.MustCompile(`^C(\d{4}) (\d+) (.*)$`)
reNewFolder = regexp.MustCompile(`^D(\d{4}) 0 (.*)$`)
No sanitization before filepath.Join(root, name), feeding prefixed(). Result: mkdir and writes anywhere the process can touch.
Vector 2: Arbitrary reads via scp -f (send mode). SSH args pass directly to prefixed() via Glob(), NewFileEntry(), NewDirEntry(). Attackers glob “/etc/shadow” or any readable file.
Vector 3: Enumeration. User-supplied globs (*, ?, []) hit filepath.Glob() post-prefix, leaking directory listings outside root.
Researcher validated all with integration tests against a real wish SSH server on port 2222, using the official examples/scp setup:
handler := scp.NewFileSystemHandler("/srv/data")
s, _ := wish.NewServer(
wish.WithAddress(net.JoinHostPort("0.0.0.0", "2222")),
wish.WithMiddleware(scp.Middleware(handler, handler)),
)
Fix and Next Steps
No patch yet—main branch stays vulnerable at 72d67e6. Charmbracelet moves fast; watch their repo. Until fixed, disable SCP middleware or run in a chroot/jail. Skeptical take: Wish targets devs building toys or internal tools, not battle-hardened servers. But if you’re shipping this exposed, rip it out now.
Mitigate broadly: Firewall SCP (port 22 typically), restrict to trusted nets, audit processes for wish binaries. Scan deps with tools like go mod graph or Trivy. Test your setup: scp -f “../../../../etc/passwd” user@localhost:2222—if it works, you’re owned.
Bigger picture: Go SSH libs like wish fill a gap—golang.org/x/crypto/ssh is low-level. Popularity invites scrutiny. This vuln echoes classic oversights; always validate paths stay in bounds. Devs, prefix with filepath.Clean() then os.DirFS(root).Open() or manual WalkDir checks. Users, patch fast—filesystem R/W is game over.