From 66f1ef63547a8c5f45dc2472801d2a973ff997cc Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 10 Mar 2026 23:20:11 +0000 Subject: [PATCH] shr_files fence ftp/sftp; this fixes GHSA-67rw-2x62-mqqm which is the 2nd season of e0a92ba72d46074209a9c304eb2a01ca0429e60c / CVE-2025-58753 since that only fixed the http / https endpoints --- copyparty/authsrv.py | 22 +++++++++++++--------- copyparty/ftpd.py | 4 +++- copyparty/sftpd.py | 6 ++++-- copyparty/smbd.py | 2 +- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index df501181..3decdd7e 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -705,18 +705,22 @@ class VFS(object): if rem: ap += "/" + rem - rap = absreal(ap) + rap = "" if self.shr_files: assert self.shr_src # !rm - vn, rem = self.shr_src - chk = absreal(os.path.join(vn.realpath, rem)) - if chk != rap: - # not the dir itself; assert file allowed - ad, fn = os.path.split(rap) - if chk != ad or fn not in self.shr_files: - return "\n\n" + if rem and rem not in self.shr_files: + return "\n\n" + if resolve: + rap = absreal(ap) + vn, rem = self.shr_src + chk = absreal(os.path.join(vn.realpath, rem)) + if chk != rap: + # not the dir itself; assert file allowed + ad, fn = os.path.split(rap) + if chk != ad or fn not in self.shr_files: + return "\n\n" - return rap if resolve else ap + return (rap or absreal(ap)) if resolve else ap def _dcanonical_shr(self, rem: str) -> str: """resolves until the final component (filename)""" diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index e5d8ff9e..072a3518 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -200,11 +200,13 @@ class FtpFs(AbstractedFS): cr, cw, cm, cd, _, _, _, _, _ = avfs.uaxs[self.h.uname] if r and not cr or w and not cw or m and not cm or d and not cd: raise FSE(t.format(vpath), 1) + else: + ap = vfs.canonical(rem, False) if "bcasechk" in vfs.flags and not vfs.casechk(rem, True): raise FSE("No such file or directory", 1) - return os.path.join(vfs.realpath, rem), vfs, rem + return ap, vfs, rem except Pebkac as ex: raise FSE(str(ex)) diff --git a/copyparty/sftpd.py b/copyparty/sftpd.py index fc1ce7f5..e4f61e4a 100644 --- a/copyparty/sftpd.py +++ b/copyparty/sftpd.py @@ -349,11 +349,13 @@ class SFTP_Srv(paramiko.SFTPServerInterface): cr, cw, cm, cd, _, _, _, _, _ = avn.uaxs[self.uname] if r and not cr or w and not cw or m and not cm or d and not cd: raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,)) + else: + ap = vn.canonical(rem, False) if "bcasechk" in vn.flags and not vn.casechk(rem, True): raise OSError(errno.ENOENT, "file does not exist case-sensitively") - return os.path.join(vn.realpath, rem), vn, rem + return ap, vn, rem def list_folder(self, path: str) -> list[SATTR] | int: try: @@ -484,7 +486,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface): try: vn, rem = self.asrv.vfs.get(vp, self.uname, rd, wr) - ap = os.path.join(vn.realpath, rem) + ap = vn.canonical(rem, False) vf = vn.flags except Pebkac as ex: t = "denied open file [%s], iflag=%s, read=%s, write=%s: %s" diff --git a/copyparty/smbd.py b/copyparty/smbd.py index 4d3d1996..e0ca920c 100644 --- a/copyparty/smbd.py +++ b/copyparty/smbd.py @@ -191,7 +191,7 @@ class SMB(object): vfs, rem = self.asrv.vfs.get(vpath, uname, *perms) if not vfs.realpath: raise Exception("unmapped vfs") - return vfs, vjoin(vfs.realpath, rem) + return vfs, vfs.canonical(rem, False) def _listdir(self, vpath: str, *a: Any, **ka: Any) -> list[str]: vpath = vpath.replace("\\", "/").lstrip("/")