GitHub Security Advisory: GHSA-xjvp-7243-rg9h
Package: github.com/charmbracelet/wish (GO)
Severity: CRITICAL (CVSS 9.6)
CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Vulnerable versions: <= 1.4.7
Patched in: unpatched

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

Details:
## 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.Wi