Files
rclone/cmd/serve/sftp/handler.go
T
Nick Craig-Wood 50b4d4c745 serve sftp: don't crash the whole server on a bad request GHSA-6jcg-q3wp-x2f4
Three ways a single client could deny service to everyone else connected to
the same serve sftp process:

A session "subsystem" request payload is a length-prefixed string, but it was
sliced at a fixed offset without checking its length, so a client sending a
truncated payload panicked the out-of-band request goroutine and killed the
process. Decode the payload instead, the way the neighbouring "exec" request
already does.

Rejecting a request then left the goroutine handling that channel waiting
forever to learn what kind of channel it was, because nothing was ever sent
on the channel it waits on. A client could open channels in a loop making
unsupported requests and grow the server's goroutines and memory without
bound. Signal the waiter when the requests run out so the channel is torn
down.

Separately, the request handlers - and reads, writes and closes on the file
handles they return - run on pkg/sftp packet worker goroutines which have no
panic recovery. A panic raised by a backend while serving one request took the
process down with it. Recover panics at that boundary, log them with a stack
trace, and return them to the requesting client as an error instead.

Addresses GHSA-6jcg-q3wp-x2f4.
2026-07-31 13:21:59 +01:00

232 lines
5.1 KiB
Go

//go:build !plan9
package sftp
import (
"fmt"
"io"
"os"
"runtime/debug"
"syscall"
"time"
"github.com/pkg/sftp"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/vfs"
)
// recoverPanic turns a panic into an error assigned through err.
func recoverPanic(err *error) {
if r := recover(); r != nil {
fs.Errorf("sftp", "panic in request handler: %v\n%s", r, debug.Stack())
*err = fmt.Errorf("request handler: %v", r)
}
}
// recoveringHandle wraps a vfs.Handle so panics in ReadAt and WriteAt,
// which pkg/sftp calls from its packet worker goroutines, are returned
// as errors.
type recoveringHandle struct {
vfs.Handle
}
func (h recoveringHandle) ReadAt(b []byte, off int64) (n int, err error) {
defer recoverPanic(&err)
return h.Handle.ReadAt(b, off)
}
func (h recoveringHandle) WriteAt(b []byte, off int64) (n int, err error) {
defer recoverPanic(&err)
return h.Handle.WriteAt(b, off)
}
func (h recoveringHandle) Close() (err error) {
defer recoverPanic(&err)
return h.Handle.Close()
}
// vfsHandler converts the VFS to be served by SFTP
type vfsHandler struct {
*vfs.VFS
}
// newVFSHandler returns a Handlers object with the test handlers.
func newVFSHandler(vfs *vfs.VFS) sftp.Handlers {
v := vfsHandler{VFS: vfs}
return sftp.Handlers{
FileGet: v,
FilePut: v,
FileCmd: v,
FileList: v,
}
}
func (v vfsHandler) Fileread(r *sftp.Request) (ra io.ReaderAt, err error) {
defer recoverPanic(&err)
file, err := v.OpenFile(r.Filepath, os.O_RDONLY, 0777)
if err != nil {
return nil, err
}
return recoveringHandle{file}, nil
}
func (v vfsHandler) Filewrite(r *sftp.Request) (wa io.WriterAt, err error) {
defer recoverPanic(&err)
// Respect the flags requested in the SFTP OPEN packet
p := r.Pflags()
flags := os.O_WRONLY
if p.Append {
flags |= os.O_APPEND
}
if p.Creat {
flags |= os.O_CREATE
}
if p.Trunc {
flags |= os.O_TRUNC
}
if p.Excl {
flags |= os.O_EXCL
}
file, err := v.OpenFile(r.Filepath, flags, 0777)
if err != nil {
return nil, err
}
return recoveringHandle{file}, nil
}
func (v vfsHandler) Filecmd(r *sftp.Request) (err error) {
defer recoverPanic(&err)
switch r.Method {
case "Setstat":
attr := r.Attributes()
flags := r.AttrFlags()
// A size attribute is a request to truncate the file
if flags.Size {
node, err := v.Stat(r.Filepath)
if err != nil {
return err
}
if err := node.Truncate(int64(attr.Size)); err != nil {
return err
}
}
if flags.Acmodtime {
atime := time.Unix(int64(attr.Atime), 0)
mtime := time.Unix(int64(attr.Mtime), 0)
err := v.Chtimes(r.Filepath, atime, mtime)
if err != nil {
return err
}
}
return nil
case "Rename":
err := v.Rename(r.Filepath, r.Target)
if err != nil {
return err
}
case "Rmdir", "Remove":
err := v.Remove(r.Filepath)
if err != nil {
return err
}
case "Mkdir":
err := v.Mkdir(r.Filepath, 0777)
if err != nil {
return err
}
case "Symlink":
// FIXME
// _, err := v.fetch(r.Filepath)
// if err != nil {
// return err
// }
// link := newMemFile(r.Target, false)
// link.symlink = r.Filepath
// v.files[r.Target] = link
return sftp.ErrSshFxOpUnsupported
case "Link":
return sftp.ErrSshFxOpUnsupported
default:
return sftp.ErrSshFxOpUnsupported
}
return nil
}
// StatVFS implements the statvfs@openssh.com extension, returning filesystem
// usage information from the VFS. It satisfies sftp.StatVFSFileCmder.
func (v vfsHandler) StatVFS(r *sftp.Request) (st *sftp.StatVFS, err error) {
defer recoverPanic(&err)
const blockSize = 4096
total, _, free := v.Statfs()
blocks := uint64(total) / blockSize
bfree := uint64(free) / blockSize
return &sftp.StatVFS{
Bsize: blockSize,
Frsize: blockSize,
Blocks: blocks,
Bfree: bfree,
Bavail: bfree,
Files: 1e9, // total file inodes - made up as the VFS has no concept of these
Ffree: 1e9, // free file inodes
Favail: 1e9, // free file inodes for non-root
Namemax: 255, // maximum filename length
}, nil
}
type listerat []os.FileInfo
// Modeled after strings.Reader's ReadAt() implementation
func (f listerat) ListAt(ls []os.FileInfo, offset int64) (n int, err error) {
defer recoverPanic(&err)
if offset >= int64(len(f)) {
return 0, io.EOF
}
n = copy(ls, f[offset:])
if n < len(ls) {
return n, io.EOF
}
return n, nil
}
func (v vfsHandler) Filelist(r *sftp.Request) (l sftp.ListerAt, err error) {
defer recoverPanic(&err)
var node vfs.Node
var handle vfs.Handle
switch r.Method {
case "List":
node, err = v.Stat(r.Filepath)
if err != nil {
return nil, err
}
if !node.IsDir() {
return nil, syscall.ENOTDIR
}
handle, err = node.Open(os.O_RDONLY)
if err != nil {
return nil, err
}
defer fs.CheckClose(handle, &err)
fis, err := handle.Readdir(-1)
if err != nil {
return nil, err
}
return listerat(fis), nil
case "Stat":
node, err = v.Stat(r.Filepath)
if err != nil {
return nil, err
}
return listerat([]os.FileInfo{node}), nil
case "Readlink":
// FIXME
// if file.symlink != "" {
// file, err = v.fetch(file.symlink)
// if err != nil {
// return nil, err
// }
// }
// return listerat([]os.FileInfo{file}), nil
}
return nil, sftp.ErrSshFxOpUnsupported
}