Hi, On Sat, May 30, 2026 at 05:11:51PM +0200, Salvatore Bonaccorso wrote: > Source: sshfs-fuse > Version: 3.7.3-1.1 > Severity: grave > Tags: security upstream > X-Debbugs-Cc: [email protected], Debian Security Team > <[email protected]> > > Hi, > > The following vulnerabilities were published for sshfs-fuse. > > CVE-2026-47187[0]: > | Symlink escape - rogue SFTP server -> local file read/write > > CVE-2026-48711[1]: > | ssh argument injection via bracketed mount source > > If you fix the vulnerabilities please also make sure to include the > CVE (Common Vulnerabilities & Exposures) ids in your changelog entry. > > For further information see: > > [0] https://security-tracker.debian.org/tracker/CVE-2026-47187 > https://www.cve.org/CVERecord?id=CVE-2026-47187 > [1] https://security-tracker.debian.org/tracker/CVE-2026-48711 > https://www.cve.org/CVERecord?id=CVE-2026-48711 > [2] https://www.openwall.com/lists/oss-security/2026/05/30/3
Here is the proposed debdiff picking up the two upstream commits (and for the one dropping the test changes which are not run anyway, as they do not apply cleanly). So far only lightly tested, but running as well on debusine: https://debusine.debian.net/debian/developers/work-request/798664/ The idea would be, given the same version across down to bookworm, to make corresponding 3.7.3-1.2~deb13u1 and 3.7.3-1.2~deb12u1 (either via a DSA or point release update, I'm not yet sure, given CVE-2026-47187 needs as well a malicious server involved). Regards, Salvatore
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 20:33:53.000000000 +0100 +++ sshfs-fuse-3.7.3/debian/changelog 2026-05-30 17:20:39.000000000 +0200 @@ -1,3 +1,13 @@ +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 <[email protected]> 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 01:00:00.000000000 +0100 +++ sshfs-fuse-3.7.3/debian/patches/add-contain_symlinks-option-to-prevent-symlink-escap.patch 2026-05-30 17:18:42.000000000 +0200 @@ -0,0 +1,160 @@ +From: Abhinav Agarwal <[email protected]> +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 01:00:00.000000000 +0100 +++ sshfs-fuse-3.7.3/debian/patches/reject-hostname-option-injection-via-bracketed-mount.patch 2026-05-30 17:19:29.000000000 +0200 @@ -0,0 +1,136 @@ +From: Abhinav Agarwal <[email protected]> +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 03:27:57.000000000 +0100 +++ sshfs-fuse-3.7.3/debian/patches/series 2026-05-30 17:19:45.000000000 +0200 @@ -1 +1,3 @@ #sshfs.1.patch +add-contain_symlinks-option-to-prevent-symlink-escap.patch +reject-hostname-option-injection-via-bracketed-mount.patch

