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

Reply via email to