From 735af8bb9943e1b65944a179276be98d5a80df0f Mon Sep 17 00:00:00 2001 From: Steven McClain Date: Thu, 13 Aug 2026 13:31:25 -0400 Subject: [PATCH 1/2] Allow callers to select an exact SSH config ET currently reads the user and system SSH configuration before launching its own SSH subprocesses. In managed jumphost clients this can rewrite an authenticated target through HostName, import LocalForward entries, or apply ambient ProxyJump and SetEnv behavior before the caller-owned SSH policy runs. Add --ssh-config and --no-ssh-config. An exact path becomes ET's sole configuration input for destination and jumphost resolution and is passed as -F to both spawned SSH shapes; none disables configuration entirely. Preserve the existing ambient behavior when neither option is present. Fail closed on missing, non-regular, symlinked, relative, or shell-unsafe paths. The conservative path alphabet is required because OpenSSH renders the implicit -J child through a shell without quoting its propagated -F value. Tests record both destination and direct-jump argv, verify exact -F propagation, and cover the disabled-config shape. --- README.md | 10 +++ src/terminal/SshSetupHandler.cpp | 31 ++++++- src/terminal/SshSetupHandler.hpp | 12 ++- src/terminal/TerminalClientMain.cpp | 108 +++++++++++++++++++----- test/unit_tests/SshSetupHandlerTest.cpp | 95 +++++++++++++++++++++ 5 files changed, 230 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 8a52f886d..978bd8135 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,16 @@ et dev (etserver running on port 2022 on both hostname and jumphost) et dev:8000 -jport 9000 (etserver running on port 9000 on jumphost) ``` +To isolate ET from ambient SSH configuration, pass an absolute path with +`--ssh-config`. ET reads only that file for its own destination and jumphost +lookup and passes the same file to every SSH process it starts, including the +implicit ProxyJump connection. The file must be readable, regular, and not a +symbolic link, and its absolute path may contain only ASCII letters, digits, +`/`, `.`, `_`, and `-`; OpenSSH does not quote this path when constructing its +implicit ProxyJump command. Pass `--ssh-config none` or the equivalent +`--no-ssh-config` to disable both user and system SSH configuration entirely. +The two options are mutually exclusive. + ## Building from Source ### macOS diff --git a/src/terminal/SshSetupHandler.cpp b/src/terminal/SshSetupHandler.cpp index 5ed125808..c02f4c578 100644 --- a/src/terminal/SshSetupHandler.cpp +++ b/src/terminal/SshSetupHandler.cpp @@ -5,6 +5,20 @@ namespace et { const string SshSetupHandler::ETTERMINAL_BIN = "etterminal"; +bool SshSetupHandler::IsSshConfigPathSafeForProxyJump(const string& path) { + if (path.empty() || path.front() != '/') { + return false; + } + for (unsigned char c : path) { + bool asciiAlphaNumeric = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9'); + if (!asciiAlphaNumeric && c != '/' && c != '.' && c != '_' && c != '-') { + return false; + } + } + return true; +} + string genCommand(const string& passkey, const string& id, const string& clientTerm, const string& user, bool kill, const string& etterminal_path, const string& options) { @@ -56,11 +70,16 @@ pair SshSetupHandler::SetupSsh( } std::vector ssh_args; + if (!sshConfigPath_.empty()) { + // An explicit `-F` suppresses OpenSSH's user and system configuration. + // OpenSSH also propagates it to the implicit proxy command created by + // `-J`, so the explicit jumphost uses the same selected policy. + ssh_args.push_back("-F"); + ssh_args.push_back(sshConfigPath_); + } if (!jumphost.empty()) { - ssh_args = { - "-J", - jumphost, - }; + ssh_args.push_back("-J"); + ssh_args.push_back(jumphost); } ssh_args.push_back(SSH_USER_PREFIX + host_alias); @@ -141,6 +160,10 @@ pair SshSetupHandler::SetupSsh( : parsedJump.user + "@" + jumphostAddr; std::vector jump_ssh_args; + if (!sshConfigPath_.empty()) { + jump_ssh_args.push_back("-F"); + jump_ssh_args.push_back(sshConfigPath_); + } if (!parsedJump.portSuffix.empty()) { // portSuffix includes the colon, e.g. ":22" jump_ssh_args.push_back("-p"); diff --git a/src/terminal/SshSetupHandler.hpp b/src/terminal/SshSetupHandler.hpp index 9252ce681..86171bf06 100644 --- a/src/terminal/SshSetupHandler.hpp +++ b/src/terminal/SshSetupHandler.hpp @@ -13,9 +13,13 @@ class SshSetupHandler { /** * @brief Constructs an SshSetupHandler with a subprocess utility. * @param subprocessUtils The subprocess utility to use for running ssh. + * @param sshConfigPath An exact SSH configuration path for every SSH + * process, "none" to disable configuration, or empty for OpenSSH defaults. */ - explicit SshSetupHandler(shared_ptr subprocessUtils) - : subprocessUtils_(subprocessUtils) {} + explicit SshSetupHandler(shared_ptr subprocessUtils, + string sshConfigPath = "") + : subprocessUtils_(subprocessUtils), + sshConfigPath_(std::move(sshConfigPath)) {} /** * @brief Constructs the ssh command line for connecting to the ET server. @@ -29,8 +33,12 @@ class SshSetupHandler { /** @brief Path to the packaged `etterminal` helper binary. */ static const string ETTERMINAL_BIN; + /** @brief Whether OpenSSH can safely propagate this path through `-J`. */ + static bool IsSshConfigPathSafeForProxyJump(const string& path); + private: shared_ptr subprocessUtils_; + string sshConfigPath_; }; } // namespace et #endif // __ET_SSH_SETUP_HANDLER__ diff --git a/src/terminal/TerminalClientMain.cpp b/src/terminal/TerminalClientMain.cpp index 570bfe806..5467c8e3d 100644 --- a/src/terminal/TerminalClientMain.cpp +++ b/src/terminal/TerminalClientMain.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include "Headers.hpp" #include "HostParsing.hpp" @@ -55,19 +57,39 @@ struct ResolvedSshConfig { string username; // Username from SSH config (empty if not specified) }; +// Parse the selected SSH policy. An empty path preserves ET's legacy ambient +// behavior, "none" disables parsing, and every other value names the sole +// configuration file to read. +void parseSelectedSshConfig(const string& host, Options* options, + const string& sshConfigPath) { + if (sshConfigPath == "none") { + return; + } + if (!sshConfigPath.empty()) { + parse_ssh_config_file(host.c_str(), options, sshConfigPath); + return; + } + + char* homeDir = ssh_get_user_home_dir(); + if (homeDir != NULL) { + parse_ssh_config_file(host.c_str(), options, + string(homeDir) + USER_SSH_CONFIG_PATH); + free(homeDir); + } + parse_ssh_config_file(host.c_str(), options, SYSTEM_SSH_CONFIG_PATH); +} + // Resolve a host alias via SSH config lookup -ResolvedSshConfig resolveSshConfigHost(const string& hostAlias) { +ResolvedSshConfig resolveSshConfigHost(const string& hostAlias, + const string& sshConfigPath) { ResolvedSshConfig result; result.hostname = hostAlias; // Default to original if not resolved - char* home_dir = ssh_get_user_home_dir(); Options opts = {NULL, NULL, NULL, NULL, NULL, NULL, 0, 0, 0, 0, 0, NULL, NULL, 0, 0, NULL, {}}; ssh_options_set(&opts, SSH_OPTIONS_HOST, hostAlias.c_str()); - parse_ssh_config_file(hostAlias.c_str(), &opts, - string(home_dir) + USER_SSH_CONFIG_PATH); - parse_ssh_config_file(hostAlias.c_str(), &opts, SYSTEM_SSH_CONFIG_PATH); + parseSelectedSshConfig(hostAlias, &opts, sshConfigPath); if (opts.host) { result.hostname = string(opts.host); @@ -77,7 +99,6 @@ ResolvedSshConfig resolveSshConfigHost(const string& hostAlias) { } freeOptionsFields(&opts); - free(home_dir); return result; } @@ -178,6 +199,12 @@ int main(int argc, char** argv) { ("f,forward-ssh-agent", "Forward ssh-agent socket") // ("ssh-socket", "The ssh-agent socket to forward", cxxopts::value()) // + ("ssh-config", + "Read only this absolute SSH configuration file (or 'none')", + cxxopts::value()) // + ("no-ssh-config", + "Do not read user or system SSH configuration for the destination " + "or jumphost") // ("telemetry", "Allow et to anonymously send errors to guide future improvements", cxxopts::value()->default_value("true")) // @@ -280,6 +307,51 @@ int main(int argc, char** argv) { string jumphost = extractSingleOptionWithDefault(result, options, "jumphost", ""); + if (result.count("ssh-config") && result.count("no-ssh-config")) { + CLOG(INFO, "stdout") + << "--ssh-config and --no-ssh-config are mutually exclusive" << endl; + exit(1); + } + string sshConfigPath; + if (result.count("ssh-config")) { + sshConfigPath = result["ssh-config"].as(); + if (sshConfigPath != "none" && + !std::filesystem::path(sshConfigPath).is_absolute()) { + CLOG(INFO, "stdout") + << "--ssh-config must be an absolute path or 'none': " + << sshConfigPath << endl; + exit(1); + } + if (sshConfigPath != "none" && + !SshSetupHandler::IsSshConfigPathSafeForProxyJump(sshConfigPath)) { + CLOG(INFO, "stdout") + << "--ssh-config must contain only ASCII letters, digits, '/', " + "'.', '_', and '-'; OpenSSH does not quote this path when " + "propagating it through ProxyJump" + << endl; + exit(1); + } + if (sshConfigPath != "none") { + std::error_code configError; + std::filesystem::file_status configStatus = + std::filesystem::symlink_status(sshConfigPath, configError); + bool regularFile = std::filesystem::is_regular_file(configStatus); + bool readableFile = false; + if (!configError && regularFile) { + std::ifstream configFile(sshConfigPath); + readableFile = configFile.good(); + } + if (!readableFile) { + CLOG(INFO, "stdout") + << "--ssh-config must name a readable, non-symlink regular file" + << endl; + exit(1); + } + } + } else if (result.count("no-ssh-config")) { + sshConfigPath = "none"; + } + bool noSshConfig = sshConfigPath == "none"; int keepaliveDuration = extractSingleOptionWithDefault( result, options, "keepalive", MAX_CLIENT_KEEP_ALIVE_DURATION); if (keepaliveDuration < 1 || @@ -291,22 +363,15 @@ int main(int argc, char** argv) { exit(0); } - { - char* home_dir = ssh_get_user_home_dir(); - const char* host_from_command = destinationHost.c_str(); + if (!noSshConfig) { ssh_options_set(&sshConfigOptions, SSH_OPTIONS_HOST, destinationHost.c_str()); - // First parse user-specific ssh config, then system-wide config. - parse_ssh_config_file(host_from_command, &sshConfigOptions, - string(home_dir) + USER_SSH_CONFIG_PATH); - parse_ssh_config_file(host_from_command, &sshConfigOptions, - SYSTEM_SSH_CONFIG_PATH); + parseSelectedSshConfig(destinationHost, &sshConfigOptions, sshConfigPath); if (sshConfigOptions.host) { LOG(INFO) << "Parsed ssh config file, connecting to " << sshConfigOptions.host; destinationHost = string(sshConfigOptions.host); } - free(home_dir); } // Parse username: cmdline > sshconfig > localuser @@ -337,8 +402,10 @@ int main(int argc, char** argv) { // Parse [user@]host[:sshport] format ParsedHostString parsed = parseHostString(jumphost); - // Resolve jumphost alias to actual hostname via SSH config - ResolvedSshConfig resolved = resolveSshConfigHost(parsed.host); + // Resolve jumphost aliases only when SSH configuration is enabled. + // In --no-ssh-config mode, keep the command-line host and user exact. + ResolvedSshConfig resolved = + resolveSshConfigHost(parsed.host, sshConfigPath); if (resolved.hostname != parsed.host) { LOG(INFO) << "Resolved jumphost alias '" << parsed.host << "' to hostname: " << resolved.hostname; @@ -351,14 +418,15 @@ int main(int argc, char** argv) { LOG(INFO) << "Using jumphost username from SSH config: " << jumphostUser; } - if (jumphostUser.empty()) { + if (jumphostUser.empty() && !noSshConfig) { char* localUsernamePtr = ssh_get_local_username(); jumphostUser = string(localUsernamePtr); SAFE_FREE(localUsernamePtr); } // Reconstruct jumphost with resolved hostname for SSH -J flag - jumphost = jumphostUser + "@" + resolved.hostname + parsed.portSuffix; + jumphost = (jumphostUser.empty() ? "" : jumphostUser + "@") + + resolved.hostname + parsed.portSuffix; socketEndpoint.set_name(resolved.hostname); socketEndpoint.set_port(result["jport"].as()); @@ -433,7 +501,7 @@ int main(int argc, char** argv) { } auto subprocessUtils = make_shared(); - SshSetupHandler sshSetupHandler(subprocessUtils); + SshSetupHandler sshSetupHandler(subprocessUtils, sshConfigPath); pair idpasskeypair = sshSetupHandler.SetupSsh( username, destinationHost, host_alias, destinationPort, jumphost, jServerFifo, result.count("x") > 0, result["verbose"].as(), diff --git a/test/unit_tests/SshSetupHandlerTest.cpp b/test/unit_tests/SshSetupHandlerTest.cpp index bcc7fa962..9084daad0 100644 --- a/test/unit_tests/SshSetupHandlerTest.cpp +++ b/test/unit_tests/SshSetupHandlerTest.cpp @@ -33,6 +33,24 @@ class FakeSshSubprocessHandler : public SubprocessUtils { } }; +/** + * @brief Fake subprocess handler that records every SSH invocation. + */ +class RecordingSshConfigSubprocessHandler : public SubprocessUtils { + public: + vector> calls; + + string SubprocessToStringInteractive(const string& command, + const vector& args) override { + REQUIRE(command == "ssh"); + calls.push_back(args); + + string id = genRandomAlphaNum(16); + string passkey = genRandomAlphaNum(32); + return string("IDPASSKEY:") + id + "/" + passkey; + } +}; + /** * @brief Fake subprocess handler that returns empty output * to simulate SSH connection failure. @@ -255,3 +273,80 @@ TEST_CASE("SshSetupHandler with jumphost and jServerFifo", REQUIRE(id.length() == 16); REQUIRE(passkey.length() == 32); } + +TEST_CASE("SshSetupHandler can select one exact SSH configuration", + "[SshSetupHandler]") { + auto fakeSubprocess = make_shared(); + const string config_path = "/private/et-client/ssh_config"; + SshSetupHandler handler(fakeSubprocess, config_path); + + auto [id, passkey] = handler.SetupSsh( + "exact-target-user", "exact-target.example", "exact-target.example", 2022, + "exact-jump-user@exact-jump.example:2222", "", false, 0, "", "", {}); + + REQUIRE(id.length() == 16); + REQUIRE(passkey.length() == 32); + REQUIRE(fakeSubprocess->calls.size() == 2); + + const auto& destination_args = fakeSubprocess->calls[0]; + REQUIRE(destination_args.size() == 6); + REQUIRE(destination_args[0] == "-F"); + REQUIRE(destination_args[1] == config_path); + REQUIRE(destination_args[2] == "-J"); + REQUIRE(destination_args[3] == "exact-jump-user@exact-jump.example:2222"); + REQUIRE(destination_args[4] == "exact-target-user@exact-target.example"); + + const auto& jump_args = fakeSubprocess->calls[1]; + REQUIRE(jump_args.size() == 6); + REQUIRE(jump_args[0] == "-F"); + REQUIRE(jump_args[1] == config_path); + REQUIRE(jump_args[2] == "-p"); + REQUIRE(jump_args[3] == "2222"); + REQUIRE(jump_args[4] == "exact-jump-user@exact-jump.example"); + REQUIRE(jump_args[5].find( + "--jump --dsthost=exact-target.example --dstport=2022") != + string::npos); +} + +TEST_CASE("SshSetupHandler can disable all SSH configuration", + "[SshSetupHandler]") { + auto fakeSubprocess = make_shared(); + SshSetupHandler handler(fakeSubprocess, "none"); + + handler.SetupSsh("exact-user", "exact-target.example", "exact-target.example", + 2022, "exact-jump-user@exact-jump.example", "", false, 0, "", + "", {}); + + REQUIRE(fakeSubprocess->calls.size() == 2); + const auto& destination_args = fakeSubprocess->calls[0]; + REQUIRE(destination_args[0] == "-F"); + REQUIRE(destination_args[1] == "none"); + REQUIRE(destination_args[2] == "-J"); + REQUIRE(destination_args[3] == "exact-jump-user@exact-jump.example"); + REQUIRE(destination_args[4] == "exact-user@exact-target.example"); + + const auto& jump_args = fakeSubprocess->calls[1]; + REQUIRE(jump_args[0] == "-F"); + REQUIRE(jump_args[1] == "none"); + REQUIRE(jump_args[2] == "exact-jump-user@exact-jump.example"); +} + +TEST_CASE("SSH config paths are safe for OpenSSH ProxyJump", + "[SshSetupHandler]") { + REQUIRE(SshSetupHandler::IsSshConfigPathSafeForProxyJump( + "/private/et-client_1/ssh.config")); + + const vector unsafe_paths = { + "relative/config", "/private/config with-space", + "/private/config\\path", "/private/config$variable", + "/private/config`cmd`", "/private/config%token", + "/private/config;cmd", "/private/config&cmd", + "/private/config|cmd", "/private/config(cmd)", + "/private/config*glob", "/private/config?glob", + "/private/config[glob]", "/private/config'quote", + "/private/config\"quote", "", + }; + for (const auto& path : unsafe_paths) { + REQUIRE_FALSE(SshSetupHandler::IsSshConfigPathSafeForProxyJump(path)); + } +} From 1d373f7af592dcf887d73be347df01cf467b7ded Mon Sep 17 00:00:00 2001 From: Jason Gauci Date: Tue, 1 Sep 2026 13:34:16 -0500 Subject: [PATCH 2/2] Accept Windows absolute paths for --ssh-config IsSshConfigPathSafeForProxyJump treated "starts with /" as absolute and rejected drive letters and backslashes, so native Windows config files could never pass. Use std::filesystem::path::is_absolute() and allow ':' and '\\' only on Windows, where OpenSSH and CreateProcess need them. Co-authored-by: Cursor --- README.md | 12 +++++++----- src/terminal/SshSetupHandler.cpp | 23 +++++++++++++++++++---- src/terminal/SshSetupHandler.hpp | 9 ++++++++- src/terminal/TerminalClientMain.cpp | 3 +++ test/unit_tests/SshSetupHandlerTest.cpp | 21 +++++++++++++++++++++ 5 files changed, 58 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 978bd8135..a2d4370ca 100644 --- a/README.md +++ b/README.md @@ -231,11 +231,13 @@ To isolate ET from ambient SSH configuration, pass an absolute path with `--ssh-config`. ET reads only that file for its own destination and jumphost lookup and passes the same file to every SSH process it starts, including the implicit ProxyJump connection. The file must be readable, regular, and not a -symbolic link, and its absolute path may contain only ASCII letters, digits, -`/`, `.`, `_`, and `-`; OpenSSH does not quote this path when constructing its -implicit ProxyJump command. Pass `--ssh-config none` or the equivalent -`--no-ssh-config` to disable both user and system SSH configuration entirely. -The two options are mutually exclusive. +symbolic link. The path must be absolute (`/path` on Unix; `C:\path`, +`C:/path`, or a UNC path on Windows) and may contain only ASCII letters, digits, +`/`, `.`, `_`, and `-` (plus `:` and `\` on Windows). Spaces are not allowed: +OpenSSH does not quote this path when constructing its implicit ProxyJump +command. Pass `--ssh-config none` or the equivalent `--no-ssh-config` to +disable both user and system SSH configuration entirely. The two options are +mutually exclusive. ## Building from Source diff --git a/src/terminal/SshSetupHandler.cpp b/src/terminal/SshSetupHandler.cpp index c02f4c578..b2409c491 100644 --- a/src/terminal/SshSetupHandler.cpp +++ b/src/terminal/SshSetupHandler.cpp @@ -5,14 +5,29 @@ namespace et { const string SshSetupHandler::ETTERMINAL_BIN = "etterminal"; +namespace { +/** @brief Characters OpenSSH can splice into an unquoted ProxyJump -F path. */ +bool isSshConfigPathCharSafeForProxyJump(unsigned char c) { + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '/' || c == '.' || c == '_' || c == '-') { + return true; + } +#ifdef WIN32 + // Native Windows paths need a drive colon and backslash; those are not + // Bourne metacharacters, and Unix clients never pass Windows paths. + return c == ':' || c == '\\'; +#else + return false; +#endif +} +} // namespace + bool SshSetupHandler::IsSshConfigPathSafeForProxyJump(const string& path) { - if (path.empty() || path.front() != '/') { + if (!std::filesystem::path(path).is_absolute()) { return false; } for (unsigned char c : path) { - bool asciiAlphaNumeric = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || - (c >= '0' && c <= '9'); - if (!asciiAlphaNumeric && c != '/' && c != '.' && c != '_' && c != '-') { + if (!isSshConfigPathCharSafeForProxyJump(c)) { return false; } } diff --git a/src/terminal/SshSetupHandler.hpp b/src/terminal/SshSetupHandler.hpp index 86171bf06..937d0eeb0 100644 --- a/src/terminal/SshSetupHandler.hpp +++ b/src/terminal/SshSetupHandler.hpp @@ -33,7 +33,14 @@ class SshSetupHandler { /** @brief Path to the packaged `etterminal` helper binary. */ static const string ETTERMINAL_BIN; - /** @brief Whether OpenSSH can safely propagate this path through `-J`. */ + /** + * @brief Whether OpenSSH can safely propagate this path through `-J`. + * + * Requires a native absolute path. Allowed characters are ASCII letters, + * digits, `/`, `.`, `_`, and `-`; Windows also allows `:` and `\\` so drive + * and UNC paths work. Spaces and shell metacharacters are rejected because + * OpenSSH interpolates `-F` into the implicit ProxyJump command unquoted. + */ static bool IsSshConfigPathSafeForProxyJump(const string& path); private: diff --git a/src/terminal/TerminalClientMain.cpp b/src/terminal/TerminalClientMain.cpp index 5467c8e3d..36daa1369 100644 --- a/src/terminal/TerminalClientMain.cpp +++ b/src/terminal/TerminalClientMain.cpp @@ -326,6 +326,9 @@ int main(int argc, char** argv) { !SshSetupHandler::IsSshConfigPathSafeForProxyJump(sshConfigPath)) { CLOG(INFO, "stdout") << "--ssh-config must contain only ASCII letters, digits, '/', " +#ifdef WIN32 + "'\\\\', ':', " +#endif "'.', '_', and '-'; OpenSSH does not quote this path when " "propagating it through ProxyJump" << endl; diff --git a/test/unit_tests/SshSetupHandlerTest.cpp b/test/unit_tests/SshSetupHandlerTest.cpp index 9084daad0..b62682173 100644 --- a/test/unit_tests/SshSetupHandlerTest.cpp +++ b/test/unit_tests/SshSetupHandlerTest.cpp @@ -333,8 +333,29 @@ TEST_CASE("SshSetupHandler can disable all SSH configuration", TEST_CASE("SSH config paths are safe for OpenSSH ProxyJump", "[SshSetupHandler]") { +#ifdef WIN32 + REQUIRE(SshSetupHandler::IsSshConfigPathSafeForProxyJump( + "C:\\et-client_1\\ssh.config")); + REQUIRE(SshSetupHandler::IsSshConfigPathSafeForProxyJump( + "C:/et-client_1/ssh.config")); + REQUIRE(SshSetupHandler::IsSshConfigPathSafeForProxyJump( + "\\\\server\\share\\ssh_config")); + REQUIRE_FALSE(SshSetupHandler::IsSshConfigPathSafeForProxyJump( + "/private/et-client_1/ssh.config")); + REQUIRE_FALSE( + SshSetupHandler::IsSshConfigPathSafeForProxyJump("C:et\\ssh.config")); + REQUIRE_FALSE( + SshSetupHandler::IsSshConfigPathSafeForProxyJump("\\et\\ssh.config")); + REQUIRE_FALSE(SshSetupHandler::IsSshConfigPathSafeForProxyJump( + "C:\\Users\\foo bar\\config")); + REQUIRE_FALSE( + SshSetupHandler::IsSshConfigPathSafeForProxyJump("C:\\et\\config;cmd")); +#else REQUIRE(SshSetupHandler::IsSshConfigPathSafeForProxyJump( "/private/et-client_1/ssh.config")); + REQUIRE_FALSE(SshSetupHandler::IsSshConfigPathSafeForProxyJump( + "C:\\et-client_1\\ssh.config")); +#endif const vector unsafe_paths = { "relative/config", "/private/config with-space",