mirror of
https://github.com/tailscale/tailscale.git
synced 2025-05-13 12:28:53 +00:00
ssh/tailssh: chdir to user's homedir when directly running a command (#15351)
Commit 4b525fdda (ssh/tailssh: only chdir incubator process to user's homedir when necessary and possible, 2024-08-16) defers changing the working directory until the incubator process drops its privileges. However, it didn't account for the case where there is no incubator process, because no tailscaled was found on the PATH. In that case, it only intended to run `tailscaled be-child` in the root directory but accidentally ran everything there. Fixes: #15350 Signed-off-by: Simon Law <sfllaw@sfllaw.ca>
This commit is contained in:
parent
0841477743
commit
3c98964065
@ -12,11 +12,13 @@
|
||||
package tailssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"log/syslog"
|
||||
"os"
|
||||
@ -29,6 +31,7 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/pkg/sftp"
|
||||
@ -70,11 +73,36 @@ var maybeStartLoginSession = func(dlogf logger.Logf, ia incubatorArgs) (close fu
|
||||
return nil
|
||||
}
|
||||
|
||||
// tryExecInDir tries to run a command in dir and returns nil if it succeeds.
|
||||
// Otherwise, it returns a filesystem error or a timeout error if the command
|
||||
// took too long.
|
||||
func tryExecInDir(ctx context.Context, dir string) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Assume that the following executables exist, are executable, and
|
||||
// immediately return.
|
||||
var name string
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
windir := os.Getenv("windir")
|
||||
name = filepath.Join(windir, "system32", "doskey.exe")
|
||||
default:
|
||||
name = "/bin/true"
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, name)
|
||||
cmd.Dir = dir
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// newIncubatorCommand returns a new exec.Cmd configured with
|
||||
// `tailscaled be-child ssh` as the entrypoint.
|
||||
//
|
||||
// If ss.srv.tailscaledPath is empty, this method is equivalent to
|
||||
// exec.CommandContext.
|
||||
// If ss.srv.tailscaledPath is empty, this method is almost equivalent to
|
||||
// exec.CommandContext. It will refuse to run in SFTP-mode. It will simulate the
|
||||
// behavior of SSHD when by falling back to the root directory if it cannot run
|
||||
// a command in the user’s home directory.
|
||||
//
|
||||
// The returned Cmd.Env is guaranteed to be nil; the caller populates it.
|
||||
func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, err error) {
|
||||
@ -104,7 +132,35 @@ func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, err
|
||||
loginShell := ss.conn.localUser.LoginShell()
|
||||
args := shellArgs(isShell, ss.RawCommand())
|
||||
logf("directly running %s %q", loginShell, args)
|
||||
return exec.CommandContext(ss.ctx, loginShell, args...), nil
|
||||
cmd = exec.CommandContext(ss.ctx, loginShell, args...)
|
||||
|
||||
// While running directly instead of using `tailscaled be-child`,
|
||||
// do what sshd does by running inside the home directory,
|
||||
// falling back to the root directory it doesn't have permissions.
|
||||
// This can happen if the system has networked home directories,
|
||||
// i.e. NFS or SMB, which enable root-squashing by default.
|
||||
cmd.Dir = ss.conn.localUser.HomeDir
|
||||
err := tryExecInDir(ss.ctx, cmd.Dir)
|
||||
switch {
|
||||
case errors.Is(err, exec.ErrNotFound):
|
||||
// /bin/true might not be installed on a barebones system,
|
||||
// so we assume that the home directory does not exist.
|
||||
cmd.Dir = "/"
|
||||
case errors.Is(err, fs.ErrPermission) || errors.Is(err, fs.ErrNotExist):
|
||||
// Ensure that cmd.Dir is the source of the error.
|
||||
var pathErr *fs.PathError
|
||||
if errors.As(err, &pathErr) && pathErr.Path == cmd.Dir {
|
||||
// If we cannot run loginShell in localUser.HomeDir,
|
||||
// we will try to run this command in the root directory.
|
||||
cmd.Dir = "/"
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
case err != nil:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
lu := ss.conn.localUser
|
||||
@ -178,7 +234,10 @@ func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, err
|
||||
}
|
||||
}
|
||||
|
||||
return exec.CommandContext(ss.ctx, ss.conn.srv.tailscaledPath, incubatorArgs...), nil
|
||||
cmd = exec.CommandContext(ss.ctx, ss.conn.srv.tailscaledPath, incubatorArgs...)
|
||||
// The incubator will chdir into the home directory after it drops privileges.
|
||||
cmd.Dir = "/"
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
var debugIncubator bool
|
||||
@ -777,7 +836,6 @@ func (ss *sshSession) launchProcess() error {
|
||||
}
|
||||
|
||||
cmd := ss.cmd
|
||||
cmd.Dir = "/"
|
||||
cmd.Env = envForUser(ss.conn.localUser)
|
||||
for _, kv := range ss.Environ() {
|
||||
if acceptEnvPair(kv) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user