/
niceSOFT
/
libssh
Обзор
Документация
Войти
/
niceSOFT
/
libssh
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/config.c
2 366 строк
76 KB
Sudharshan Hegde
client: add ExitOnForwardFailure config option parsing and storage
31 июл 2026, 12:35
31 июл 2026, 12:35
a67838d
Код
Авторство
О чём код?
/* * config.c - parse the ssh config file * * This file is part of the SSH Library * * Copyright (c) 2009-2013 by Andreas Schneider <asn@cryptomilk.org> * * The SSH Library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or (at your * option) any later version. * * The SSH Library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the SSH Library; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, * MA 02111-1307, USA. */ #include "config.h" #include <ctype.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #ifdef HAVE_GLOB_H # include <glob.h> #endif #include <stdbool.h> #include <limits.h> #ifndef _WIN32 # include <sys/types.h> # include <sys/stat.h> # include <fcntl.h> # include <errno.h> # include <signal.h> # include <sys/wait.h> # include <net/if.h> # include <netinet/in.h> #endif #ifdef HAVE_IFADDRS_H #include <ifaddrs.h> #endif #include "libssh/config_parser.h" #include "libssh/config.h" #include "libssh/priv.h" #include "libssh/session.h" #include "libssh/misc.h" #include "libssh/options.h" #ifndef MAX_LINE_SIZE #define MAX_LINE_SIZE 1024 #endif struct ssh_config_keyword_table_s { const char *name; enum ssh_config_opcode_e opcode; bool cli_supported; }; static struct ssh_config_keyword_table_s ssh_config_keyword_table[] = { {"host", SOC_HOST, true}, {"match", SOC_MATCH, false}, {"hostname", SOC_HOSTNAME, true}, {"port", SOC_PORT, true}, {"user", SOC_USERNAME, true}, {"identityfile", SOC_IDENTITY, true}, {"ciphers", SOC_CIPHERS, true}, {"macs", SOC_MACS, true}, {"compression", SOC_COMPRESSION, true}, {"connecttimeout", SOC_TIMEOUT, true}, {"stricthostkeychecking", SOC_STRICTHOSTKEYCHECK, true}, {"userknownhostsfile", SOC_KNOWNHOSTS, true}, {"proxycommand", SOC_PROXYCOMMAND, true}, {"gssapiserveridentity", SOC_GSSAPISERVERIDENTITY, false}, {"gssapiclientidentity", SOC_GSSAPICLIENTIDENTITY, false}, {"gssapidelegatecredentials", SOC_GSSAPIDELEGATECREDENTIALS, true}, {"include", SOC_INCLUDE, true}, {"bindaddress", SOC_BINDADDRESS, true}, {"globalknownhostsfile", SOC_GLOBALKNOWNHOSTSFILE, true}, {"loglevel", SOC_LOGLEVEL, true}, {"hostkeyalgorithms", SOC_HOSTKEYALGORITHMS, true}, {"kexalgorithms", SOC_KEXALGORITHMS, true}, {"gssapiauthentication", SOC_GSSAPIAUTHENTICATION, true}, {"kbdinteractiveauthentication", SOC_KBDINTERACTIVEAUTHENTICATION, true}, {"challengeresponseauthentication", SOC_KBDINTERACTIVEAUTHENTICATION, true}, {"passwordauthentication", SOC_PASSWORDAUTHENTICATION, true}, {"pubkeyauthentication", SOC_PUBKEYAUTHENTICATION, true}, {"addkeystoagent", SOC_UNSUPPORTED, true}, {"addressfamily", SOC_ADDRESSFAMILY, true}, {"batchmode", SOC_BATCHMODE, true}, {"canonicaldomains", SOC_UNSUPPORTED, true}, {"canonicalizefallbacklocal", SOC_UNSUPPORTED, true}, {"canonicalizehostname", SOC_UNSUPPORTED, true}, {"canonicalizemaxdots", SOC_UNSUPPORTED, true}, {"canonicalizepermittedcnames", SOC_UNSUPPORTED, true}, {"certificatefile", SOC_CERTIFICATE, true}, {"checkhostip", SOC_UNSUPPORTED, true}, {"connectionattempts", SOC_UNSUPPORTED, true}, {"enablesshkeysign", SOC_UNSUPPORTED, true}, {"fingerprinthash", SOC_UNSUPPORTED, true}, {"forwardagent", SOC_UNSUPPORTED, true}, {"hashknownhosts", SOC_UNSUPPORTED, true}, {"hostbasedauthentication", SOC_UNSUPPORTED, true}, {"hostbasedacceptedalgorithms", SOC_UNSUPPORTED, true}, {"hostkeyalias", SOC_UNSUPPORTED, true}, {"identitiesonly", SOC_IDENTITIESONLY, true}, {"identityagent", SOC_IDENTITYAGENT, true}, {"ipqos", SOC_UNSUPPORTED, true}, {"kbdinteractivedevices", SOC_UNSUPPORTED, true}, {"nohostauthenticationforlocalhost", SOC_UNSUPPORTED, true}, {"numberofpasswordprompts", SOC_NUMBER_OF_PASSWORD_PROMPTS, true}, {"pkcs11provider", SOC_UNSUPPORTED, true}, {"preferredauthentications", SOC_PREFERRED_AUTHENTICATIONS, true}, {"proxyjump", SOC_PROXYJUMP, true}, {"proxyusefdpass", SOC_UNSUPPORTED, true}, {"pubkeyacceptedalgorithms", SOC_PUBKEYACCEPTEDKEYTYPES, true}, {"rekeylimit", SOC_REKEYLIMIT, true}, {"remotecommand", SOC_UNSUPPORTED, true}, {"revokedhostkeys", SOC_UNSUPPORTED, true}, {"serveralivecountmax", SOC_UNSUPPORTED, true}, {"serveraliveinterval", SOC_UNSUPPORTED, true}, {"streamlocalbindmask", SOC_UNSUPPORTED, true}, {"streamlocalbindunlink", SOC_UNSUPPORTED, true}, {"syslogfacility", SOC_UNSUPPORTED, true}, {"tcpkeepalive", SOC_UNSUPPORTED, true}, {"updatehostkeys", SOC_UNSUPPORTED, true}, {"verifyhostkeydns", SOC_UNSUPPORTED, true}, {"visualhostkey", SOC_UNSUPPORTED, true}, {"clearallforwardings", SOC_NA, true}, {"controlmaster", SOC_NA, true}, {"controlpersist", SOC_NA, true}, {"controlpath", SOC_NA, true}, {"dynamicforward", SOC_NA, true}, {"escapechar", SOC_ESCAPE_CHAR, true}, {"exitonforwardfailure", SOC_EXIT_ON_FORWARD_FAILURE, true}, {"forwardx11", SOC_NA, true}, {"forwardx11timeout", SOC_NA, true}, {"forwardx11trusted", SOC_NA, true}, {"gatewayports", SOC_NA, true}, {"ignoreunknown", SOC_NA, true}, {"localcommand", SOC_NA, true}, {"localforward", SOC_LOCAL_FORWARD, true}, {"permitlocalcommand", SOC_NA, true}, {"remoteforward", SOC_REMOTE_FORWARD, true}, {"requesttty", SOC_REQUEST_TTY, true}, {"sendenv", SOC_SEND_ENV, true}, {"tunnel", SOC_NA, true}, {"tunneldevice", SOC_NA, true}, {"xauthlocation", SOC_NA, true}, {"pubkeyacceptedkeytypes", SOC_PUBKEYACCEPTEDKEYTYPES, true}, {"requiredrsasize", SOC_REQUIRED_RSA_SIZE, true}, {"gssapikeyexchange", SOC_GSSAPIKEYEXCHANGE, true}, {"gssapikexalgorithms", SOC_GSSAPIKEXALGORITHMS, true}, {"tag", SOC_TAG, true}, {NULL, SOC_UNKNOWN, false}, }; enum ssh_config_match_e { MATCH_UNKNOWN = -1, MATCH_ALL, MATCH_FINAL, MATCH_CANONICAL, MATCH_EXEC, MATCH_HOST, MATCH_ORIGINALHOST, MATCH_USER, MATCH_LOCALUSER, MATCH_LOCALNETWORK, MATCH_VERSION, MATCH_TAGGED, }; struct ssh_config_match_keyword_table_s { const char *name; enum ssh_config_match_e opcode; }; static struct ssh_config_match_keyword_table_s ssh_config_match_keyword_table[] = { {"all", MATCH_ALL}, {"canonical", MATCH_CANONICAL}, {"final", MATCH_FINAL}, {"exec", MATCH_EXEC}, {"host", MATCH_HOST}, {"originalhost", MATCH_ORIGINALHOST}, {"user", MATCH_USER}, {"localuser", MATCH_LOCALUSER}, {"localnetwork", MATCH_LOCALNETWORK}, {"version", MATCH_VERSION}, {"tagged", MATCH_TAGGED}, {NULL, MATCH_UNKNOWN}, }; int ssh_config_parse_line(ssh_session session, const char *line, unsigned int count, int *parsing, unsigned int depth, bool global); static int ssh_config_parse_line_internal(ssh_session session, const char *line, unsigned int count, int *parsing, unsigned int depth, bool global, bool is_cli, bool fail_on_unknown); int ssh_config_parse_line_cli(ssh_session session, const char *line); enum ssh_config_opcode_e ssh_config_get_opcode(char *keyword) { int i; for (i = 0; ssh_config_keyword_table[i].name != NULL; i++) { if (strcasecmp(keyword, ssh_config_keyword_table[i].name) == 0) { return ssh_config_keyword_table[i].opcode; } } return SOC_UNKNOWN; } static bool ssh_config_is_cli_supported(enum ssh_config_opcode_e opcode) { int i; for (i = 0; ssh_config_keyword_table[i].name != NULL; i++) { if (opcode == ssh_config_keyword_table[i].opcode) { return ssh_config_keyword_table[i].cli_supported; } } return false; } #define LIBSSH_CONF_MAX_DEPTH 16 static void local_parse_file(ssh_session session, const char *filename, int *parsing, unsigned int depth, bool global) { FILE *f = NULL; char line[MAX_LINE_SIZE] = {0}; unsigned int count = 0; int rv; if (depth > LIBSSH_CONF_MAX_DEPTH) { ssh_set_error(session, SSH_FATAL, "ERROR - Too many levels of configuration includes " "when processing file '%s'", filename); return; } f = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE); if (f == NULL) { SSH_LOG(SSH_LOG_RARE, "Failed to open included configuration file %s", filename); return; } SSH_LOG(SSH_LOG_PACKET, "Reading additional configuration data from %s", filename); while (fgets(line, sizeof(line), f)) { count++; rv = ssh_config_parse_line(session, line, count, parsing, depth, global); if (rv < 0) { fclose(f); return; } } fclose(f); return; } #if defined(HAVE_GLOB) && defined(HAVE_GLOB_GL_FLAGS_MEMBER) && \ !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) static void local_parse_glob(ssh_session session, const char *fileglob, int *parsing, unsigned int depth, bool global) { glob_t globbuf = { .gl_flags = 0, }; int rt; size_t i; rt = glob(fileglob, GLOB_TILDE, NULL, &globbuf); if (rt == GLOB_NOMATCH) { globfree(&globbuf); return; } else if (rt != 0) { SSH_LOG(SSH_LOG_RARE, "Glob error: %s", fileglob); globfree(&globbuf); return; } for (i = 0; i < globbuf.gl_pathc; i++) { local_parse_file(session, globbuf.gl_pathv[i], parsing, depth, global); } globfree(&globbuf); } #endif /* defined(HAVE_GLOB) && defined(HAVE_GLOB_GL_FLAGS_MEMBER) && \ !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) */ static enum ssh_config_match_e ssh_config_get_match_opcode(const char *keyword) { size_t i; for (i = 0; ssh_config_match_keyword_table[i].name != NULL; i++) { if (strcasecmp(keyword, ssh_config_match_keyword_table[i].name) == 0) { return ssh_config_match_keyword_table[i].opcode; } } return MATCH_UNKNOWN; } /** * @brief Convert a raw Match predicate result to the final boolean outcome. * * @param ok Raw predicate result where a positive value means a match and * zero or a negative value means no match. * @param negate Whether the Match predicate was prefixed with '!'. * * @return 1 if the predicate matches after applying negation, 0 otherwise. */ static inline int ssh_config_match_result(int ok, bool negate) { if (ok <= 0 && negate == true) { return 1; } if (ok > 0 && negate == false) { return 1; } return 0; } /** * @brief Evaluate a Match predicate against a value and pattern list. * * @param value Value to match. * @param pattern Pattern list used for comparison. * @param negate Whether the Match predicate was prefixed with '!'. * * @return 1 if the predicate matches after applying negation, 0 otherwise. */ static int ssh_config_match(const char *value, const char *pattern, bool negate) { int ok, result = 0; ok = match_pattern_list(value, pattern, strlen(pattern), 0); result = ssh_config_match_result(ok, negate); SSH_LOG(SSH_LOG_TRACE, "%s '%s' against pattern '%s'%s (ok=%d)", result == 1 ? "Matched" : "Not matched", value, pattern, negate == true ? " (negated)" : "", ok); return result; } /** * @brief Evaluate a Match tagged predicate against the current session tag. * * @param value Current session tag. NULL is treated as an unset tag. * @param pattern Pattern to compare against the current tag. * @param negate Whether the Match predicate was prefixed with '!'. * * @return 1 if the predicate matches after applying negation, 0 otherwise. */ static int ssh_config_match_tagged(const char *value, const char *pattern, bool negate) { const char *tag = value != NULL ? value : ""; int ok, result = 0; size_t pattern_len; if (tag[0] == '\0') { /* Match tagged treats an explicit empty pattern as "match unset tag". * The generic match_pattern_list() should still treat empty patterns * as no match for other predicates. */ ok = (pattern[0] == '\0'); } else { pattern_len = strlen(pattern); ok = match_pattern_list(tag, pattern, pattern_len, 0) > 0; } result = ssh_config_match_result(ok, negate); SSH_LOG(SSH_LOG_TRACE, "%s tag '%s' against pattern '%s'%s (ok=%d)", result == 1 ? "Matched" : "Not matched", tag, pattern, negate == true ? " (negated)" : "", ok); return result; } #ifdef WITH_EXEC /* FIXME reuse the ssh_execute_command() from socket.c */ static int ssh_exec_shell(char *cmd) { char *shell = NULL; pid_t pid; int status, devnull, rc; shell = getenv("SHELL"); if (shell == NULL || shell[0] == '\0') { shell = (char *)"/bin/sh"; } rc = access(shell, X_OK); if (rc != 0) { SSH_LOG(SSH_LOG_WARN, "The shell '%s' is not executable", shell); return -1; } /* Need this to redirect subprocess stdin/out */ devnull = open("/dev/null", O_RDWR); if (devnull == -1) { SSH_LOG_STRERROR(SSH_LOG_WARN, errno, "Failed to open(/dev/null): %s"); return -1; } SSH_LOG(SSH_LOG_DEBUG, "Running command '%s'", cmd); pid = fork(); if (pid == 0) { /* Child */ char *argv[4]; /* Redirect child stdin and stdout. Leave stderr */ rc = dup2(devnull, STDIN_FILENO); if (rc == -1) { SSH_LOG_STRERROR(SSH_LOG_WARN, errno, "dup2: %s"); exit(1); } rc = dup2(devnull, STDOUT_FILENO); if (rc == -1) { SSH_LOG_STRERROR(SSH_LOG_WARN, errno, "dup2: %s"); exit(1); } if (devnull > STDERR_FILENO) { close(devnull); } argv[0] = shell; argv[1] = (char *) "-c"; argv[2] = strdup(cmd); argv[3] = NULL; rc = execv(argv[0], argv); if (rc == -1) { SSH_LOG_STRERROR(SSH_LOG_WARN, errno, "Failed to execute command '%s': %s", cmd); /* Die with signal to make this error apparent to parent. */ signal(SIGTERM, SIG_DFL); kill(getpid(), SIGTERM); _exit(1); } } /* Parent */ close(devnull); if (pid == -1) { /* Error */ SSH_LOG_STRERROR(SSH_LOG_WARN, errno, "Failed to fork child: %s"); return -1; } while (waitpid(pid, &status, 0) == -1) { if (errno != EINTR) { SSH_LOG_STRERROR(SSH_LOG_WARN, errno, "waitpid failed: %s"); return -1; } } if (!WIFEXITED(status)) { SSH_LOG(SSH_LOG_WARN, "Command %s exited abnormally", cmd); return -1; } SSH_LOG(SSH_LOG_TRACE, "Command '%s' returned %d", cmd, WEXITSTATUS(status)); return WEXITSTATUS(status); } static int ssh_match_exec(ssh_session session, const char *command, bool negate) { int rv, result = 0; char *cmd = NULL; /* TODO There should be more supported expansions */ cmd = ssh_path_expand_escape(session, command); if (cmd == NULL) { return 0; } rv = ssh_exec_shell(cmd); if (rv > 0 && negate == true) { result = 1; } else if (rv == 0 && negate == false) { result = 1; } SSH_LOG(SSH_LOG_TRACE, "%s 'exec' command '%s'%s (rv=%d)", result == 1 ? "Matched" : "Not matched", cmd, negate == true ? " (negated)" : "", rv); free(cmd); return result; } #else static int ssh_match_exec(ssh_session session, const char *command, bool negate) { (void)session; (void)command; (void)negate; SSH_LOG(SSH_LOG_TRACE, "Unsupported 'exec' command on Windows '%s'", command); return 0; } #endif /* WITH_EXEC */ /* * HostName recognizes %% and %h during config parsing. Other %X sequences are * left for deferred HostName expansion, and only a trailing bare '%' is * rejected here. */ static int ssh_config_scan_hostname_tokens(ssh_session session, const char *hostname, bool *needs_host, bool *has_unknown) { const char *p = NULL; if (needs_host != NULL) { *needs_host = false; } if (has_unknown != NULL) { *has_unknown = false; } if (hostname == NULL) { ssh_set_error(session, SSH_FATAL, "Cannot scan HostName tokens from NULL input"); return -1; } for (p = hostname; *p != '\0'; p++) { if (*p == '%') { if (p[1] == '\0') { ssh_set_error(session, SSH_FATAL, "Incomplete Hostname token"); return -1; } switch (p[1]) { case '%': p++; continue; case 'h': if (needs_host != NULL) { *needs_host = true; } p++; continue; default: if (has_unknown != NULL) { *has_unknown = true; } p++; continue; } } } return 0; } static char *ssh_config_lowercase_hostname_pattern(const char *hostname) { char *pattern = NULL; char *p = NULL; bool escape = false; if (hostname == NULL) { return NULL; } pattern = strdup(hostname); if (pattern == NULL) { return NULL; } for (p = pattern; *p != '\0'; p++) { if (escape) { escape = false; continue; } if (*p == '%') { escape = true; continue; } *p = tolower((unsigned char)*p); } return pattern; } /** * @brief: Parse the ProxyJump configuration line and if parsing, * stores the result in the configuration option * * @param[in] session The ssh session * @param[in] s The string to be parsed. * @param[in] do_parsing Whether to parse or not. * * @returns SSH_OK if the provided string is formatted and parsed correctly * SSH_ERROR on failure */ int ssh_config_parse_proxy_jump(ssh_session session, const char *s, bool do_parsing) { char *c = NULL, *cp = NULL, *endp = NULL; char *username = NULL; char *hostname = NULL; char *port = NULL; char *next = NULL; int cmp, rv = SSH_ERROR; struct ssh_jump_info_struct *jump_host = NULL; bool parse_entry = do_parsing; bool libssh_proxy_jump = ssh_libssh_proxy_jumps(); if (do_parsing) { SAFE_FREE(session->opts.proxy_jumps_str); ssh_proxyjumps_free(session->opts.proxy_jumps); } /* Special value none disables the proxy */ cmp = strcasecmp(s, "none"); if (cmp == 0) { if (!libssh_proxy_jump && do_parsing) { ssh_options_set(session, SSH_OPTIONS_PROXYCOMMAND, s); } return SSH_OK; } /* This is comma-separated list of [user@]host[:port] entries */ c = strdup(s); if (c == NULL) { ssh_set_error_oom(session); return SSH_ERROR; } if (do_parsing) { /* Store the whole string in session */ SAFE_FREE(session->opts.proxy_jumps_str); session->opts.proxy_jumps_str = strdup(s); if (session->opts.proxy_jumps_str == NULL) { free(c); ssh_set_error_oom(session); return SSH_ERROR; } } cp = c; do { endp = strchr(cp, ','); if (endp != NULL) { /* Split out the token */ *endp = '\0'; } if (parse_entry && libssh_proxy_jump) { jump_host = calloc(1, sizeof(struct ssh_jump_info_struct)); if (jump_host == NULL) { ssh_set_error_oom(session); rv = SSH_ERROR; goto out; } rv = ssh_config_parse_uri(cp, &jump_host->username, &jump_host->hostname, &port, false, false); if (rv != SSH_OK) { ssh_set_error_invalid(session); SAFE_FREE(jump_host); goto out; } /* Leave the port at 0 when it is not given, so that the jump * host's own configuration can supply it later. */ if (port != NULL) { jump_host->port = strtol(port, NULL, 10); SAFE_FREE(port); } /* Allocate on first use (no longer pre-allocated in ssh_new()) */ if (session->opts.proxy_jumps == NULL) { session->opts.proxy_jumps = ssh_list_new(); if (session->opts.proxy_jumps == NULL) { ssh_set_error_oom(session); SAFE_FREE(jump_host); rv = SSH_ERROR; goto out; } } /* Prepend because we will recursively proxy jump */ rv = ssh_list_prepend(session->opts.proxy_jumps, jump_host); if (rv != SSH_OK) { ssh_set_error_oom(session); SAFE_FREE(jump_host); goto out; } } else if (parse_entry) { /* We actually care only about the first item */ rv = ssh_config_parse_uri(cp, &username, &hostname, &port, false, false); if (rv != SSH_OK) { ssh_set_error_invalid(session); goto out; } /* The rest of the list needs to be passed on */ if (endp != NULL) { next = strdup(endp + 1); if (next == NULL) { ssh_set_error_oom(session); rv = SSH_ERROR; goto out; } } } else { /* The rest is just sanity-checked to avoid failures later */ rv = ssh_config_parse_uri(cp, NULL, NULL, NULL, false, false); if (rv != SSH_OK) { ssh_set_error_invalid(session); goto out; } } if (!libssh_proxy_jump) { parse_entry = 0; } if (endp != NULL) { cp = endp + 1; } else { cp = NULL; /* end */ } } while (cp != NULL); if (!libssh_proxy_jump && hostname != NULL && do_parsing) { char com[512] = {0}; rv = snprintf(com, sizeof(com), "ssh%s%s%s%s%s%s -W '[%%h]:%%p' %s", username ? " -l " : "", username ? username : "", port ? " -p " : "", port ? port : "", next ? " -J " : "", next ? next : "", hostname); if (rv < 0 || rv >= (int)sizeof(com)) { SSH_LOG(SSH_LOG_TRACE, "Too long ProxyJump configuration line"); rv = SSH_ERROR; goto out; } rv = ssh_options_set(session, SSH_OPTIONS_PROXYCOMMAND, com); if (rv != SSH_OK) { ssh_set_error_oom(session); goto out; } } rv = SSH_OK; out: if (rv != SSH_OK) { ssh_proxyjumps_free(session->opts.proxy_jumps); } SAFE_FREE(username); SAFE_FREE(hostname); SAFE_FREE(port); SAFE_FREE(next); SAFE_FREE(c); return rv; } static char * ssh_config_make_absolute(ssh_session session, const char *path, bool global) { size_t outlen = 0; char *out = NULL; int rv; /* Looks like absolute path */ if (path[0] == '/') { return strdup(path); } /* relative path */ if (global) { /* Parsing global config */ outlen = strlen(path) + strlen("/etc/ssh/") + 1; out = malloc(outlen); if (out == NULL) { ssh_set_error_oom(session); return NULL; } rv = snprintf(out, outlen, "/etc/ssh/%s", path); if (rv < 1) { free(out); return NULL; } return out; } /* paths starting with tilde are already absolute */ if (path[0] == '~') { return ssh_path_expand_tilde(path); } /* Parsing user config relative to home directory (generally ~/.ssh) */ if (session->opts.sshdir == NULL) { ssh_set_error_invalid(session); return NULL; } outlen = strlen(path) + strlen(session->opts.sshdir) + 1 + 1; out = malloc(outlen); if (out == NULL) { ssh_set_error_oom(session); return NULL; } rv = snprintf(out, outlen, "%s/%s", session->opts.sshdir, path); if (rv < 1) { free(out); return NULL; } return out; } #ifdef HAVE_IFADDRS_H /** * @brief Checks if host address matches the local network specified. * * Verify whether a local network interface address matches any of the CIDR * patterns. * * @param addrlist The CIDR pattern-list to be checked, can contain both * IPv4 and IPv6 addresses and has to be comma separated * (',' only, space after comma not allowed). * * @param negate The negate condition. The return value is negated * (returns 1 instead of 0 and vice versa). * * @return 1 if match found. * @return 0 if no match found. * @return -1 on errors. */ static int ssh_match_localnetwork(const char *addrlist, bool negate) { struct ifaddrs *ifa = NULL, *ifaddrs = NULL; int r, found = 0; char address[NI_MAXHOST] = {0}; socklen_t sa_len; r = getifaddrs(&ifaddrs); if (r != 0) { SSH_LOG_STRERROR(SSH_LOG_WARN, errno, "Match localnetwork: getifaddrs() failed: %s"); return -1; } for (ifa = ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { if (ifa->ifa_addr == NULL || (ifa->ifa_flags & IFF_UP) == 0) { continue; } switch (ifa->ifa_addr->sa_family) { case AF_INET: sa_len = sizeof(struct sockaddr_in); break; case AF_INET6: sa_len = sizeof(struct sockaddr_in6); break; default: SSH_LOG(SSH_LOG_TRACE, "Interface %s: unsupported address family %d", ifa->ifa_name, ifa->ifa_addr->sa_family); continue; } r = getnameinfo(ifa->ifa_addr, sa_len, address, sizeof(address), NULL, 0, NI_NUMERICHOST); if (r != 0) { SSH_LOG(SSH_LOG_TRACE, "Interface %s getnameinfo failed: %s", ifa->ifa_name, gai_strerror(r)); continue; } SSH_LOG(SSH_LOG_TRACE, "Interface %s address %s", ifa->ifa_name, address); r = match_cidr_address_list(address, addrlist, ifa->ifa_addr->sa_family); if (r == 1) { SSH_LOG(SSH_LOG_TRACE, "Matched interface %s: address %s in %s", ifa->ifa_name, address, addrlist); found = 1; break; } } freeifaddrs(ifaddrs); return (found == (negate ? 0 : 1)); } #endif /* HAVE_IFADDRS_H */ static enum ssh_options_e ssh_config_get_auth_option(enum ssh_config_opcode_e opcode) { struct auth_option_map { enum ssh_config_opcode_e opcode; const char *name; enum ssh_options_e option; }; static struct auth_option_map auth_options[] = { { SOC_GSSAPIAUTHENTICATION, "GSSAPIAuthentication", SSH_OPTIONS_GSSAPI_AUTH, }, { SOC_KBDINTERACTIVEAUTHENTICATION, "KbdInteractiveAuthentication", SSH_OPTIONS_KBDINT_AUTH, }, { SOC_PASSWORDAUTHENTICATION, "PasswordAuthentication", SSH_OPTIONS_PASSWORD_AUTH, }, { SOC_PUBKEYAUTHENTICATION, "PubkeyAuthentication", SSH_OPTIONS_PUBKEY_AUTH, }, {0, NULL, 0}, }; for (struct auth_option_map *map = auth_options; map->name != NULL; map++) { if (map->opcode == opcode) { return map->option; } } return -1; } static long ssh_config_convtime(const char *p, long notfound) { char *endp = NULL; long total = 0; long value; long multiplier; if (p == NULL || *p == '\0') { return notfound; } while (*p != '\0') { errno = 0; value = strtol(p, &endp, 10); if (p == endp || errno != 0 || value < 0) { return notfound; } switch (*endp) { case '\0': case 's': case 'S': multiplier = 1; break; case 'm': case 'M': multiplier = 60; break; case 'h': case 'H': multiplier = 60 * 60; break; case 'd': case 'D': multiplier = 24 * 60 * 60; break; case 'w': case 'W': multiplier = 7 * 24 * 60 * 60; break; default: return notfound; } /* * OpenSSH accepts ConnectTimeout values only up to INT_MAX * seconds: see openssh-portable/misc.c:convtime(). */ if (value > (INT_MAX - total) / multiplier) { return notfound; } total += value * multiplier; if (*endp == '\0') { p = endp; } else { p = endp + 1; } } return total; } struct ssh_config_token_value_map { const char *token; int value; }; static int ssh_config_get_token_value(char **str, const struct ssh_config_token_value_map *map, int notfound) { const char *p = NULL; size_t i; p = ssh_config_get_str_tok(str, NULL); if (p == NULL) { return notfound; } for (i = 0; map[i].token != NULL; i++) { if (strcasecmp(p, map[i].token) == 0) { return map[i].value; } } return notfound; } /* * OpenSSH keeps Compression on the narrower yes/no syntax, and accepts "yes" * only when zlib support is available, so Compression does not use the * broader true/false aliases handled by ssh_config_get_yesno(). */ static const struct ssh_config_token_value_map compression_map[] = { #ifdef WITH_ZLIB {"yes", 1}, #endif /* WITH_ZLIB */ {"no", 0}, {NULL, 0}, }; static int ssh_config_get_strict_hostkey(char **str, int notfound) { static const struct ssh_config_token_value_map strict_hostkey_map[] = { {"yes", SSH_STRICT_HOSTKEY_YES}, {"true", SSH_STRICT_HOSTKEY_YES}, {"no", SSH_STRICT_HOSTKEY_OFF}, {"false", SSH_STRICT_HOSTKEY_OFF}, {"off", SSH_STRICT_HOSTKEY_OFF}, {"ask", SSH_STRICT_HOSTKEY_ASK}, {"accept-new", SSH_STRICT_HOSTKEY_ACCEPT_NEW}, {NULL, 0}, }; return ssh_config_get_token_value(str, strict_hostkey_map, notfound); } static int ssh_config_get_pubkey_auth(char **str, int notfound) { static const struct ssh_config_token_value_map pubkey_auth_map[] = { {"yes", SSH_PUBKEY_AUTH_ALL}, {"true", SSH_PUBKEY_AUTH_ALL}, {"no", SSH_PUBKEY_AUTH_NO}, {"false", SSH_PUBKEY_AUTH_NO}, {"unbound", SSH_PUBKEY_AUTH_UNBOUND}, {"host-bound", SSH_PUBKEY_AUTH_HOST_BOUND}, {NULL, 0}, }; return ssh_config_get_token_value(str, pubkey_auth_map, notfound); } #define CHECK_COND_OR_FAIL(cond, error_message) \ if ((cond)) { \ SSH_LOG(SSH_LOG_DEBUG, \ "line %d: %s: %s", \ count, \ error_message, \ keyword); \ if (fail_on_unknown) { \ ssh_set_error(session, \ SSH_FATAL, \ is_cli ? "%s '%s' value on CLI" \ : "%s '%s' value at line %d", \ error_message, \ keyword, \ is_cli ? 0 : count); \ SAFE_FREE(x); \ return SSH_ERROR; \ } \ break; \ } static int ssh_config_parse_line_internal(ssh_session session, const char *line, unsigned int count, int *parsing, unsigned int depth, bool global, bool is_cli, bool fail_on_unknown) { enum ssh_config_opcode_e opcode; const char *p = NULL, *p2 = NULL; char *s = NULL, *x = NULL; char *keyword = NULL; char *lowerhost = NULL; size_t len; int i, rv, cmp; uint8_t *seen = session->opts.options_seen; long l; int64_t ll; /* Ignore empty lines */ if (line == NULL || *line == '\0') { if (is_cli) { return SSH_ERROR; } return 0; } x = s = strdup(line); if (s == NULL) { ssh_set_error_oom(session); return -1; } /* Remove trailing spaces */ for (len = strlen(s) - 1; len > 0; len--) { if (! isspace(s[len])) { break; } s[len] = '\0'; } keyword = ssh_config_get_token(&s); if (keyword == NULL || *keyword == '#' || *keyword == '\0' || *keyword == '\n') { SAFE_FREE(x); return 0; } opcode = ssh_config_get_opcode(keyword); if (is_cli && !ssh_config_is_cli_supported(opcode)) { ssh_set_error( session, SSH_FATAL, "Option '%s' is not supported in command-line configuration", keyword); SAFE_FREE(x); return SSH_ERROR; } if (*parsing == 1 && opcode != SOC_HOST && opcode != SOC_MATCH && opcode != SOC_INCLUDE && opcode != SOC_IDENTITY && opcode != SOC_CERTIFICATE && opcode != SOC_LOCAL_FORWARD && opcode != SOC_SEND_ENV && opcode != SOC_REMOTE_FORWARD && opcode > SOC_UNSUPPORTED && opcode < SOC_MAX) { /* Ignore all unknown types here */ /* Skip all the options that were already applied */ if (seen[opcode] != 0) { SAFE_FREE(x); return 0; } seen[opcode] = 1; } switch (opcode) { case SOC_INCLUDE: /* recursive include of other files */ p = ssh_config_get_str_tok(&s, NULL); if (p && *parsing) { char *path = ssh_config_make_absolute(session, p, global); if (path == NULL) { SSH_LOG(SSH_LOG_WARN, "line %d: Failed to allocate memory " "for the include path expansion", count); SAFE_FREE(x); return -1; } #if defined(HAVE_GLOB) && defined(HAVE_GLOB_GL_FLAGS_MEMBER) && \ !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) local_parse_glob(session, path, parsing, depth + 1, global); #else local_parse_file(session, path, parsing, depth + 1, global); #endif free(path); } break; case SOC_MATCH: { bool negate; int match_error = 0; int result = 1; size_t args = 0; enum ssh_config_match_e opt; struct ssh_config_token_info keyword_info; struct ssh_config_token_info arg_info; char *localuser = NULL; const char *version = NULL; *parsing = 0; do { p = p2 = ssh_config_get_token_info(&s, &keyword_info); if (!keyword_info.found || p[0] == '\0') { break; } args++; SSH_LOG(SSH_LOG_DEBUG, "line %d: Processing Match keyword '%s'", count, p); /* If the option is prefixed with ! the result should be negated */ negate = false; if (p[0] == '!') { negate = true; p++; } opt = ssh_config_get_match_opcode(p); switch (opt) { case MATCH_ALL: p = ssh_config_get_str_tok(&s, NULL); if (args <= 2 && (p == NULL || p[0] == '\0')) { /* The first or second, but last argument. The "all" keyword * can be prefixed with either "final" or "canonical" * keywords which do not have any effect here. */ if (negate == true) { result = 0; } break; } ssh_set_error(session, SSH_FATAL, "line %d: ERROR - Match all cannot be combined with " "other Match attributes", count); match_error = -1; goto out_match; case MATCH_FINAL: case MATCH_CANONICAL: SSH_LOG(SSH_LOG_DEBUG, "line %d: Unsupported Match keyword '%s', skipping", count, p); /* Not set any result here -- the result is dependent on the * following matches after this keyword */ break; case MATCH_EXEC: /* Skip one argument (including in quotes) */ p = ssh_config_get_token(&s); if (p == NULL || p[0] == '\0') { SSH_LOG(SSH_LOG_TRACE, "line %d: Match keyword " "'%s' requires argument", count, p2); match_error = -1; goto out_match; } if (result != 1) { SSH_LOG(SSH_LOG_DEBUG, "line %d: Skipped match exec " "'%s' as previous conditions already failed.", count, p2); continue; } result &= ssh_match_exec(session, p, negate); args++; break; case MATCH_LOCALUSER: /* Here we match only one argument */ p = ssh_config_get_str_tok(&s, NULL); if (p == NULL || p[0] == '\0') { ssh_set_error(session, SSH_FATAL, "line %d: ERROR - Match localuser keyword " "requires argument", count); match_error = -1; goto out_match; } localuser = ssh_get_local_username(); if (localuser == NULL) { SSH_LOG(SSH_LOG_TRACE, "line %d: Can not get local username " "for conditional matching.", count); match_error = -1; goto out_match; } result &= ssh_config_match(localuser, p, negate); SAFE_FREE(localuser); args++; break; case MATCH_ORIGINALHOST: /* Here we match only one argument */ p = ssh_config_get_str_tok(&s, NULL); if (p == NULL || p[0] == '\0') { ssh_set_error(session, SSH_FATAL, "line %d: ERROR - Match originalhost keyword " "requires argument", count); match_error = -1; goto out_match; } result &= ssh_config_match(session->opts.originalhost, p, negate); args++; break; case MATCH_HOST: /* Here we match only one argument */ p = ssh_config_get_str_tok(&s, NULL); if (p == NULL || p[0] == '\0') { ssh_set_error(session, SSH_FATAL, "line %d: ERROR - Match host keyword " "requires argument", count); match_error = -1; goto out_match; } result &= ssh_config_match(session->opts.host ? session->opts.host : session->opts.originalhost, p, negate); args++; break; case MATCH_USER: /* Here we match only one argument */ p = ssh_config_get_str_tok(&s, NULL); if (p == NULL || p[0] == '\0') { ssh_set_error(session, SSH_FATAL, "line %d: ERROR - Match user keyword " "requires argument", count); match_error = -1; goto out_match; } result &= ssh_config_match(session->opts.username, p, negate); args++; break; case MATCH_LOCALNETWORK: /* Here we match only one argument */ p = ssh_config_get_str_tok(&s, NULL); if (p == NULL || p[0] == '\0') { ssh_set_error(session, SSH_FATAL, "line %d: ERROR - Match local network keyword" "requires argument", count); match_error = -1; goto out_match; } #ifdef HAVE_IFADDRS_H rv = match_cidr_address_list(NULL, p, -1); if (rv == -1) { ssh_set_error(session, SSH_FATAL, "line %d: ERROR - List invalid entry: %s", count, p); match_error = -1; goto out_match; } rv = ssh_match_localnetwork(p, negate); if (rv == -1) { ssh_set_error(session, SSH_FATAL, "line %d: ERROR - Error while retrieving " "network interface information -" " List entry: %s", count, p); match_error = -1; goto out_match; } result &= rv; #else /* HAVE_IFADDRS_H */ ssh_set_error(session, SSH_FATAL, "line %d: ERROR - match localnetwork " "not supported on this platform", count); match_error = -1; goto out_match; #endif /* HAVE_IFADDRS_H */ args++; break; case MATCH_VERSION: /* Here we match only one argument */ p = ssh_config_get_str_tok(&s, NULL); if (p == NULL || p[0] == '\0') { ssh_set_error(session, SSH_FATAL, "line %d: ERROR - Match version keyword " "requires argument", count); match_error = -1; goto out_match; } version = "libssh_" SSH_STRINGIFY(LIBSSH_VERSION); result &= ssh_config_match(version, p, negate); args++; break; case MATCH_TAGGED: /* Here we match exactly one argument. * An explicit empty argument ('tagged=' or 'tagged ""') is * allowed, but a missing argument is an error. */ p = ssh_config_get_token_info(&s, &arg_info); if (arg_info.invalid) { ssh_set_error(session, SSH_FATAL, "line %d: ERROR - Match tagged keyword " "contains unterminated quotes", count); match_error = -1; goto out_match; } /* found=false means there was no argument token at all. An * explicit empty token ("") still reports found=true. */ if (!arg_info.found) { if (keyword_info.had_equal) { p = ""; } else { ssh_set_error(session, SSH_FATAL, "line %d: ERROR - Match tagged keyword " "requires argument", count); match_error = -1; goto out_match; } } result &= ssh_config_match_tagged(session->opts.tag, p, negate); /* MATCH_TAGGED explicitly allows an empty pattern ('tagged=' * or 'tagged ""') to mean "match when no tag is set". That * leaves p pointing at "", so the outer do-while loop would * terminate early and skip any remaining predicates on the * same Match line. Restore p to the keyword token before * iterating. */ p = p2; args++; break; case MATCH_UNKNOWN: default: SSH_LOG(SSH_LOG_WARN, "Unknown argument '%s' for Match keyword. Not matching", p); result = 0; break; } } while (p != NULL && p[0] != '\0'); if (args == 0) { SSH_LOG(SSH_LOG_WARN, "ERROR - Match keyword requires an argument. Not matching"); result = 0; } out_match: SAFE_FREE(localuser); if (match_error != 0) { SAFE_FREE(x); return -1; } *parsing = result; break; } case SOC_HOST: { int ok = 0, result = -1; *parsing = 0; lowerhost = (session->opts.originalhost) ? ssh_lowercase(session->opts.originalhost) : NULL; for (p = ssh_config_get_str_tok(&s, NULL); p != NULL && p[0] != '\0'; p = ssh_config_get_str_tok(&s, NULL)) { if (ok >= 0) { ok = match_hostname(lowerhost, p, strlen(p)); if (result == -1 && ok < 0) { result = 0; } else if (result == -1 && ok > 0) { result = 1; } } } SAFE_FREE(lowerhost); if (result != -1) { *parsing = result; } break; } case SOC_TAG: p = ssh_config_get_str_tok(&s, NULL); if (p == NULL) { ssh_set_error(session, SSH_FATAL, "line %d: ERROR - Tag keyword requires argument", count); SAFE_FREE(x); return SSH_ERROR; } if (*parsing) { char *tag = NULL; tag = strdup(p); if (tag == NULL) { ssh_set_error_oom(session); SAFE_FREE(x); return SSH_ERROR; } SAFE_FREE(session->opts.tag); session->opts.tag = tag; } break; case SOC_HOSTNAME: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { int rc; bool had_expansion = strchr(p, '%') != NULL; bool needs_host = false; bool has_unknown = false; rc = ssh_config_scan_hostname_tokens(session, p, &needs_host, &has_unknown); if (rc < 0) { SAFE_FREE(x); return -1; } if (had_expansion) { if (!has_unknown && (!needs_host || session->opts.host != NULL || session->opts.originalhost != NULL)) { char *expanded = ssh_path_expand_hostname(session, p); if (expanded == NULL) { SAFE_FREE(x); return -1; } session->opts.config_hostname_only = true; rv = ssh_options_set(session, SSH_OPTIONS_HOST, expanded); session->opts.config_hostname_only = false; free(expanded); if (rv != SSH_OK) { /* Expanded HostName values remain fatal if host * validation rejects the resulting hostname. */ SAFE_FREE(x); return -1; } } else { char *hostname_pattern = ssh_config_lowercase_hostname_pattern(p); if (hostname_pattern == NULL) { ssh_set_error_oom(session); SAFE_FREE(x); return -1; } SAFE_FREE(session->opts.config_hostname); session->opts.config_hostname = hostname_pattern; } } else { char *lower = ssh_lowercase(p); if (lower == NULL) { ssh_set_error_oom(session); SAFE_FREE(x); return -1; } session->opts.config_hostname_only = true; ssh_options_set(session, SSH_OPTIONS_HOST, lower); session->opts.config_hostname_only = false; free(lower); } } break; case SOC_PORT: l = ssh_config_get_long(&s, -1); CHECK_COND_OR_FAIL(l <= 0 || l > 65535, "Invalid argument"); if (*parsing) { i = (int)l; ssh_options_set(session, SSH_OPTIONS_PORT, &i); } break; case SOC_USERNAME: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_USER, p); } break; case SOC_IDENTITY: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_ADD_IDENTITY, p); } break; case SOC_CIPHERS: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_CIPHERS_C_S, p); ssh_options_set(session, SSH_OPTIONS_CIPHERS_S_C, p); } break; case SOC_MACS: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_HMAC_C_S, p); ssh_options_set(session, SSH_OPTIONS_HMAC_S_C, p); } break; case SOC_COMPRESSION: i = ssh_config_get_token_value(&s, compression_map, -1); CHECK_COND_OR_FAIL(i < 0, "Invalid argument"); if (*parsing) { if (i) { ssh_options_set(session, SSH_OPTIONS_COMPRESSION, "yes"); } else { ssh_options_set(session, SSH_OPTIONS_COMPRESSION, "no"); } } break; case SOC_TIMEOUT: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); cmp = strcmp(p, "none"); if (cmp == 0) { if (*parsing) { session->opts.timeout = (unsigned long)SSH_TIMEOUT_INFINITE; session->opts.timeout_usec = 0; } break; } l = ssh_config_convtime(p, -1); CHECK_COND_OR_FAIL(l < 0, "Invalid argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_TIMEOUT, &l); } break; case SOC_STRICTHOSTKEYCHECK: i = ssh_config_get_strict_hostkey(&s, -1); CHECK_COND_OR_FAIL(i < 0, "Invalid argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_STRICTHOSTKEYCHECK, &i); } break; case SOC_KNOWNHOSTS: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, p); } break; case SOC_PROXYCOMMAND: p = ssh_config_get_cmd(&s); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); /* We share the seen value with the ProxyJump */ if (*parsing && !seen[SOC_PROXYJUMP]) { ssh_options_set(session, SSH_OPTIONS_PROXYCOMMAND, p); } break; case SOC_PROXYJUMP: p = ssh_config_get_str_tok(&s, NULL); if (p == NULL) { SAFE_FREE(x); return -1; } /* We share the seen value with the ProxyCommand */ rv = ssh_config_parse_proxy_jump(session, p, (*parsing && !seen[SOC_PROXYCOMMAND])); if (rv != SSH_OK) { SAFE_FREE(x); return -1; } break; case SOC_GSSAPISERVERIDENTITY: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_GSSAPI_SERVER_IDENTITY, p); } break; case SOC_GSSAPICLIENTIDENTITY: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_GSSAPI_CLIENT_IDENTITY, p); } break; case SOC_GSSAPIDELEGATECREDENTIALS: i = ssh_config_get_yesno(&s, -1); CHECK_COND_OR_FAIL(i < 0, "Invalid argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_GSSAPI_DELEGATE_CREDENTIALS, &i); } break; case SOC_BINDADDRESS: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_BINDADDR, p); } break; case SOC_GLOBALKNOWNHOSTSFILE: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_GLOBAL_KNOWNHOSTS, p); } break; case SOC_LOGLEVEL: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { int value = -1; if (strcasecmp(p, "quiet") == 0) { value = SSH_LOG_NONE; } else if (strcasecmp(p, "fatal") == 0 || strcasecmp(p, "error")== 0) { value = SSH_LOG_WARN; } else if (strcasecmp(p, "verbose") == 0 || strcasecmp(p, "info") == 0) { value = SSH_LOG_INFO; } else if (strcasecmp(p, "DEBUG") == 0 || strcasecmp(p, "DEBUG1") == 0) { value = SSH_LOG_DEBUG; } else if (strcasecmp(p, "DEBUG2") == 0 || strcasecmp(p, "DEBUG3") == 0) { value = SSH_LOG_TRACE; } CHECK_COND_OR_FAIL(value == -1, "Invalid value"); if (value != -1) { ssh_options_set(session, SSH_OPTIONS_LOG_VERBOSITY, &value); } } break; case SOC_HOSTKEYALGORITHMS: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, p); } break; case SOC_PUBKEYACCEPTEDKEYTYPES: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_PUBLICKEY_ACCEPTED_TYPES, p); } break; case SOC_KEXALGORITHMS: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_KEY_EXCHANGE, p); } break; case SOC_REKEYLIMIT: /* Parse the data limit */ p = ssh_config_get_str_tok(&s, NULL); if (p == NULL) { CHECK_COND_OR_FAIL(1, "Missing data limit"); break; } else if (strcmp(p, "default") == 0) { /* Default rekey limits enforced automatically */ ll = 0; } else { char *endp = NULL; ll = strtoll(p, &endp, 10); if (p == endp || ll < 0) { CHECK_COND_OR_FAIL(1, "Invalid data limit"); break; } switch (*endp) { case 'g': case 'G': if (ll > LLONG_MAX / 1024) { SSH_LOG(SSH_LOG_TRACE, "Possible overflow of rekey limit"); ll = -1; break; } ll = ll * 1024; FALL_THROUGH; case 'm': case 'M': if (ll > LLONG_MAX / 1024) { SSH_LOG(SSH_LOG_TRACE, "Possible overflow of rekey limit"); ll = -1; break; } ll = ll * 1024; FALL_THROUGH; case 'k': case 'K': if (ll > LLONG_MAX / 1024) { SSH_LOG(SSH_LOG_TRACE, "Possible overflow of rekey limit"); ll = -1; break; } ll = ll * 1024; endp++; FALL_THROUGH; case '\0': /* just the number */ break; default: /* Ignore invalid suffix and trailing garbage */ SSH_LOG(SSH_LOG_TRACE, "Ignoring invalid suffix"); break; } } CHECK_COND_OR_FAIL(ll < 0, "Invalid data limit"); CHECK_COND_OR_FAIL(ll > 0 && ll < 16, "RekeyLimit too small"); if (*parsing) { uint64_t v = (uint64_t)ll; ssh_options_set(session, SSH_OPTIONS_REKEY_DATA, &v); } /* Parse the time limit */ p = ssh_config_get_str_tok(&s, NULL); if (p == NULL) { break; } else if (strcmp(p, "none") == 0) { ll = 0; } else { char *endp = NULL; ll = strtoll(p, &endp, 10); if (p == endp || ll < 0) { /* No number or negative */ CHECK_COND_OR_FAIL(1, "Invalid time limit"); break; } switch (*endp) { case 'w': case 'W': if (ll > LLONG_MAX / 7) { SSH_LOG(SSH_LOG_TRACE, "Possible overflow of rekey limit"); ll = -1; break; } ll = ll * 7; FALL_THROUGH; case 'd': case 'D': if (ll > LLONG_MAX / 24) { SSH_LOG(SSH_LOG_TRACE, "Possible overflow of rekey limit"); ll = -1; break; } ll = ll * 24; FALL_THROUGH; case 'h': case 'H': if (ll > LLONG_MAX / 60) { SSH_LOG(SSH_LOG_TRACE, "Possible overflow of rekey limit"); ll = -1; break; } ll = ll * 60; FALL_THROUGH; case 'm': case 'M': if (ll > LLONG_MAX / 60) { SSH_LOG(SSH_LOG_TRACE, "Possible overflow of rekey limit"); ll = -1; break; } ll = ll * 60; FALL_THROUGH; case 's': case 'S': endp++; FALL_THROUGH; case '\0': /* just the number */ break; default: /* Ignore invalid suffix and trailing garbage */ SSH_LOG(SSH_LOG_TRACE, "Ignoring invalid suffix"); break; } } CHECK_COND_OR_FAIL(ll < 0, "Invalid time limit"); if (ll > -1 && *parsing) { uint32_t v = (uint32_t)ll; ssh_options_set(session, SSH_OPTIONS_REKEY_TIME, &v); } break; case SOC_GSSAPIAUTHENTICATION: case SOC_KBDINTERACTIVEAUTHENTICATION: case SOC_PASSWORDAUTHENTICATION: { enum ssh_options_e option = ssh_config_get_auth_option(opcode); i = ssh_config_get_yesno(&s, -1); CHECK_COND_OR_FAIL(i < 0, "Authentication option"); if (*parsing) { ssh_options_set(session, option, &i); } break; } case SOC_PUBKEYAUTHENTICATION: { i = ssh_config_get_pubkey_auth(&s, -1); CHECK_COND_OR_FAIL(i < 0, "Authentication option"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_PUBKEY_AUTH, &i); } break; } case SOC_NA: CHECK_COND_OR_FAIL(1, "Unapplicable option"); break; case SOC_UNSUPPORTED: CHECK_COND_OR_FAIL(1, "Unsupported option"); break; case SOC_UNKNOWN: CHECK_COND_OR_FAIL(1, "Unknown option"); break; case SOC_IDENTITYAGENT: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_IDENTITY_AGENT, p); } break; case SOC_IDENTITIESONLY: i = ssh_config_get_yesno(&s, -1); CHECK_COND_OR_FAIL(i < 0, "Invalid argument"); if (*parsing) { bool b = i; ssh_options_set(session, SSH_OPTIONS_IDENTITIES_ONLY, &b); } break; case SOC_BATCHMODE: i = ssh_config_get_yesno(&s, -1); CHECK_COND_OR_FAIL(i < 0, "Invalid argument"); if (*parsing) { bool b = i; ssh_options_set(session, SSH_OPTIONS_BATCH_MODE, &b); } break; case SOC_PREFERRED_AUTHENTICATIONS: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_PREFERRED_AUTHENTICATIONS, p); } break; case SOC_NUMBER_OF_PASSWORD_PROMPTS: l = ssh_config_get_long(&s, -1); CHECK_COND_OR_FAIL(l <= 0 || l > INT_MAX, "Invalid argument"); if (*parsing) { i = (int)l; ssh_options_set(session, SSH_OPTIONS_NUMBER_OF_PASSWORD_PROMPTS, &i); } break; case SOC_REQUEST_TTY: { static const struct ssh_config_token_value_map request_tty_map[] = { {"no", SSH_REQUEST_TTY_NO}, {"yes", SSH_REQUEST_TTY_YES}, {"auto", SSH_REQUEST_TTY_AUTO}, {"force", SSH_REQUEST_TTY_FORCE}, {NULL, 0}, }; i = ssh_config_get_token_value(&s, request_tty_map, -1); CHECK_COND_OR_FAIL(i < 0, "Invalid argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_REQUEST_TTY, &i); } break; } case SOC_ESCAPE_CHAR: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { int value = 0; if (strcasecmp(p, "none") == 0) { value = -1; } else if (p[0] == '^' && (unsigned char)p[1] >= 64 && (unsigned char)p[1] < 128 && p[2] == '\0') { /* Control character notation like ^C for Ctrl-C */ value = (unsigned char)p[1] & 31; } else if (p[0] != '\0' && p[1] == '\0') { value = (unsigned char)p[0]; } else { CHECK_COND_OR_FAIL(1, "Invalid argument"); break; } ssh_options_set(session, SSH_OPTIONS_ESCAPE_CHAR, &value); } break; case SOC_LOCAL_FORWARD: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); p2 = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p2 == NULL, "Missing remote destination"); if (*parsing) { char buf[MAX_LINE_SIZE] = {0}; len = snprintf(buf, sizeof(buf), "%s %s", p, p2); CHECK_COND_OR_FAIL(len >= (int)sizeof(buf), "Forwarding specification too long"); ssh_options_set(session, SSH_OPTIONS_LOCAL_FORWARD, buf); } break; case SOC_REMOTE_FORWARD: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); p2 = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p2 == NULL, "Missing remote destination"); if (*parsing) { char buf[MAX_LINE_SIZE] = {0}; len = snprintf(buf, sizeof(buf), "%s %s", p, p2); CHECK_COND_OR_FAIL(len >= (int)sizeof(buf), "Forwarding specification too long"); ssh_options_set(session, SSH_OPTIONS_REMOTE_FORWARD, buf); } break; case SOC_EXIT_ON_FORWARD_FAILURE: i = ssh_config_get_yesno(&s, -1); CHECK_COND_OR_FAIL(i < 0, "Invalid argument"); if (*parsing) { bool b = (i == 1) ? true : false; ssh_options_set(session, SSH_OPTIONS_EXIT_ON_FORWARD_FAILURE, &b); } break; case SOC_SEND_ENV: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { while (p != NULL && p[0] != '\0') { ssh_options_set(session, SSH_OPTIONS_SEND_ENV, p); p = ssh_config_get_str_tok(&s, NULL); } } break; case SOC_CONTROLMASTER: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "ControlMaster"); if (*parsing) { int value = -1; if (strcasecmp(p, "auto") == 0) { value = SSH_CONTROL_MASTER_AUTO; } else if (strcasecmp(p, "yes") == 0) { value = SSH_CONTROL_MASTER_YES; } else if (strcasecmp(p, "no") == 0) { value = SSH_CONTROL_MASTER_NO; } else if (strcasecmp(p, "autoask") == 0) { value = SSH_CONTROL_MASTER_AUTOASK; } else if (strcasecmp(p, "ask") == 0) { value = SSH_CONTROL_MASTER_ASK; } CHECK_COND_OR_FAIL(value == -1, "Invalid argument"); if (value != -1) { ssh_options_set(session, SSH_OPTIONS_CONTROL_MASTER, &value); } } break; case SOC_CONTROLPATH: p = ssh_config_get_str_tok(&s, NULL); if (p == NULL) { SAFE_FREE(x); return -1; } if (*parsing) { ssh_options_set(session, SSH_OPTIONS_CONTROL_PATH, p); } break; case SOC_CERTIFICATE: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_CERTIFICATE, p); } break; case SOC_GSSAPIKEYEXCHANGE: { i = ssh_config_get_yesno(&s, -1); CHECK_COND_OR_FAIL(i < 0, "Invalid argument"); if (*parsing) { bool b = (i == 1) ? true : false; ssh_options_set(session, SSH_OPTIONS_GSSAPI_KEY_EXCHANGE, &b); } break; } case SOC_GSSAPIKEXALGORITHMS: p = ssh_config_get_str_tok(&s, NULL); CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); if (*parsing) { ssh_options_set(session, SSH_OPTIONS_GSSAPI_KEY_EXCHANGE_ALGS, p); } break; case SOC_REQUIRED_RSA_SIZE: l = ssh_config_get_long(&s, -1); CHECK_COND_OR_FAIL(l < 0 || l > INT_MAX, "Invalid argument"); if (*parsing) { i = (int)l; ssh_options_set(session, SSH_OPTIONS_RSA_MIN_SIZE, &i); } break; case SOC_ADDRESSFAMILY: p = ssh_config_get_str_tok(&s, NULL); if (p == NULL) { SSH_LOG(SSH_LOG_WARNING, "line %d: no argument after keyword \"addressfamily\"", count); SAFE_FREE(x); return SSH_ERROR; } if (*parsing) { int value = -1; if (strcasecmp(p, "any") == 0) { value = SSH_ADDRESS_FAMILY_ANY; } else if (strcasecmp(p, "inet") == 0) { value = SSH_ADDRESS_FAMILY_INET; } else if (strcasecmp(p, "inet6") == 0) { value = SSH_ADDRESS_FAMILY_INET6; } else { SSH_LOG(SSH_LOG_WARNING, "line %d: invalid argument \"%s\"", count, p); SAFE_FREE(x); return SSH_ERROR; } ssh_options_set(session, SSH_OPTIONS_ADDRESS_FAMILY, &value); } break; default: ssh_set_error(session, SSH_FATAL, "ERROR - unimplemented opcode: %d", opcode); SAFE_FREE(x); return -1; break; } SAFE_FREE(x); return 0; } #undef CHECK_COND_OR_FAIL int ssh_config_parse_line(ssh_session session, const char *line, unsigned int count, int *parsing, unsigned int depth, bool global) { return ssh_config_parse_line_internal(session, line, count, parsing, depth, global, false, false); } int ssh_config_parse_line_cli(ssh_session session, const char *line) { int parsing = 1; return ssh_config_parse_line_internal(session, line, 0, &parsing, 0, false, true, true); } /** * @brief Parse configuration from a file pointer * * @param[in] session The ssh session * @param[in] fp A valid file pointer * @param[in] global Whether the config is global or not * * @returns 0 on successful parsing the configuration file, -1 on error */ int ssh_config_parse(ssh_session session, FILE *fp, bool global) { char line[MAX_LINE_SIZE] = {0}; unsigned int count = 0; int parsing, rv; parsing = 1; while (fgets(line, sizeof(line), fp)) { count++; rv = ssh_config_parse_line(session, line, count, &parsing, 0, global); if (rv < 0) { return -1; } } return 0; } /** * @brief Parse configuration file and set the options to the given session * * @param[in] session The ssh session * @param[in] filename The path to the ssh configuration file * * @returns 0 on successful parsing the configuration file, -1 on error */ int ssh_config_parse_file(ssh_session session, const char *filename) { FILE *fp = NULL; int rv; bool global = 0; fp = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE); if (fp == NULL) { /* The underlying function logs the reasons */ return 0; } rv = strcmp(filename, GLOBAL_CLIENT_CONFIG); #ifdef USR_GLOBAL_CLIENT_CONFIG if (rv != 0) { rv = strcmp(filename, USR_GLOBAL_CLIENT_CONFIG); } #endif if (rv == 0) { global = true; } SSH_LOG(SSH_LOG_PACKET, "Reading configuration data from %s", filename); rv = ssh_config_parse(session, fp, global); fclose(fp); return rv; } /** * @brief Parse configuration string and set the options to the given session * * @param[in] session The ssh session * @param[in] input Null terminated string containing the configuration * * @returns SSH_OK on successful parsing the configuration string, * SSH_ERROR on error */ int ssh_config_parse_string(ssh_session session, const char *input) { char line[MAX_LINE_SIZE] = {0}; const char *c = input, *line_start = input; unsigned int line_num = 0; size_t line_len; int parsing, rv; SSH_LOG(SSH_LOG_DEBUG, "Reading configuration data from string:"); SSH_LOG(SSH_LOG_DEBUG, "START\n%s\nEND", input); parsing = 1; while (1) { line_num++; line_start = c; c = strchr(line_start, '\n'); if (c == NULL) { /* if there is no newline at the end of the string */ c = strchr(line_start, '\0'); } if (c == NULL) { /* should not happen, would mean a string without trailing '\0' */ SSH_LOG(SSH_LOG_TRACE, "No trailing '\\0' in config string"); return SSH_ERROR; } line_len = c - line_start; if (line_len > MAX_LINE_SIZE - 1) { SSH_LOG(SSH_LOG_TRACE, "Line %u too long: %zu characters", line_num, line_len); return SSH_ERROR; } memcpy(line, line_start, line_len); line[line_len] = '\0'; SSH_LOG(SSH_LOG_DEBUG, "Line %u: %s", line_num, line); rv = ssh_config_parse_line(session, line, line_num, &parsing, 0, false); if (rv < 0) { return SSH_ERROR; } if (*c == '\0') { break; } c++; } return SSH_OK; }