Browse Source

fix: SFTP HomeDir path translation for user operations

When users have a non-root HomeDir (e.g., '/sftp/user'), their SFTP
operations should be relative to that directory. Previously, when a
user uploaded to '/' via SFTP, the path was not translated to their
home directory, causing 'permission denied for / for permission write'.

This fix adds a toAbsolutePath() method that implements chroot-like
behavior where the user's HomeDir becomes their root. All file and
directory operations now translate paths through this method.

Example: User with HomeDir='/sftp/user' uploading to '/' now correctly
maps to '/sftp/user'.

Fixes: https://github.com/seaweedfs/seaweedfs/issues/7470
fix/sftp-homedir-path-translation
chrislu 15 hours ago
parent
commit
cd0f6aba8b
  1. 5
      weed/sftpd/sftp_file_writer.go
  2. 62
      weed/sftpd/sftp_filer.go
  3. 27
      weed/sftpd/sftp_server.go

5
weed/sftpd/sftp_file_writer.go

@ -72,6 +72,7 @@ func (l listerat) ListAt(ls []os.FileInfo, offset int64) (int, error) {
type SeaweedSftpFileWriter struct { type SeaweedSftpFileWriter struct {
fs SftpServer fs SftpServer
req *sftp.Request req *sftp.Request
absPath string // Absolute path after HomeDir translation
mu sync.Mutex mu sync.Mutex
tmpFile *os.File tmpFile *os.File
permissions os.FileMode permissions os.FileMode
@ -105,6 +106,6 @@ func (w *SeaweedSftpFileWriter) Close() error {
return err return err
} }
// Stream the file instead of loading it
return w.fs.putFile(w.req.Filepath, w.tmpFile, w.fs.user)
// Stream the file to the absolute path (after HomeDir translation)
return w.fs.putFile(w.absPath, w.tmpFile, w.fs.user)
} }

62
weed/sftpd/sftp_filer.go

