Skip to content

Commit 29bb565

Browse files
reject hostname option injection via bracketed mount source
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.
1 parent 25b58aa commit 29bb565

3 files changed

Lines changed: 68 additions & 2 deletions

File tree

sshfs.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4019,6 +4019,11 @@ static char *find_base_path(void)
40194019
*d++ = '\0';
40204020
s++;
40214021

4022+
if (sshfs.host[0] == '-') {
4023+
fprintf(stderr, "invalid hostname '%s'\n", sshfs.host);
4024+
exit(1);
4025+
}
4026+
40224027
return s;
40234028
}
40244029

@@ -4410,7 +4415,6 @@ int main(int argc, char *argv[])
44104415
tmp = g_strdup_printf("-%i", sshfs.ssh_ver);
44114416
ssh_add_arg(tmp);
44124417
g_free(tmp);
4413-
ssh_add_arg(sshfs.host);
44144418
if (sshfs.sftp_server)
44154419
sftp_server = sshfs.sftp_server;
44164420
else if (sshfs.ssh_ver == 1)
@@ -4421,6 +4425,8 @@ int main(int argc, char *argv[])
44214425
if (sshfs.ssh_ver != 1 && strchr(sftp_server, '/') == NULL)
44224426
ssh_add_arg("-s");
44234427

4428+
ssh_add_arg("--");
4429+
ssh_add_arg(sshfs.host);
44244430
ssh_add_arg(sftp_server);
44254431
free(sshfs.sftp_server);
44264432

test/meson.build

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
test_scripts = [ 'conftest.py', 'pytest.ini', 'test_sshfs.py',
2-
'util.py' ]
2+
'test_hostname_validation.py', 'util.py' ]
33
custom_target('test_scripts', input: test_scripts,
44
output: test_scripts, build_by_default: true,
55
command: ['cp', '-fPp',

test/test_hostname_validation.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env python3
2+
"""Tests for hostname validation — no FUSE mount required."""
3+
4+
if __name__ == "__main__":
5+
import pytest
6+
import sys
7+
8+
sys.exit(pytest.main([__file__] + sys.argv[1:]))
9+
10+
import subprocess
11+
from util import base_cmdline, basename
12+
from os.path import join as pjoin
13+
14+
15+
def test_reject_option_injection_in_hostname(tmpdir):
16+
"""Bracketed source that resolves to a dash-prefixed host must be rejected."""
17+
18+
mnt_dir = str(tmpdir.mkdir("mnt"))
19+
malicious = "[-oProxyCommand=echo pwned]:/path"
20+
21+
cmdline = base_cmdline + [
22+
pjoin(basename, "sshfs"),
23+
"-f",
24+
malicious,
25+
mnt_dir,
26+
]
27+
res = subprocess.run(
28+
cmdline,
29+
stdin=subprocess.DEVNULL,
30+
stdout=subprocess.PIPE,
31+
stderr=subprocess.PIPE,
32+
timeout=10,
33+
text=True,
34+
)
35+
assert res.returncode != 0
36+
assert "invalid hostname" in res.stderr
37+
38+
39+
def test_reject_dash_host_after_doubledash(tmpdir):
40+
"""Non-bracketed dash-prefixed source after -- must also be rejected."""
41+
42+
mnt_dir = str(tmpdir.mkdir("mnt"))
43+
44+
cmdline = base_cmdline + [
45+
pjoin(basename, "sshfs"),
46+
"-f",
47+
"--",
48+
"-oProxyCommand=echo pwned:/path",
49+
mnt_dir,
50+
]
51+
res = subprocess.run(
52+
cmdline,
53+
stdin=subprocess.DEVNULL,
54+
stdout=subprocess.PIPE,
55+
stderr=subprocess.PIPE,
56+
timeout=10,
57+
text=True,
58+
)
59+
assert res.returncode != 0
60+
assert "invalid hostname" in res.stderr

0 commit comments

Comments
 (0)