From 6592fcf93712d50952729891f78aac0edc40ce3d Mon Sep 17 00:00:00 2001 From: Martin Hebnes Pedersen Date: Sun, 19 Apr 2026 17:33:10 +0200 Subject: [PATCH] Limit prehook executable paths to the prehooks dir --- app/connect.go | 29 ++++++++++++++------------- cli/connect.go | 2 +- internal/prehook/prehook.go | 39 ++++++++++++++++++++++++++++++------- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/app/connect.go b/app/connect.go index a066812..65bf029 100644 --- a/app/connect.go +++ b/app/connect.go @@ -167,9 +167,14 @@ func (a *App) Connect(connectStr string) (success bool) { a.dialing = url a.websocketHub.UpdateStatus() - if exec := url.Params.Get("prehook"); exec != "" { - if err := prehook.Verify(exec); err != nil { - log.Printf("prehook invalid: %s", err) + prehookScript := prehook.Script{ + Dir: a.options.PrehooksPath, + File: url.Params.Get("prehook"), + Args: url.Params["prehook-arg"], + } + if prehookScript.File != "" { + if err := prehookScript.VerifyFile(); err != nil { + log.Printf("invalid prehook: %s", err) return } } @@ -192,19 +197,15 @@ func (a *App) Connect(connectStr string) (success bool) { return } - if exec := url.Params.Get("prehook"); exec != "" { + if prehookScript.File != "" { log.Println("Running prehook...") - script := prehook.Script{ - File: exec, - Args: url.Params["prehook-arg"], - Env: append([]string{ - buildinfo.AppName + "_DIAL_URL=" + connectStr, - buildinfo.AppName + "_REMOTE_ADDR=" + conn.RemoteAddr().String(), - buildinfo.AppName + "_LOCAL_ADDR=" + conn.LocalAddr().String(), - }, append(os.Environ(), a.Env()...)...), - } + prehookScript.Env = append([]string{ + buildinfo.AppName + "_DIAL_URL=" + connectStr, + buildinfo.AppName + "_REMOTE_ADDR=" + conn.RemoteAddr().String(), + buildinfo.AppName + "_LOCAL_ADDR=" + conn.LocalAddr().String(), + }, append(os.Environ(), a.Env()...)...) conn = prehook.Wrap(conn) - if err := script.Execute(ctx, conn); err != nil { + if err := prehookScript.Execute(ctx, conn); err != nil { conn.Close() log.Printf("Prehook script failed: %s", err) return diff --git a/cli/connect.go b/cli/connect.go index 0fd9def..9945dad 100644 --- a/cli/connect.go +++ b/cli/connect.go @@ -50,7 +50,7 @@ params: ?freq= Sets QSY frequency (ardop and ax25 only) ?host= Overrides the host part of the path. Useful for serial-tnc to specify e.g. /dev/ttyS0. ?prehook= Sets an executable middleware to run before the connection is handed over to the B2F protocol. - The executable must be given as full path, or a file located in $PATH or {CONFIG_DIR}/prehooks/. + The executable must be located in {CONFIG_DIR}/prehooks/. Received packets are forwarded to STDIN. Data written to STDOUT forwarded to the remote node. Additional arguments can be passed with one or more &prehook-arg=. Environment variables describing the dialed connection are provided. diff --git a/internal/prehook/prehook.go b/internal/prehook/prehook.go index 6564d1f..9c9b751 100644 --- a/internal/prehook/prehook.go +++ b/internal/prehook/prehook.go @@ -12,9 +12,11 @@ import ( "net" "os" "os/exec" + "path/filepath" "time" "github.com/la5nta/pat/internal/debug" + "github.com/la5nta/pat/internal/directories" "golang.org/x/sync/errgroup" ) @@ -22,6 +24,7 @@ var ErrConnNotWrapped = errors.New("connection not wrapped for prehook") type Script struct { File string + Dir string Args []string Env []string } @@ -41,13 +44,31 @@ type Conn struct { br *bufio.Reader } -// Verify returns nil if the given script file is found and valid. -func Verify(file string) error { - _, err := exec.LookPath(file) - if errors.Is(err, exec.ErrDot) { - err = nil +// VerifyFile returns nil if the given script file is found and valid. +func (s Script) VerifyFile() error { + p, err := s.Path() + if err != nil { + return err } - return err + info, err := os.Stat(p) + if err != nil { + return err + } + if info.IsDir() { + return fmt.Errorf("%s: is a directory", s.File) + } + if _, err := exec.LookPath(p); err != nil { + return err + } + return nil +} + +func (s Script) Path() (string, error) { + p := filepath.Join(s.Dir, s.File) + if !directories.IsInPath(s.Dir, p) { + return "", fmt.Errorf("%s: escapes base path", p) + } + return p, nil } // Wrap returns a wrapped connection with the ability to execute a prehook. @@ -67,7 +88,11 @@ func (p *Conn) Read(b []byte) (int, error) { return p.br.Read(b) } // Execute executes the prehook script, returning nil if the process // terminated successfully (exit code 0). func (p *Conn) Execute(ctx context.Context, script Script) error { - cmd := exec.CommandContext(ctx, script.File, script.Args...) + name, err := script.Path() + if err != nil { + return err + } + cmd := exec.CommandContext(ctx, name, script.Args...) cmd.Env = script.Env cmd.Stderr = os.Stderr cmd.Stdout = p.Conn