// ABOUTME: Unit tests for sshfsc helpers — path-traversal guard in // ABOUTME: verify_mount_dir (XDG_RUNTIME_DIR-rooted) and field allowlist regexes. package main import ( "os" "path/filepath" "strings" "testing" ) func TestVerifyMountDir(t *testing.T) { runtime := t.TempDir() t.Setenv("XDG_RUNTIME_DIR", runtime) base := filepath.Join(runtime, "sshfs") if err := os.MkdirAll(base, 0700); err != nil { t.Fatalf("setup base: %v", err) } realBase, err := filepath.EvalSymlinks(base) if err != nil { t.Fatalf("resolve base: %v", err) } cases := []struct { name string hostname string wantErr string // substring; "" means no error wantPath string // only checked when wantErr == "" }{ {"normal alias", "server1", "", filepath.Join(realBase, "server1")}, {"trailing slash", "server1/", "", filepath.Join(realBase, "server1")}, {"dot in name", "db.prod", "", filepath.Join(realBase, "db.prod")}, {"parent traversal", "../etc", "escapes mount base", ""}, {"deep traversal", "../../tmp/x", "escapes mount base", ""}, {"empty string", "", "escapes mount base", ""}, {"single dot", ".", "escapes mount base", ""}, // nested path lands under base — accepted by guard. The hostname allowlist // (rxHostUser) rejects slashes upstream; this test pins guard behaviour. {"nested path stays under base", "sub/dir", "", filepath.Join(realBase, "sub/dir")}, // absolute-looking input does not escape because filepath.Join keeps it // under base. Same upstream allowlist applies. {"absolute-looking input", "/etc/passwd", "", filepath.Join(realBase, "etc/passwd")}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, err := verify_mount_dir(tc.hostname) if tc.wantErr != "" { if err == nil { t.Fatalf("want error containing %q, got nil (path=%q)", tc.wantErr, got) } if !strings.Contains(err.Error(), tc.wantErr) { t.Fatalf("error %q does not contain %q", err.Error(), tc.wantErr) } return } if err != nil { t.Fatalf("unexpected error: %v", err) } if got != tc.wantPath { t.Fatalf("path = %q, want %q", got, tc.wantPath) } }) } } func TestValidateSSHFieldRegexes(t *testing.T) { cases := []struct { name string value string rx string ok bool }{ {"plain host", "myserver", "host", true}, {"host with dots", "db.prod.example.com", "host", true}, {"host with hyphen", "edge-01", "host", true}, {"host leading dash rejected", "-rogue", "host", false}, {"host with comma rejected", "a,b", "host", false}, {"host with space rejected", "a b", "host", false}, {"host with slash rejected", "a/b", "host", false}, {"host empty rejected", "", "host", false}, {"identityfile tilde", "~/.ssh/id_ed25519", "ifile", true}, {"identityfile absolute", "/home/dom/.ssh/id_rsa", "ifile", true}, {"identityfile letter start", "id_rsa", "ifile", true}, {"identityfile digit start", "0key", "ifile", true}, {"identityfile dash mid accepted", "~/.ssh/id-host", "ifile", true}, {"identityfile with comma rejected", "~/.ssh/id,allow_other", "ifile", false}, {"identityfile with space rejected", "~/.ssh/id rsa", "ifile", false}, {"identityfile leading dash rejected", "-Ffoo", "ifile", false}, {"identityfile leading dot rejected", ".ssh/id", "ifile", false}, {"identityfile leading colon rejected", ":weird", "ifile", false}, {"identityfile empty rejected", "", "ifile", false}, {"remote-dir absolute", "/var/www", "rdir", true}, {"remote-dir tilde", "~/work", "rdir", true}, {"remote-dir relative", "projects/foo", "rdir", true}, {"remote-dir leading dash rejected", "-rf", "rdir", false}, {"remote-dir leading dot rejected", "./x", "rdir", false}, {"remote-dir space rejected", "/var /www", "rdir", false}, {"remote-dir comma rejected", "/a,b", "rdir", false}, {"remote-dir empty rejected", "", "rdir", false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var rx = rxHostUser switch tc.rx { case "ifile": rx = rxIdentityFile case "rdir": rx = rxRemoteDir } err := validate_ssh_field("X", tc.value, rx) if tc.ok && err != nil { t.Fatalf("want accept, got error: %v", err) } if !tc.ok && err == nil { t.Fatalf("want reject, got accept") } }) } } func TestVerifyMountDirRejectsSymlink(t *testing.T) { runtime := t.TempDir() t.Setenv("XDG_RUNTIME_DIR", runtime) base := filepath.Join(runtime, "sshfs") if err := os.MkdirAll(base, 0700); err != nil { t.Fatalf("setup base: %v", err) } target := t.TempDir() if err := os.Symlink(target, filepath.Join(base, "evil")); err != nil { t.Fatalf("create symlink: %v", err) } _, err := verify_mount_dir("evil") if err == nil || !strings.Contains(err.Error(), "is a symlink") { t.Fatalf("want symlink rejection, got %v", err) } }