Limit prehook executable paths to the prehooks dir

This commit is contained in:
Martin Hebnes Pedersen 2026-04-19 17:33:10 +02:00
parent ba089e05dd
commit 6592fcf937
3 changed files with 48 additions and 22 deletions

View file

@ -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

View file

@ -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.

View file

@ -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