diff --git a/auth.c b/auth.c index 3b380d9bbd20..c6ca6dc8ef0d 100644 --- a/auth.c +++ b/auth.c @@ -381,7 +381,7 @@ auth_root_allowed(struct ssh *ssh, const char *method) * This returns a buffer allocated by xmalloc. */ char * -expand_authorized_keys(const char *filename, struct passwd *pw) +expand_user_file(const char *filename, struct passwd *pw, const char *filetype) { char *file, uidstr[32], ret[PATH_MAX]; int i; @@ -400,7 +400,7 @@ expand_authorized_keys(const char *filename, struct passwd *pw) i = snprintf(ret, sizeof(ret), "%s/%s", pw->pw_dir, file); if (i < 0 || (size_t)i >= sizeof(ret)) - fatal("expand_authorized_keys: path too long"); + fatal("expand_user_file (%s): path too long", filetype); free(file); return (xstrdup(ret)); } @@ -410,7 +410,8 @@ authorized_principals_file(struct passwd *pw) { if (options.authorized_principals_file == NULL) return NULL; - return expand_authorized_keys(options.authorized_principals_file, pw); + return expand_user_file(options.authorized_principals_file, pw, + "AuthorizedPrincipals"); } /* return ok if key exists in sysfile or userfile */ diff --git a/auth.h b/auth.h index 6d2d3976234e..2a6e43535b9b 100644 --- a/auth.h +++ b/auth.h @@ -190,7 +190,7 @@ int bsdauth_respond(void *, u_int, char **); int allowed_user(struct ssh *, struct passwd *); struct passwd * getpwnamallow(struct ssh *, const char *user); -char *expand_authorized_keys(const char *, struct passwd *pw); +char *expand_user_file(const char *, struct passwd *pw, const char *); char *authorized_principals_file(struct passwd *); int auth_key_is_revoked(struct sshkey *); diff --git a/auth2-pubkey.c b/auth2-pubkey.c index 3f49e1df3b7d..94b3556ce7ca 100644 --- a/auth2-pubkey.c +++ b/auth2-pubkey.c @@ -777,8 +777,8 @@ user_key_allowed(struct ssh *ssh, struct passwd *pw, struct sshkey *key, for (i = 0; !success && i < options.num_authkeys_files; i++) { if (strcasecmp(options.authorized_keys_files[i], "none") == 0) continue; - file = expand_authorized_keys( - options.authorized_keys_files[i], pw); + file = expand_user_file(options.authorized_keys_files[i], pw, + "AuthorizedKeys"); success = user_key_allowed2(pw, key, file, remote_ip, remote_host, &opts); free(file); diff --git a/servconf.c b/servconf.c index 4b434909ab2f..5093e2273526 100644 --- a/servconf.c +++ b/servconf.c @@ -115,6 +115,7 @@ initialize_server_options(ServerOptions *options) options->x11_use_localhost = -1; options->permit_tty = -1; options->permit_user_rc = -1; + options->num_user_rc_files = 0; options->xauth_location = NULL; options->strict_modes = -1; options->tcp_keep_alive = -1; @@ -141,6 +142,7 @@ initialize_server_options(ServerOptions *options) options->permit_empty_passwd = -1; options->permit_user_env = -1; options->permit_user_env_allowlist = NULL; + options->num_user_env_files = 0; options->compression = -1; options->rekey_limit = -1; options->rekey_interval = -1; @@ -332,6 +334,11 @@ fill_default_server_options(ServerOptions *options) options->permit_tty = 1; if (options->permit_user_rc == -1) options->permit_user_rc = 1; + if (options->num_user_rc_files == 0) + opt_array_append("[default]", 0, "UserRCFiles", + &options->user_rc_files, + &options->num_user_rc_files, + _PATH_SSH_USER_RC); if (options->strict_modes == -1) options->strict_modes = 1; if (options->tcp_keep_alive == -1) @@ -372,6 +379,11 @@ fill_default_server_options(ServerOptions *options) options->permit_user_env = 0; options->permit_user_env_allowlist = NULL; } + if (options->num_user_env_files == 0) + opt_array_append("[default]", 0, "UserEnvFiles", + &options->user_env_files, + &options->num_user_env_files, + _PATH_SSH_USER_DIR "/environment"); if (options->compression == -1) #ifdef WITH_ZLIB options->compression = COMP_DELAYED; @@ -508,7 +520,8 @@ typedef enum { sPrintMotd, sPrintLastLog, sIgnoreRhosts, sX11Forwarding, sX11DisplayOffset, sX11UseLocalhost, sPermitTTY, sStrictModes, sEmptyPasswd, sTCPKeepAlive, - sPermitUserEnvironment, sAllowTcpForwarding, sCompression, + sPermitUserEnvironment, sUserEnvironmentFile, + sAllowTcpForwarding, sCompression, sRekeyLimit, sAllowUsers, sDenyUsers, sAllowGroups, sDenyGroups, sIgnoreUserKnownHosts, sCiphers, sMacs, sPidFile, sModuliFile, sGatewayPorts, sPubkeyAuthentication, sPubkeyAcceptedAlgorithms, @@ -526,7 +539,7 @@ typedef enum { sAuthorizedPrincipalsCommand, sAuthorizedPrincipalsCommandUser, sKexAlgorithms, sCASignatureAlgorithms, sIPQoS, sVersionAddendum, sAuthorizedKeysCommand, sAuthorizedKeysCommandUser, - sAuthenticationMethods, sHostKeyAgent, sPermitUserRC, + sAuthenticationMethods, sHostKeyAgent, sPermitUserRC, sUserRCFile, sStreamLocalBindMask, sStreamLocalBindUnlink, sAllowStreamLocalForwarding, sFingerprintHash, sDisableForwarding, sExposeAuthInfo, sRDomain, sPubkeyAuthOptions, sSecurityKeyProvider, @@ -628,6 +641,7 @@ static struct { { "strictmodes", sStrictModes, SSHCFG_GLOBAL }, { "permitemptypasswords", sEmptyPasswd, SSHCFG_ALL }, { "permituserenvironment", sPermitUserEnvironment, SSHCFG_GLOBAL }, + { "userenvironmentfile", sUserEnvironmentFile, SSHCFG_GLOBAL }, { "uselogin", sDeprecated, SSHCFG_GLOBAL }, { "compression", sCompression, SSHCFG_GLOBAL }, { "rekeylimit", sRekeyLimit, SSHCFG_ALL }, @@ -663,6 +677,7 @@ static struct { { "permittunnel", sPermitTunnel, SSHCFG_ALL }, { "permittty", sPermitTTY, SSHCFG_ALL }, { "permituserrc", sPermitUserRC, SSHCFG_ALL }, + { "userrcfile", sUserRCFile, SSHCFG_ALL }, { "match", sMatch, SSHCFG_ALL }, { "permitopen", sPermitOpen, SSHCFG_ALL }, { "permitlisten", sPermitListen, SSHCFG_ALL }, @@ -1673,6 +1688,30 @@ process_server_config_line_depth(ServerOptions *options, char *line, intptr = &options->permit_user_rc; goto parse_flag; + /* + * These options can contain %X options expanded at + * connect time, so that you can specify paths like: + * + * UserRCFile /etc/ssh_user_rc/%u + */ + case sUserRCFile: + value = options->num_user_rc_files; + while ((arg = argv_next(&ac, &av)) != NULL) { + if (*arg == '\0') { + error("%s line %d: keyword %s empty argument", + filename, linenum, keyword); + goto out; + } + arg2 = tilde_expand_filename(arg, getuid()); + if (*activep && value == 0) { + opt_array_append(filename, linenum, keyword, + &options->user_rc_files, + &options->num_user_rc_files, arg2); + } + free(arg2); + } + break; + case sStrictModes: intptr = &options->strict_modes; goto parse_flag; @@ -1711,6 +1750,30 @@ process_server_config_line_depth(ServerOptions *options, char *line, free(p); break; + /* + * These options can contain %X options expanded at + * connect time, so that you can specify paths like: + * + * UserEnvironmentFile /etc/ssh_user_env/%u + */ + case sUserEnvironmentFile: + value = options->num_user_env_files; + while ((arg = argv_next(&ac, &av)) != NULL) { + if (*arg == '\0') { + error("%s line %d: keyword %s empty argument", + filename, linenum, keyword); + goto out; + } + arg2 = tilde_expand_filename(arg, getuid()); + if (*activep && value == 0) { + opt_array_append(filename, linenum, keyword, + &options->user_env_files, + &options->num_user_env_files, arg2); + } + free(arg2); + } + break; + case sCompression: intptr = &options->compression; multistate_ptr = multistate_compression; @@ -3175,6 +3238,10 @@ dump_config(ServerOptions *o) /* string array arguments */ dump_cfg_strarray_oneline(sAuthorizedKeysFile, o->num_authkeys_files, o->authorized_keys_files); + dump_cfg_strarray_oneline(sUserRCFile, o->num_user_rc_files, + o->user_rc_files); + dump_cfg_strarray_oneline(sUserEnvironmentFile, o->num_user_env_files, + o->user_env_files); dump_cfg_strarray(sHostKeyFile, o->num_host_key_files, o->host_key_files); dump_cfg_strarray(sHostCertificate, o->num_host_cert_files, diff --git a/servconf.h b/servconf.h index ed7b72e8e0e3..ec1a4f9afdc3 100644 --- a/servconf.h +++ b/servconf.h @@ -233,6 +233,11 @@ typedef struct { u_int num_channel_timeouts; int unused_connection_timeout; + + u_int num_user_env_files; /* alternative .ssh/environment files */ + u_int num_user_rc_files; /* alternative .ssh/rc files */ + char **user_env_files; + char **user_rc_files; } ServerOptions; /* Information about the incoming connection as used by Match */ @@ -281,6 +286,8 @@ TAILQ_HEAD(include_list, include_item); M_CP_STROPT(routing_domain); \ M_CP_STROPT(permit_user_env_allowlist); \ M_CP_STRARRAYOPT(authorized_keys_files, num_authkeys_files); \ + M_CP_STRARRAYOPT(user_rc_files, num_user_rc_files); \ + M_CP_STRARRAYOPT(user_env_files, num_user_env_files); \ M_CP_STRARRAYOPT(allow_users, num_allow_users); \ M_CP_STRARRAYOPT(deny_users, num_deny_users); \ M_CP_STRARRAYOPT(allow_groups, num_allow_groups); \ diff --git a/session.c b/session.c index c821dcd4462d..3025762cc24f 100644 --- a/session.c +++ b/session.c @@ -832,6 +832,48 @@ check_quietlogin(Session *s, const char *command) return 0; } +/* + * Check that any custom user environment/rc file is "safe" to run, i.e. + * - If the file is located in the logged-in user's $HOME, it is owned by the + * user and accessible *only* to the user + * - If the file is located outside the user's $HOME, it is owned by the super + * user + */ +static int +safe_user_file(const char *filename, struct passwd *pw) +{ + struct stat st; + char *path; + int safe = 0; + + if (stat(filename, &st) == -1) + return safe; + + path = realpath(filename, NULL); + if (strncmp(path, pw->pw_dir, strlen(pw->pw_dir)) == 0) { + if (st.st_uid != pw->pw_uid) { + fprintf(stderr, "Bad owner on %s, ignoring.\n", path); + goto out; + } + if ((st.st_mode & 077) != 0) { + fprintf(stderr, + "Permissions too open (%3o) on %s, ignoring.\n", + st.st_mode & 0777u, path); + goto out; + } + safe = 1; + goto out; + } + if (st.st_uid != 0 || st.st_gid != 0) { + fprintf(stderr, "%s not root-owned, ignoring.\n", path); + goto out; + } + safe = 1; +out: + free(path); + return safe; +} + /* * Reads environment variables from the given file and adds/overrides them * into the environment. If the file does not exist, this does nothing. @@ -984,7 +1026,7 @@ do_setup_env(struct ssh *ssh, Session *s, const char *shell) char buf[256]; size_t n; u_int i, envsize; - char *ocp, *cp, *value, **env, *laddr; + char *ocp, *cp, *value, **env, *laddr, *userenv = NULL; struct passwd *pw = s->pw; #if !defined (HAVE_LOGIN_CAP) && !defined (HAVE_CYGWIN) char *path = NULL; @@ -1116,12 +1158,16 @@ do_setup_env(struct ssh *ssh, Session *s, const char *shell) } } - /* read $HOME/.ssh/environment. */ + /* read user environment files. */ if (options.permit_user_env) { - snprintf(buf, sizeof buf, "%.200s/%s/environment", - pw->pw_dir, _PATH_SSH_USER_DIR); - read_environment_file(&env, &envsize, buf, - options.permit_user_env_allowlist); + for (i = 0; i < options.num_user_env_files; i++) { + userenv = expand_user_file(options.user_env_files[i], pw, + "UserEnvironment"); + if (safe_user_file(userenv, pw)) + read_environment_file(&env, &envsize, userenv, + options.permit_user_env_allowlist); + } + free(userenv); } #ifdef USE_PAM @@ -1204,29 +1250,34 @@ do_rc_files(struct ssh *ssh, Session *s, const char *shell) char *cmd = NULL, *user_rc = NULL; int do_xauth; struct stat st; + u_int i; do_xauth = s->display != NULL && s->auth_proto != NULL && s->auth_data != NULL; - xasprintf(&user_rc, "%s/%s", s->pw->pw_dir, _PATH_SSH_USER_RC); /* ignore _PATH_SSH_USER_RC for subsystems and admin forced commands */ if (!s->is_subsystem && options.adm_forced_command == NULL && - auth_opts->permit_user_rc && options.permit_user_rc && - stat(user_rc, &st) >= 0) { - if (xasprintf(&cmd, "%s -c '%s %s'", shell, _PATH_BSHELL, - user_rc) == -1) - fatal_f("xasprintf: %s", strerror(errno)); - if (debug_flag) - fprintf(stderr, "Running %s\n", cmd); - f = popen(cmd, "w"); - if (f) { - if (do_xauth) - fprintf(f, "%s %s\n", s->auth_proto, - s->auth_data); - pclose(f); - } else - fprintf(stderr, "Could not run %s\n", - user_rc); + auth_opts->permit_user_rc && options.permit_user_rc) { + for (i = 0; i < options.num_user_rc_files; i++) { + user_rc = expand_user_file(options.user_rc_files[i], + s->pw, "UserRC"); + if (safe_user_file(user_rc, s->pw)) { + if (xasprintf(&cmd, "%s -c '%s %s'", shell, + _PATH_BSHELL, user_rc) == -1) + fatal_f("xasprintf: %s", strerror(errno)); + if (debug_flag) + fprintf(stderr, "Running %s\n", cmd); + f = popen(cmd, "w"); + if (f) { + if (do_xauth) + fprintf(f, "%s %s\n", s->auth_proto, + s->auth_data); + pclose(f); + } else + fprintf(stderr, "Could not run %s\n", + user_rc); + } + } } else if (stat(_PATH_SSH_SYSTEM_RC, &st) >= 0) { if (debug_flag) fprintf(stderr, "Running %s %s\n", _PATH_BSHELL,