Package: release.debian.org
Severity: normal
Tags: bookworm
X-Debbugs-Cc: [email protected], [email protected],
[email protected]
Control: affects -1 + src:sshfs-fuse
User: [email protected]
Usertags: pu
Hi SRM
[ Reason ]
sshfs-fuse is affected by two CVEs:
- Improper Neutralization of Argument Delimiters in a Command
('Argument Injection') in sshfs (CVE-2026-48711)
- Symlink Escape: Rogue SFTP Server → Local File Read/Write
(CVE-2026-47187)
While they are marked for the later one 'critical' from upstream they
need a rogue server at first place. So we think it does not warrant a
DSA.
[ Impact ]
If not updating remains vulnerable to those two CVEs.
[ Tests ]
Some manual tests. Unfortunately the test suite is disabled since
3.7.3-1.1.
[ Risks ]
The symlink CVE introduces a new contain_symlink option as has a
default as on, and will reject absolute symlinks targets and targets
containing '..' components, and returning EPERM. This can be
considered a behaviour changes but was according to upstream the
simplest rule to apply for beeing enough against the thread model
describd. So I think we need ot go for it. If in doupt, we can have
the uploads not yet accepted and give the unstable version bit more
exposure.
[ Checklist ]
[x] *all* changes are documented in the d/changelog
[x] I reviewed all changes and I approve them
[x] attach debdiff against the package in (old)stable
[x] the issue is verified as fixed in unstable
[ Changes ]
* Add contain_symlinks option to prevent symlink escape attacks
(CVE-2026-47187)
* Reject hostname option injection via bracketed mount source (CVE-2026-48711)
[ Other info ]
The unstable version has not yet migrated to testing.
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-06-02 13:13:13.000000000 +0200
@@ -1,3 +1,20 @@
+sshfs-fuse (3.7.3-1.2~deb12u1) bookworm; urgency=medium
+
+ * Non-maintainer upload.
+ * Rebuild for bookworm
+
+ -- Salvatore Bonaccorso <[email protected]> Tue, 02 Jun 2026 13:13:13 +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 <[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-06-02 13:12:58.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-06-02 13:12:58.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-06-02 13:12:58.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