@ -100,18 +100,20 @@ func (fs *SftpServer) withTimeoutContext(fn func(ctx context.Context) error) err
// ==================== Command Dispatcher ==================== // ==================== Command Dispatcher ====================
func (fs *SftpServer) dispatchCmd(r *sftp.Request) error { func (fs *SftpServer) dispatchCmd(r *sftp.Request) error {
glog.V(0).Infof("Dispatch: %s %s", r.Method, r.Filepath)
absPath := fs.toAbsolutePath(r.Filepath)
glog.V(0).Infof("Dispatch: %s %s (absolute: %s)", r.Method, r.Filepath, absPath)
switch r.Method { switch r.Method {
case "Remove": case "Remove":
return fs.removeEntry(r)
return fs.removeEntry(absPath)
case "Rename": case "Rename":
return fs.renameEntry(r)
absTarget := fs.toAbsolutePath(r.Target)
return fs.renameEntry(absPath, absTarget)
case "Mkdir": case "Mkdir":
return fs.makeDir(r)
return fs.makeDir(absPath)
case "Rmdir": case "Rmdir":
return fs.removeDir(r)
return fs.removeDir(absPath)
case "Setstat": case "Setstat":
return fs.setFileStat(r)
return fs.setFileStatWithRequest(absPath, r)
default: default:
return fmt.Errorf("unsupported: %s", r.Method) return fmt.Errorf("unsupported: %s", r.Method)
} }
@ -120,10 +122,11 @@ func (fs *SftpServer) dispatchCmd(r *sftp.Request) error {
// ==================== File Operations ==================== // ==================== File Operations ====================
func (fs *SftpServer) readFile(r *sftp.Request) (io.ReaderAt, error) { func (fs *SftpServer) readFile(r *sftp.Request) (io.ReaderAt, error) {
if err := fs.checkFilePermission(r.Filepath, "read"); err != nil {
absPath := fs.toAbsolutePath(r.Filepath)
if err := fs.checkFilePermission(absPath, "read"); err != nil {
return nil, err return nil, err
} }
entry, err := fs.getEntry(r.Filepath)
entry, err := fs.getEntry(absPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -131,7 +134,8 @@ func (fs *SftpServer) readFile(r *sftp.Request) (io.ReaderAt, error) {
} }
func (fs *SftpServer) newFileWriter(r *sftp.Request) (io.WriterAt, error) { func (fs *SftpServer) newFileWriter(r *sftp.Request) (io.WriterAt, error) {
dir, _ := util.FullPath(r.Filepath).DirAndName()
absPath := fs.toAbsolutePath(r.Filepath)
dir, _ := util.FullPath(absPath).DirAndName()
if err := fs.checkFilePermission(dir, "write"); err != nil { if err := fs.checkFilePermission(dir, "write"); err != nil {
glog.Errorf("Permission denied for %s", dir) glog.Errorf("Permission denied for %s", dir)
return nil, err return nil, err
@ -145,6 +149,7 @@ func (fs *SftpServer) newFileWriter(r *sftp.Request) (io.WriterAt, error) {
return &SeaweedSftpFileWriter{ return &SeaweedSftpFileWriter{
fs: *fs, fs: *fs,
req: r, req: r,
absPath: absPath,
tmpFile: tmpFile, tmpFile: tmpFile,
permissions: 0644, permissions: 0644,
uid: fs.user.Uid, uid: fs.user.Uid,
@ -153,16 +158,16 @@ func (fs *SftpServer) newFileWriter(r *sftp.Request) (io.WriterAt, error) {
}, nil }, nil
} }
func (fs *SftpServer) removeEntry(r *sftp.Request) error {
return fs.deleteEntry(r.Filepath, false)
func (fs *SftpServer) removeEntry(absPath string) error {
return fs.deleteEntry(absPath, false)
} }
func (fs *SftpServer) renameEntry(r *sftp.Request) error {
if err := fs.checkFilePermission(r.Filepath, "rename"); err != nil {
func (fs *SftpServer) renameEntry(absPath, absTarget string) error {
if err := fs.checkFilePermission(absPath, "rename"); err != nil {
return err return err
} }
oldDir, oldName := util.FullPath(r.Filepath).DirAndName()
newDir, newName := util.FullPath(r.Target).DirAndName()
oldDir, oldName := util.FullPath(absPath).DirAndName()
newDir, newName := util.FullPath(absTarget).DirAndName()
return fs.callWithClient(false, func(ctx context.Context, client filer_pb.SeaweedFilerClient) error { return fs.callWithClient(false, func(ctx context.Context, client filer_pb.SeaweedFilerClient) error {
_, err := client.AtomicRenameEntry(ctx, &filer_pb.AtomicRenameEntryRequest{ _, err := client.AtomicRenameEntry(ctx, &filer_pb.AtomicRenameEntryRequest{
OldDirectory: oldDir, OldName: oldName, OldDirectory: oldDir, OldName: oldName,
@ -172,15 +177,15 @@ func (fs *SftpServer) renameEntry(r *sftp.Request) error {
}) })
} }
func (fs *SftpServer) setFileStat(r *sftp.Request) error {
if err := fs.checkFilePermission(r.Filepath, "write"); err != nil {
func (fs *SftpServer) setFileStatWithRequest(absPath string, r *sftp.Request) error {
if err := fs.checkFilePermission(absPath, "write"); err != nil {
return err return err
} }
entry, err := fs.getEntry(r.Filepath)
entry, err := fs.getEntry(absPath)
if err != nil { if err != nil {
return err return err
} }
dir, _ := util.FullPath(r.Filepath).DirAndName()
dir, _ := util.FullPath(absPath).DirAndName()
// apply attrs // apply attrs
if r.AttrFlags().Permissions { if r.AttrFlags().Permissions {
entry.Attributes.FileMode = uint32(r.Attributes().FileMode()) entry.Attributes.FileMode = uint32(r.Attributes().FileMode())
@ -201,18 +206,19 @@ func (fs *SftpServer) setFileStat(r *sftp.Request) error {
// ==================== Directory Operations ==================== // ==================== Directory Operations ====================
func (fs *SftpServer) listDir(r *sftp.Request) (sftp.ListerAt, error) { func (fs *SftpServer) listDir(r *sftp.Request) (sftp.ListerAt, error) {
if err := fs.checkFilePermission(r.Filepath, "list"); err != nil {
absPath := fs.toAbsolutePath(r.Filepath)
if err := fs.checkFilePermission(absPath, "list"); err != nil {
return nil, err return nil, err
} }
if r.Method == "Stat" || r.Method == "Lstat" { if r.Method == "Stat" || r.Method == "Lstat" {
entry, err := fs.getEntry(r.Filepath)
entry, err := fs.getEntry(absPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
fi := &EnhancedFileInfo{FileInfo: FileInfoFromEntry(entry), uid: entry.Attributes.Uid, gid: entry.Attributes.Gid} fi := &EnhancedFileInfo{FileInfo: FileInfoFromEntry(entry), uid: entry.Attributes.Uid, gid: entry.Attributes.Gid}
return listerat([]os.FileInfo{fi}), nil return listerat([]os.FileInfo{fi}), nil
} }
return fs.listAllPages(r.Filepath)
return fs.listAllPages(absPath)
} }
func (fs *SftpServer) listAllPages(dirPath string) (sftp.ListerAt, error) { func (fs *SftpServer) listAllPages(dirPath string) (sftp.ListerAt, error) {
@ -259,18 +265,18 @@ func (fs *SftpServer) fetchDirectoryPage(dirPath, start string) ([]os.FileInfo,
} }
// makeDir creates a new directory with proper permissions. // makeDir creates a new directory with proper permissions.
func (fs *SftpServer) makeDir(r *sftp.Request) error {
func (fs *SftpServer) makeDir(absPath string) error {
if fs.user == nil { if fs.user == nil {
return fmt.Errorf("cannot create directory: no user info") return fmt.Errorf("cannot create directory: no user info")
} }
dir, name := util.FullPath(r.Filepath).DirAndName()
if err := fs.checkFilePermission(r.Filepath, "mkdir"); err != nil {
dir, name := util.FullPath(absPath).DirAndName()
if err := fs.checkFilePermission(absPath, "mkdir"); err != nil {
return err return err
} }
// default mode and ownership // default mode and ownership
err := filer_pb.Mkdir(context.Background(), fs, string(dir), name, func(entry *filer_pb.Entry) { err := filer_pb.Mkdir(context.Background(), fs, string(dir), name, func(entry *filer_pb.Entry) {
mode := uint32(0755 | os.ModeDir) mode := uint32(0755 | os.ModeDir)
if strings.HasPrefix(r.Filepath, fs.user.HomeDir) {
if strings.HasPrefix(absPath, fs.user.HomeDir) {
mode = uint32(0700 | os.ModeDir) mode = uint32(0700 | os.ModeDir)
} }
entry.Attributes.FileMode = mode entry.Attributes.FileMode = mode
@ -288,8 +294,8 @@ func (fs *SftpServer) makeDir(r *sftp.Request) error {
} }
// removeDir deletes a directory. // removeDir deletes a directory.
func (fs *SftpServer) removeDir(r *sftp.Request) error {
return fs.deleteEntry(r.Filepath, false)
func (fs *SftpServer) removeDir(absPath string) error {
return fs.deleteEntry(absPath, false)
} }
func (fs *SftpServer) putFile(filepath string, reader io.Reader, user *user.User) error { func (fs *SftpServer) putFile(filepath string, reader io.Reader, user *user.User) error {

27
weed/sftpd/sftp_server.go

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path"
"time" "time"
"github.com/pkg/sftp" "github.com/pkg/sftp"
@ -37,6 +38,32 @@ func NewSftpServer(filerAddr pb.ServerAddress, grpcDialOption grpc.DialOption, d
} }
} }
// toAbsolutePath translates a user-relative path to an absolute filer path.
// When a user has HomeDir="/sftp/user", their view of "/" maps to "/sftp/user".
// This implements chroot-like behavior where the user's home directory
// becomes their root.
func (fs *SftpServer) toAbsolutePath(userPath string) string {
// If user has root as home directory, no translation needed
if fs.user.HomeDir == "" || fs.user.HomeDir == "/" {
return userPath
}
// Clean the path to normalize it
cleanPath := path.Clean(userPath)
if cleanPath == "." {
cleanPath = "/"
}
// Join the home directory with the user path
// path.Join handles the case where cleanPath starts with "/"
// by treating it as relative to the home directory
if cleanPath == "/" {
return fs.user.HomeDir
}
return path.Join(fs.user.HomeDir, cleanPath)
}
// Fileread is invoked for “get” requests. // Fileread is invoked for “get” requests.
func (fs *SftpServer) Fileread(req *sftp.Request) (io.ReaderAt, error) { func (fs *SftpServer) Fileread(req *sftp.Request) (io.ReaderAt, error) {
return fs.readFile(req) return fs.readFile(req)

Loading…
Cancel
Save