Version in base suite: 3.7.3-1.1 Base version: sshfs-fuse_3.7.3-1.1 Target version: sshfs-fuse_3.7.3-1.2~deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/s/sshfs-fuse/sshfs-fuse_3.7.3-1.1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/s/sshfs-fuse/sshfs-fuse_3.7.3-1.2~deb13u1.dsc changelog | 17 + patches/add-contain_symlinks-option-to-prevent-symlink-escap.patch | 160 ++++++++++ patches/reject-hostname-option-injection-via-bracketed-mount.patch | 136 ++++++++ patches/series | 2 4 files changed, 315 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpc04nhppi/sshfs-fuse_3.7.3-1.1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpc04nhppi/sshfs-fuse_3.7.3-1.2~deb13u1.dsc: no acceptable signature found diff -Nru sshfs-fuse-3.7.3/debian/changelog sshfs-fuse-3.7.3/debian/changelog --- sshfs-fuse-3.7.3/debian/changelog 2023-02-07 19:33:53.000000000 +0000 +++ sshfs-fuse-3.7.3/debian/changelog 2026-06-02 11:11:01.000000000 +0000 @@ -1,3 +1,20 @@ +sshfs-fuse (3.7.3-1.2~deb13u1) trixie; urgency=medium + + * Non-maintainer upload. + * Rebuild for trixie + + -- Salvatore Bonaccorso Tue, 02 Jun 2026 13:11:01 +0200 + +sshfs-fuse (3.7.3-1.2) unstable; urgency=high + + * Non-maintainer upload. + * add contain_symlinks option to prevent symlink escape attacks + (CVE-2026-47187) (Closes: #1138293) + * reject hostname option injection via bracketed mount source (CVE-2026-48711) + (Closes: #1138293) + + -- Salvatore Bonaccorso Sat, 30 May 2026 17:20:39 +0200 + sshfs-fuse (3.7.3-1.1) unstable; urgency=high * Non-maintainer upload. diff -Nru sshfs-fuse-3.7.3/debian/patches/add-contain_symlinks-option-to-prevent-symlink-escap.patch sshfs-fuse-3.7.3/debian/patches/add-contain_symlinks-option-to-prevent-symlink-escap.patch --- sshfs-fuse-3.7.3/debian/patches/add-contain_symlinks-option-to-prevent-symlink-escap.patch 1970-01-01 00:00:00.000000000 +0000 +++ sshfs-fuse-3.7.3/debian/patches/add-contain_symlinks-option-to-prevent-symlink-escap.patch 2026-06-02 11:10:49.000000000 +0000 @@ -0,0 +1,160 @@ +From: Abhinav Agarwal +Date: Sun, 17 May 2026 01:27:17 -0700 +Subject: add contain_symlinks option to prevent symlink escape attacks +Origin: https://github.com/libfuse/sshfs/commit/bcd132f17ccf1b8592a229df797c9b08883fec26 +Bug: https://github.com/libfuse/sshfs/pull/361 +Bug-Debian: https://bugs.debian.org/1138293 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2026-47187 + +A malicious SFTP server can return symlink targets that the local +kernel VFS resolves outside the mount root, enabling local file reads +or writes through ordinary operations like cp following a symlink. + +Add a contain_symlinks option (default on) that rejects absolute +symlink targets and any target containing a `..` component, returning +EPERM. Users who need legacy pass-through for trusted servers can opt +out with -o no_contain_symlinks. + +The check is purely lexical and deliberately strict: in an adversarial +filesystem the server controls intermediate path components, so any +non-`..` component could be a symlink anywhere, making lexical depth +tracking unreliable. Rejecting absolute and any `..` is the simplest +rule that is provably complete against the threat model. + +transform_symlinks composes poorly with containment because transformed +results often contain `..`; a warning is emitted when both are enabled. + +Tests cover default-on containment (readlink + open/stat traversal), +opt-out behavior, transform_symlinks interaction (both arms), and +option precedence. +--- + sshfs.c | 50 +++++++++++++ + sshfs.rst | 15 ++++ + test/test_sshfs.py | 170 +++++++++++++++++++++++++++++++++++++++++++++ + 3 files changed, 235 insertions(+) + +--- a/sshfs.c ++++ b/sshfs.c +@@ -312,6 +312,7 @@ struct sshfs { + int fstat_workaround; + int createmode_workaround; + int transform_symlinks; ++ int contain_symlinks; + int follow_symlinks; + int no_check_root; + int detect_uid; +@@ -493,6 +494,8 @@ static struct fuse_opt sshfs_opts[] = { + SSHFS_OPT("sshfs_verbose", verbose, 1), + SSHFS_OPT("reconnect", reconnect, 1), + SSHFS_OPT("transform_symlinks", transform_symlinks, 1), ++ SSHFS_OPT("contain_symlinks", contain_symlinks, 1), ++ SSHFS_OPT("no_contain_symlinks", contain_symlinks, 0), + SSHFS_OPT("follow_symlinks", follow_symlinks, 1), + SSHFS_OPT("no_check_root", no_check_root, 1), + SSHFS_OPT("password_stdin", password_stdin, 1), +@@ -2104,6 +2107,36 @@ static void strip_common(const char **sp + } while ((*s == *t && *s) || (!*s && *t == '/') || (*s == '/' && !*t)); + } + ++/* ++ * Reject symlink targets that could escape the mount root: absolute ++ * paths and any target containing a ".." component. Returns 1 if ++ * the target is safe to expose to the kernel, 0 otherwise. ++ */ ++static int symlink_target_is_contained(const char *target) ++{ ++ const char *p = target; ++ ++ if (*p == '/') ++ return 0; ++ ++ while (*p) { ++ const char *comp = p; ++ ++ while (*p && *p != '/') ++ p++; ++ /* ++ * Reject any ".." rather than try to normalize: in an ++ * adversarial filesystem the server controls intermediate ++ * components, so lexical normalization cannot be trusted. ++ */ ++ if (p - comp == 2 && comp[0] == '.' && comp[1] == '.') ++ return 0; ++ while (*p == '/') ++ p++; ++ } ++ return 1; ++} ++ + static void transform_symlink(const char *path, char **linkp) + { + const char *l = *linkp; +@@ -2168,6 +2201,13 @@ static int sshfs_readlink(const char *pa + buf_get_string(&name, &link) != -1) { + if (sshfs.transform_symlinks) + transform_symlink(path, &link); ++ if (sshfs.contain_symlinks && ++ !symlink_target_is_contained(link)) { ++ free(link); ++ buf_free(&name); ++ buf_free(&buf); ++ return -EPERM; ++ } + strncpy(linkbuf, link, size - 1); + linkbuf[size - 1] = '\0'; + free(link); +@@ -3641,6 +3681,9 @@ static void usage(const char *progname) + " -o passive communicate over stdin and stdout bypassing network\n" + " -o disable_hardlink link(2) will return with errno set to ENOSYS\n" + " -o transform_symlinks transform absolute symlinks to relative\n" ++" -o contain_symlinks reject absolute symlinks and symlinks containing ..\n" ++" (enabled by default; disable with no_contain_symlinks)\n" ++" -o no_contain_symlinks allow all symlink targets including absolute and ..\n" + " -o follow_symlinks follow symlinks on the server\n" + " -o no_check_root don't check for existence of 'dir' on server\n" + " -o password_stdin read password from stdin (only for pam_mount!)\n" +@@ -4187,6 +4230,7 @@ int main(int argc, char *argv[]) + sshfs.max_conns = 1; + sshfs.ptyfd = -1; + sshfs.dir_cache = 1; ++ sshfs.contain_symlinks = 1; + sshfs.show_help = 0; + sshfs.show_version = 0; + sshfs.singlethread = 0; +@@ -4237,6 +4281,12 @@ int main(int argc, char *argv[]) + exit(1); + } + ++ if (sshfs.transform_symlinks && sshfs.contain_symlinks) ++ fprintf(stderr, "warning: transform_symlinks with " ++ "contain_symlinks may reject transformed links " ++ "containing '..' - consider adding " ++ "-o no_contain_symlinks\n"); ++ + if (sshfs.idmap == IDMAP_USER) + sshfs.detect_uid = 1; + else if (sshfs.idmap == IDMAP_FILE) { +--- a/sshfs.rst ++++ b/sshfs.rst +@@ -172,6 +172,21 @@ Options + ``/foo/bar/com`` is a symlink to ``/foo/blub``, SSHFS will + transform the link target to ``../blub`` on the client side. + ++-o contain_symlinks ++ reject symlink targets that are absolute or contain ``..`` ++ components. When a blocked symlink is encountered, readlink ++ returns EPERM. This is enabled by default to prevent a ++ malicious server from inducing local file reads or writes ++ through crafted symlink targets. Note that this is stricter ++ than ``transform_symlinks``: the two options should not normally ++ be combined, since transformed results often contain ``..`` ++ and would be rejected by containment. ++ ++-o no_contain_symlinks ++ disable symlink containment and allow all symlink targets ++ through unchanged, including absolute paths and paths ++ containing ``..``. Only use this with fully trusted servers. ++ + -o follow_symlinks + follow symlinks on the server, i.e. present them as regular + files on the client. If a symlink is dangling (i.e, the target does diff -Nru sshfs-fuse-3.7.3/debian/patches/reject-hostname-option-injection-via-bracketed-mount.patch sshfs-fuse-3.7.3/debian/patches/reject-hostname-option-injection-via-bracketed-mount.patch --- sshfs-fuse-3.7.3/debian/patches/reject-hostname-option-injection-via-bracketed-mount.patch 1970-01-01 00:00:00.000000000 +0000 +++ sshfs-fuse-3.7.3/debian/patches/reject-hostname-option-injection-via-bracketed-mount.patch 2026-06-02 11:10:49.000000000 +0000 @@ -0,0 +1,136 @@ +From: Abhinav Agarwal +Date: Fri, 29 May 2026 15:38:43 -0700 +Subject: reject hostname option injection via bracketed mount source +Origin: https://github.com/libfuse/sshfs/commit/29bb565ea6405e2dd5a0ea65fe64da117e76055e +Bug: https://github.com/libfuse/sshfs/pull/362 +Bug-Debian: https://bugs.debian.org/1138293 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2026-48711 + +A source like [-oProxyCommand=CMD]:/path passes the bracket-parsing +check in find_base_path() and ends up as -oProxyCommand=CMD in the +ssh argv. When sftp_server is a path, ssh gets a destination argument +and executes the injected ProxyCommand before connecting. + +Reject hostnames starting with - after bracket stripping, and add -- +before the hostname in the ssh command line so positional args can't +be misread as options. +--- + sshfs.c | 8 ++++- + test/meson.build | 2 +- + test/test_hostname_validation.py | 60 ++++++++++++++++++++++++++++++++ + 3 files changed, 68 insertions(+), 2 deletions(-) + create mode 100644 test/test_hostname_validation.py + +diff --git a/sshfs.c b/sshfs.c +index 1bb83c765e2f..67d6a247181e 100644 +--- a/sshfs.c ++++ b/sshfs.c +@@ -4019,6 +4019,11 @@ static char *find_base_path(void) + *d++ = '\0'; + s++; + ++ if (sshfs.host[0] == '-') { ++ fprintf(stderr, "invalid hostname '%s'\n", sshfs.host); ++ exit(1); ++ } ++ + return s; + } + +@@ -4410,7 +4415,6 @@ int main(int argc, char *argv[]) + tmp = g_strdup_printf("-%i", sshfs.ssh_ver); + ssh_add_arg(tmp); + g_free(tmp); +- ssh_add_arg(sshfs.host); + if (sshfs.sftp_server) + sftp_server = sshfs.sftp_server; + else if (sshfs.ssh_ver == 1) +@@ -4421,6 +4425,8 @@ int main(int argc, char *argv[]) + if (sshfs.ssh_ver != 1 && strchr(sftp_server, '/') == NULL) + ssh_add_arg("-s"); + ++ ssh_add_arg("--"); ++ ssh_add_arg(sshfs.host); + ssh_add_arg(sftp_server); + free(sshfs.sftp_server); + +diff --git a/test/meson.build b/test/meson.build +index c0edde2d0482..4b26321f482d 100644 +--- a/test/meson.build ++++ b/test/meson.build +@@ -1,5 +1,5 @@ + test_scripts = [ 'conftest.py', 'pytest.ini', 'test_sshfs.py', +- 'util.py' ] ++ 'test_hostname_validation.py', 'util.py' ] + custom_target('test_scripts', input: test_scripts, + output: test_scripts, build_by_default: true, + command: ['cp', '-fPp', +diff --git a/test/test_hostname_validation.py b/test/test_hostname_validation.py +new file mode 100644 +index 000000000000..07b0c4f2bf04 +--- /dev/null ++++ b/test/test_hostname_validation.py +@@ -0,0 +1,60 @@ ++#!/usr/bin/env python3 ++"""Tests for hostname validation — no FUSE mount required.""" ++ ++if __name__ == "__main__": ++ import pytest ++ import sys ++ ++ sys.exit(pytest.main([__file__] + sys.argv[1:])) ++ ++import subprocess ++from util import base_cmdline, basename ++from os.path import join as pjoin ++ ++ ++def test_reject_option_injection_in_hostname(tmpdir): ++ """Bracketed source that resolves to a dash-prefixed host must be rejected.""" ++ ++ mnt_dir = str(tmpdir.mkdir("mnt")) ++ malicious = "[-oProxyCommand=echo pwned]:/path" ++ ++ cmdline = base_cmdline + [ ++ pjoin(basename, "sshfs"), ++ "-f", ++ malicious, ++ mnt_dir, ++ ] ++ res = subprocess.run( ++ cmdline, ++ stdin=subprocess.DEVNULL, ++ stdout=subprocess.PIPE, ++ stderr=subprocess.PIPE, ++ timeout=10, ++ text=True, ++ ) ++ assert res.returncode != 0 ++ assert "invalid hostname" in res.stderr ++ ++ ++def test_reject_dash_host_after_doubledash(tmpdir): ++ """Non-bracketed dash-prefixed source after -- must also be rejected.""" ++ ++ mnt_dir = str(tmpdir.mkdir("mnt")) ++ ++ cmdline = base_cmdline + [ ++ pjoin(basename, "sshfs"), ++ "-f", ++ "--", ++ "-oProxyCommand=echo pwned:/path", ++ mnt_dir, ++ ] ++ res = subprocess.run( ++ cmdline, ++ stdin=subprocess.DEVNULL, ++ stdout=subprocess.PIPE, ++ stderr=subprocess.PIPE, ++ timeout=10, ++ text=True, ++ ) ++ assert res.returncode != 0 ++ assert "invalid hostname" in res.stderr +-- +2.53.0 + diff -Nru sshfs-fuse-3.7.3/debian/patches/series sshfs-fuse-3.7.3/debian/patches/series --- sshfs-fuse-3.7.3/debian/patches/series 2019-11-16 02:27:57.000000000 +0000 +++ sshfs-fuse-3.7.3/debian/patches/series 2026-06-02 11:10:49.000000000 +0000 @@ -1 +1,3 @@ #sshfs.1.patch +add-contain_symlinks-option-to-prevent-symlink-escap.patch +reject-hostname-option-injection-via-bracketed-mount.patch