diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 13:14:46 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 13:14:46 +0000 |
commit | 025c439e829e0db9ac511cd9c1b8d5fd53475ead (patch) | |
tree | fa6986b4690f991613ffb97cea1f6942427baf5d /plugins/sudoers/match_command.c | |
parent | Initial commit. (diff) | |
download | sudo-025c439e829e0db9ac511cd9c1b8d5fd53475ead.tar.xz sudo-025c439e829e0db9ac511cd9c1b8d5fd53475ead.zip |
Adding upstream version 1.9.15p5.upstream/1.9.15p5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'plugins/sudoers/match_command.c')
-rw-r--r-- | plugins/sudoers/match_command.c | 898 |
1 files changed, 898 insertions, 0 deletions
diff --git a/plugins/sudoers/match_command.c b/plugins/sudoers/match_command.c new file mode 100644 index 0000000..4ac56ee --- /dev/null +++ b/plugins/sudoers/match_command.c @@ -0,0 +1,898 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 1996, 1998-2005, 2007-2023 + * Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Sponsored in part by the Defense Advanced Research Projects + * Agency (DARPA) and Air Force Research Laboratory, Air Force + * Materiel Command, USAF, under agreement number F39502-99-1-0512. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#include <sys/stat.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#ifndef SUDOERS_NAME_MATCH +# ifdef HAVE_GLOB +# include <glob.h> +# else +# include <compat/glob.h> +# endif /* HAVE_GLOB */ +#endif /* SUDOERS_NAME_MATCH */ +#include <dirent.h> +#include <fcntl.h> +#include <errno.h> +#ifdef HAVE_FNMATCH +# include <fnmatch.h> +#else +# include <compat/fnmatch.h> +#endif /* HAVE_FNMATCH */ +#include <regex.h> + +#include <sudoers.h> +#include <gram.h> + +#if !defined(O_EXEC) && defined(O_PATH) +# define O_EXEC O_PATH +#endif + +static int +regex_matches(const char *pattern, const char *str) +{ + const char *errstr; + regex_t re; + int ret; + debug_decl(regex_matches, SUDOERS_DEBUG_MATCH); + + if (!sudo_regex_compile(&re, pattern, &errstr)) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to compile regular expression \"%s\": %s", + pattern, errstr); + debug_return_int(DENY); + } + + if (regexec(&re, str, 0, NULL, 0) == 0) + ret = ALLOW; + else + ret = DENY; + regfree(&re); + + debug_return_int(ret); +} + +static int +command_args_match(struct sudoers_context *ctx, const char *sudoers_cmnd, + const char *sudoers_args) +{ + const char *args = ctx->user.cmnd_args ? ctx->user.cmnd_args : ""; + int flags = 0; + debug_decl(command_args_match, SUDOERS_DEBUG_MATCH); + + /* + * If no args specified in sudoers, any user args are allowed. + * If the empty string is specified in sudoers, no user args are allowed. + */ + if (sudoers_args == NULL) + debug_return_int(ALLOW); + if (strcmp("\"\"", sudoers_args) == 0) + debug_return_int(ctx->user.cmnd_args ? DENY : ALLOW); + + /* + * If args are specified in sudoers, they must match the user args. + * Args are matched either as a regular expression or glob pattern. + */ + if (sudoers_args[0] == '^') { + size_t len = strlen(sudoers_args); + if (len > 0 && sudoers_args[len - 1] == '$') + debug_return_int(regex_matches(sudoers_args, args)); + } + + /* If running as sudoedit, all args are assumed to be paths. */ + if (strcmp(sudoers_cmnd, "sudoedit") == 0) + flags = FNM_PATHNAME; + if (fnmatch(sudoers_args, args, flags) == 0) + debug_return_int(ALLOW); + debug_return_int(DENY); +} + +#ifndef SUDOERS_NAME_MATCH +/* + * Stat file by fd is possible, else by path. + * Returns true on success, else false. + */ +static bool +do_stat(int fd, const char *path, struct stat *sb) +{ + bool ret; + debug_decl(do_stat, SUDOERS_DEBUG_MATCH); + + if (fd != -1) { + ret = fstat(fd, sb) == 0; + } else { + ret = stat(path, sb) == 0; + } + debug_return_bool(ret); +} +#endif /* SUDOERS_NAME_MATCH */ + +/* + * Check whether the fd refers to a shell script with a "#!" shebang. + */ +static bool +is_script(int fd) +{ + bool ret = false; + char magic[2]; + debug_decl(is_script, SUDOERS_DEBUG_MATCH); + + if (pread(fd, magic, 2, 0) == 2) { + if (magic[0] == '#' && magic[1] == '!') + ret = true; + } + debug_return_bool(ret); +} + +/* + * Open path if fdexec is enabled or if a digest is present. + * Returns false on error, else true. + */ +static bool +open_cmnd(const char *path, const struct command_digest_list *digests, int *fdp) +{ + int fd; + debug_decl(open_cmnd, SUDOERS_DEBUG_MATCH); + + /* Only open the file for fdexec or for digest matching. */ + if (def_fdexec != always && TAILQ_EMPTY(digests)) + debug_return_bool(true); + + fd = open(path, O_RDONLY|O_NONBLOCK); +# ifdef O_EXEC + if (fd == -1 && errno == EACCES && TAILQ_EMPTY(digests)) { + /* Try again with O_EXEC if no digest is specified. */ + const int saved_errno = errno; + if ((fd = open(path, O_EXEC)) == -1) + errno = saved_errno; + } +# endif + if (fd == -1) + debug_return_bool(false); + + (void)fcntl(fd, F_SETFD, FD_CLOEXEC); + *fdp = fd; + debug_return_bool(true); +} + +static void +set_cmnd_fd(struct sudoers_context *ctx, int fd, int real_root) +{ + debug_decl(set_cmnd_fd, SUDOERS_DEBUG_MATCH); + + if (ctx->runas.execfd != -1) + close(ctx->runas.execfd); + + if (fd != -1) { + if (def_fdexec == never) { + /* Never use fexedcve() */ + close(fd); + fd = -1; + } else if (is_script(fd)) { + char fdpath[PATH_MAX]; + struct stat sb; + int error, flags; + + /* We can only use fexecve() on a script if /dev/fd/N exists. */ + if (real_root != -1) { + /* Path relative to old root directory. */ + (void)snprintf(fdpath, sizeof(fdpath), "dev/fd/%d", fd); + error = fstatat(real_root, fdpath, &sb, 0); + } else { + /* Absolute path. */ + (void)snprintf(fdpath, sizeof(fdpath), "/dev/fd/%d", fd); + error = stat(fdpath, &sb); + } + if (error != 0) { + /* Missing /dev/fd file, can't use fexecve(). */ + close(fd); + fd = -1; + } else { + /* + * Shell scripts go through namei twice so we can't have the + * close on exec flag set on the fd for fexecve(2). + */ + flags = fcntl(fd, F_GETFD) & ~FD_CLOEXEC; + (void)fcntl(fd, F_SETFD, flags); + } + } + } + + ctx->runas.execfd = fd; + + debug_return; +} + +#ifndef SUDOERS_NAME_MATCH +/* + * Return true if ctx->user.cmnd names one of the inodes in dir, else false. + */ +static int +command_matches_dir(struct sudoers_context *ctx, const char *sudoers_dir, + size_t dlen, int real_root, const struct command_digest_list *digests) +{ + struct stat sudoers_stat; + char path[PATH_MAX]; + int len, fd = -1; + int ret = DENY; + debug_decl(command_matches_dir, SUDOERS_DEBUG_MATCH); + + /* Compare the canonicalized directories, if possible. */ + if (ctx->user.cmnd_dir != NULL) { + char *resolved = canon_path(sudoers_dir); + if (resolved != NULL) { + if (strcmp(resolved, ctx->user.cmnd_dir) != 0) { + canon_path_free(resolved); + goto done; + } + canon_path_free(resolved); + } + } + + /* Check for command in sudoers_dir. */ + len = snprintf(path, sizeof(path), "%s/%s", sudoers_dir, ctx->user.cmnd_base); + if (len < 0 || len >= ssizeof(path)) + goto done; + + /* Open the file for fdexec or for digest matching. */ + if (!open_cmnd(path, digests, &fd)) + goto done; + if (!do_stat(fd, path, &sudoers_stat)) + goto done; + + if (ctx->user.cmnd_stat == NULL || + (ctx->user.cmnd_stat->st_dev == sudoers_stat.st_dev && + ctx->user.cmnd_stat->st_ino == sudoers_stat.st_ino)) { + if (digest_matches(fd, path, digests) != ALLOW) + goto done; + free(ctx->runas.cmnd); + if ((ctx->runas.cmnd = strdup(path)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + } + ret = ALLOW; + goto done; + } + ret = DENY; + +done: + if (fd != -1) + close(fd); + debug_return_int(ret); +} +#else /* SUDOERS_NAME_MATCH */ +/* + * Return true if ctx->user.cmnd names one of the inodes in dir, else false. + */ +static int +command_matches_dir(struct sudoers_context *ctx, const char *sudoers_dir, + size_t dlen, int real_root, const struct command_digest_list *digests) +{ + int fd = -1; + debug_decl(command_matches_dir, SUDOERS_DEBUG_MATCH); + + /* Match ctx->user.cmnd against sudoers_dir. */ + if (strncmp(ctx->user.cmnd, sudoers_dir, dlen) != 0 || ctx->user.cmnd[dlen] != '/') + goto bad; + + /* Make sure ctx->user.cmnd is not in a subdir of sudoers_dir. */ + if (strchr(ctx->user.cmnd + dlen + 1, '\0') != NULL) + goto bad; + + /* Open the file for fdexec or for digest matching. */ + if (!open_cmnd(ctx->user.cmnd, digests, &fd)) + goto bad; + if (digest_matches(fd, ctx->user.cmnd, digests) != ALLOW) + goto bad; + set_cmnd_fd(ctx, fd, real_root); + + debug_return_int(ALLOW); +bad: + if (fd != -1) + close(fd); + debug_return_int(DENY); +} +#endif /* SUDOERS_NAME_MATCH */ + +static int +command_matches_all(struct sudoers_context *ctx, int real_root, + const struct command_digest_list *digests) +{ +#ifndef SUDOERS_NAME_MATCH + struct stat sb; +#endif + int fd = -1; + debug_decl(command_matches_all, SUDOERS_DEBUG_MATCH); + + if (strchr(ctx->user.cmnd, '/') != NULL) { +#ifndef SUDOERS_NAME_MATCH + /* Open the file for fdexec or for digest matching. */ + bool open_error = !open_cmnd(ctx->user.cmnd, digests, &fd); + + /* A non-existent file is not an error for "sudo ALL". */ + if (do_stat(fd, ctx->user.cmnd, &sb)) { + if (open_error) { + /* File exists but we couldn't open it above? */ + goto bad; + } + } +#else + /* Open the file for fdexec or for digest matching. */ + (void)open_cmnd(ctx->user.cmnd, digests, &fd); +#endif + } + + /* Check digest of ctx->user.cmnd since we have no sudoers_cmnd for ALL. */ + if (digest_matches(fd, ctx->user.cmnd, digests) != ALLOW) + goto bad; + set_cmnd_fd(ctx, fd, real_root); + + /* No need to set ctx->runas.cmnd for ALL. */ + debug_return_int(ALLOW); +bad: + if (fd != -1) + close(fd); + debug_return_int(DENY); +} + +static int +command_matches_fnmatch(struct sudoers_context *ctx, const char *sudoers_cmnd, + const char *sudoers_args, int real_root, + const struct command_digest_list *digests) +{ + const char *cmnd = ctx->user.cmnd; + char buf[PATH_MAX]; + int len, fd = -1; +#ifndef SUDOERS_NAME_MATCH + struct stat sb; +#endif + debug_decl(command_matches_fnmatch, SUDOERS_DEBUG_MATCH); + + /* A relative ctx->user.cmnd will not match, try canonicalized version. */ + if (ctx->user.cmnd[0] != '/') { + if (ctx->user.cmnd_dir == NULL) + debug_return_int(DENY); + len = snprintf(buf, sizeof(buf), "%s/%s", ctx->user.cmnd_dir, + ctx->user.cmnd_base); + if (len < 0 || len >= ssizeof(buf)) + debug_return_int(DENY); + cmnd = buf; + } + + /* + * Return ALLOW if fnmatch(3) succeeds AND + * a) there are no args in sudoers OR + * b) there are no args on command line and none required by sudoers OR + * c) there are args in sudoers and on command line and they match + * else return DENY. + */ + if (fnmatch(sudoers_cmnd, cmnd, FNM_PATHNAME) != 0) + debug_return_int(DENY); + + if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) { + /* Open the file for fdexec or for digest matching. */ + if (!open_cmnd(cmnd, digests, &fd)) + goto bad; +#ifndef SUDOERS_NAME_MATCH + if (!do_stat(fd, cmnd, &sb)) + goto bad; +#endif + /* Check digest of cmnd since sudoers_cmnd is a pattern. */ + if (digest_matches(fd, cmnd, digests) != ALLOW) + goto bad; + set_cmnd_fd(ctx, fd, real_root); + + /* No need to set ctx->runas.cmnd since cmnd matches sudoers_cmnd */ + debug_return_int(ALLOW); +bad: + if (fd != -1) + close(fd); + } + debug_return_int(DENY); +} + +static int +command_matches_regex(struct sudoers_context *ctx, const char *sudoers_cmnd, + const char *sudoers_args, int real_root, + const struct command_digest_list *digests) +{ + const char *cmnd = ctx->user.cmnd; + char buf[PATH_MAX]; + int len, fd = -1; +#ifndef SUDOERS_NAME_MATCH + struct stat sb; +#endif + debug_decl(command_matches_regex, SUDOERS_DEBUG_MATCH); + + /* A relative ctx->user.cmnd will not match, try canonicalized version. */ + if (ctx->user.cmnd[0] != '/') { + if (ctx->user.cmnd_dir == NULL) + debug_return_int(DENY); + len = snprintf(buf, sizeof(buf), "%s/%s", ctx->user.cmnd_dir, + ctx->user.cmnd_base); + if (len < 0 || len >= ssizeof(buf)) + debug_return_int(DENY); + cmnd = buf; + } + + /* + * Return ALLOW if sudoers_cmnd regex matches cmnd AND + * a) there are no args in sudoers OR + * b) there are no args on command line and none required by sudoers OR + * c) there are args in sudoers and on command line and they match + * else return DENY. + */ + if (regex_matches(sudoers_cmnd, cmnd) != ALLOW) + debug_return_int(DENY); + + if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) { + /* Open the file for fdexec or for digest matching. */ + if (!open_cmnd(cmnd, digests, &fd)) + goto bad; +#ifndef SUDOERS_NAME_MATCH + if (!do_stat(fd, cmnd, &sb)) + goto bad; +#endif + /* Check digest of cmnd since sudoers_cmnd is a pattern. */ + if (digest_matches(fd, cmnd, digests) != ALLOW) + goto bad; + set_cmnd_fd(ctx, fd, real_root); + + /* No need to set ctx->runas.cmnd since cmnd matches sudoers_cmnd */ + debug_return_int(ALLOW); +bad: + if (fd != -1) + close(fd); + } + debug_return_int(DENY); +} + +#ifndef SUDOERS_NAME_MATCH +static int +command_matches_glob(struct sudoers_context *ctx, const char *sudoers_cmnd, + const char *sudoers_args, int real_root, + const struct command_digest_list *digests) +{ + struct stat sudoers_stat; + bool bad_digest = false; + char **ap, *base, *cp; + int fd = -1; + size_t dlen; + glob_t gl; + debug_decl(command_matches_glob, SUDOERS_DEBUG_MATCH); + + /* + * First check to see if we can avoid the call to glob(3). + * Short circuit if there are no meta chars in the command itself + * and ctx->user.cmnd_base and basename(sudoers_cmnd) don't match. + */ + dlen = strlen(sudoers_cmnd); + if (sudoers_cmnd[dlen - 1] != '/') { + base = sudo_basename(sudoers_cmnd); + if (!has_meta(base) && strcmp(ctx->user.cmnd_base, base) != 0) + debug_return_int(DENY); + } + + /* + * Return ALLOW if we find a match in the glob(3) results AND + * a) there are no args in sudoers OR + * b) there are no args on command line and none required by sudoers OR + * c) there are args in sudoers and on command line and they match + * else return DENY. + */ + if (glob(sudoers_cmnd, GLOB_NOSORT, NULL, &gl) != 0 || gl.gl_pathc == 0) { + globfree(&gl); + debug_return_int(DENY); + } + + /* If ctx->user.cmnd is fully-qualified, check for an exact match. */ + if (ctx->user.cmnd[0] == '/') { + for (ap = gl.gl_pathv; (cp = *ap) != NULL; ap++) { + if (fd != -1) { + close(fd); + fd = -1; + } + + if (strcmp(cp, ctx->user.cmnd) != 0) + continue; + /* Open the file for fdexec or for digest matching. */ + if (!open_cmnd(cp, digests, &fd)) + continue; + if (!do_stat(fd, cp, &sudoers_stat)) + continue; + if (ctx->user.cmnd_stat == NULL || + (ctx->user.cmnd_stat->st_dev == sudoers_stat.st_dev && + ctx->user.cmnd_stat->st_ino == sudoers_stat.st_ino)) { + /* There could be multiple matches, check digest early. */ + if (digest_matches(fd, cp, digests) != ALLOW) { + bad_digest = true; + continue; + } + free(ctx->runas.cmnd); + if ((ctx->runas.cmnd = strdup(cp)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + cp = NULL; /* fail closed */ + } + } else { + /* Paths match, but st_dev and st_ino are different. */ + cp = NULL; /* fail closed */ + } + goto done; + } + } + /* No exact match, compare basename, cmnd_dir, st_dev and st_ino. */ + if (!bad_digest) { + for (ap = gl.gl_pathv; (cp = *ap) != NULL; ap++) { + if (fd != -1) { + close(fd); + fd = -1; + } + + /* If it ends in '/' it is a directory spec. */ + dlen = strlen(cp); + if (cp[dlen - 1] == '/') { + if (command_matches_dir(ctx, cp, dlen, real_root, digests) == ALLOW) { + globfree(&gl); + debug_return_int(ALLOW); + } + continue; + } + + /* Only proceed if ctx->user.cmnd_base and basename(cp) match */ + base = sudo_basename(cp); + if (strcmp(ctx->user.cmnd_base, base) != 0) + continue; + + /* Compare the canonicalized parent directories, if possible. */ + if (ctx->user.cmnd_dir != NULL) { + char *slash = strrchr(cp, '/'); + if (slash != NULL) { + char *resolved; + *slash = '\0'; + resolved = canon_path(cp); + *slash = '/'; + if (resolved != NULL) { + /* Canonicalized directories must match. */ + int result = strcmp(resolved, ctx->user.cmnd_dir); + canon_path_free(resolved); + if (result != 0) + continue; + } + } + } + + /* Open the file for fdexec or for digest matching. */ + if (!open_cmnd(cp, digests, &fd)) + continue; + if (!do_stat(fd, cp, &sudoers_stat)) + continue; + if (ctx->user.cmnd_stat == NULL || + (ctx->user.cmnd_stat->st_dev == sudoers_stat.st_dev && + ctx->user.cmnd_stat->st_ino == sudoers_stat.st_ino)) { + if (digest_matches(fd, cp, digests) != ALLOW) + continue; + free(ctx->runas.cmnd); + if ((ctx->runas.cmnd = strdup(cp)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + cp = NULL; /* fail closed */ + } + goto done; + } + } + } +done: + globfree(&gl); + if (cp != NULL) { + if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) { + /* ctx->runas.cmnd was set above. */ + set_cmnd_fd(ctx, fd, real_root); + debug_return_int(ALLOW); + } + } + if (fd != -1) + close(fd); + debug_return_int(DENY); +} + +static int +command_matches_normal(struct sudoers_context *ctx, const char *sudoers_cmnd, + const char *sudoers_args, int real_root, + const struct command_digest_list *digests) +{ + struct stat sudoers_stat; + const char *base; + size_t dlen; + int fd = -1; + debug_decl(command_matches_normal, SUDOERS_DEBUG_MATCH); + + /* If it ends in '/' it is a directory spec. */ + dlen = strlen(sudoers_cmnd); + if (sudoers_cmnd[dlen - 1] == '/') { + debug_return_int(command_matches_dir(ctx, sudoers_cmnd, dlen, + real_root, digests)); + } + + /* Only proceed if ctx->user.cmnd_base and basename(sudoers_cmnd) match */ + base = sudo_basename(sudoers_cmnd); + if (strcmp(ctx->user.cmnd_base, base) != 0) + debug_return_int(DENY); + + /* Compare the canonicalized parent directories, if possible. */ + if (ctx->user.cmnd_dir != NULL) { + const char *slash = strrchr(sudoers_cmnd, '/'); + if (slash != NULL) { + char sudoers_cmnd_dir[PATH_MAX], *resolved; + const size_t len = (size_t)(slash - sudoers_cmnd); + if (len >= sizeof(sudoers_cmnd_dir)) + goto bad; + if (len != 0) + memcpy(sudoers_cmnd_dir, sudoers_cmnd, len); + sudoers_cmnd_dir[len] = '\0'; + resolved = canon_path(sudoers_cmnd_dir); + if (resolved != NULL) { + if (strcmp(resolved, ctx->user.cmnd_dir) != 0) { + canon_path_free(resolved); + goto bad; + } + canon_path_free(resolved); + } + } + } + + /* Open the file for fdexec or for digest matching. */ + if (!open_cmnd(sudoers_cmnd, digests, &fd)) + goto bad; + + /* + * Return true if command matches AND + * a) there are no args in sudoers OR + * b) there are no args on command line and none req by sudoers OR + * c) there are args in sudoers and on command line and they match + * d) there is a digest and it matches + */ + if (ctx->user.cmnd_stat != NULL && do_stat(fd, sudoers_cmnd, &sudoers_stat)) { + if (ctx->user.cmnd_stat->st_dev != sudoers_stat.st_dev || + ctx->user.cmnd_stat->st_ino != sudoers_stat.st_ino) + goto bad; + } else { + /* Either user or sudoers command does not exist, match by name. */ + if (strcmp(ctx->user.cmnd, sudoers_cmnd) != 0) + goto bad; + } + if (command_args_match(ctx, sudoers_cmnd, sudoers_args) != ALLOW) + goto bad; + if (digest_matches(fd, sudoers_cmnd, digests) != ALLOW) { + /* XXX - log functions not available but we should log very loudly */ + goto bad; + } + free(ctx->runas.cmnd); + if ((ctx->runas.cmnd = strdup(sudoers_cmnd)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto bad; + } + set_cmnd_fd(ctx, fd, real_root); + debug_return_int(ALLOW); +bad: + if (fd != -1) + close(fd); + debug_return_int(DENY); +} +#else /* SUDOERS_NAME_MATCH */ +static int +command_matches_glob(struct sudoers_context *ctx, const char *sudoers_cmnd, + const char *sudoers_args, int real_root, + const struct command_digest_list *digests) +{ + return command_matches_fnmatch(ctx, sudoers_cmnd, sudoers_args, real_root, + digests); +} + +static int +command_matches_normal(struct sudoers_context *ctx, const char *sudoers_cmnd, + const char *sudoers_args, int real_root, + const struct command_digest_list *digests) +{ + size_t dlen; + int fd = -1; + debug_decl(command_matches_normal, SUDOERS_DEBUG_MATCH); + + /* If it ends in '/' it is a directory spec. */ + dlen = strlen(sudoers_cmnd); + if (sudoers_cmnd[dlen - 1] == '/') { + debug_return_int(command_matches_dir(ctx, sudoers_cmnd, dlen, real_root, + digests)); + } + + if (strcmp(ctx->user.cmnd, sudoers_cmnd) == 0) { + if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) { + /* Open the file for fdexec or for digest matching. */ + if (!open_cmnd(ctx->user.cmnd, digests, &fd)) + goto bad; + if (digest_matches(fd, ctx->user.cmnd, digests) != ALLOW) + goto bad; + + /* Successful match. */ + free(ctx->runas.cmnd); + if ((ctx->runas.cmnd = strdup(sudoers_cmnd)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + goto bad; + } + set_cmnd_fd(ctx, fd, real_root); + debug_return_int(ALLOW); + } + } +bad: + if (fd != -1) + close(fd); + debug_return_int(DENY); +} +#endif /* SUDOERS_NAME_MATCH */ + +/* + * If path doesn't end in /, return ALLOW iff cmnd & path name the same inode; + * otherwise, return ALLOW if ctx->user.cmnd names one of the inodes in path. + * Returns DENY on failure. + */ +int +command_matches(struct sudoers_context *ctx, const char *sudoers_cmnd, + const char *sudoers_args, const char *runchroot, struct cmnd_info *info, + const struct command_digest_list *digests) +{ + struct sudoers_pivot pivot_state = SUDOERS_PIVOT_INITIALIZER; + char *saved_user_cmnd = NULL; + struct stat saved_user_stat; + bool reset_cmnd = false; + int real_root = -1; + int ret = DENY; + debug_decl(command_matches, SUDOERS_DEBUG_MATCH); + + if (ctx->runas.chroot != NULL) { + if (runchroot != NULL && strcmp(runchroot, "*") != 0 && + strcmp(runchroot, ctx->runas.chroot) != 0) { + /* CHROOT mismatch */ + goto done; + } + /* User-specified runchroot (cmnd_stat already set appropriately). */ + runchroot = ctx->runas.chroot; + } else if (runchroot == NULL) { + /* No rule-specific runchroot, use global (cmnd_stat already set). */ + if (def_runchroot != NULL && strcmp(def_runchroot, "*") != '\0') + runchroot = def_runchroot; + } else { + /* Rule-specific runchroot, must reset cmnd and cmnd_stat. */ + reset_cmnd = true; + } + + /* Pivot root. */ + if (runchroot != NULL) { + if (!pivot_root(runchroot, &pivot_state)) + goto done; + real_root = pivot_state.saved_root; + } + + if (reset_cmnd) { + /* Rule-specific runchroot, set cmnd and cmnd_stat after pivot. */ + int status; + + /* Save old ctx->user.cmnd first, set_cmnd_path() will free it. */ + saved_user_cmnd = ctx->user.cmnd; + ctx->user.cmnd = NULL; + if (ctx->user.cmnd_stat != NULL) + saved_user_stat = *ctx->user.cmnd_stat; + status = set_cmnd_path(ctx, NULL); + if (status != FOUND) { + ctx->user.cmnd = saved_user_cmnd; + saved_user_cmnd = NULL; + } + if (info != NULL) + info->status = status; + } + + if (sudoers_cmnd == NULL) { + sudoers_cmnd = "ALL"; + ret = command_matches_all(ctx, real_root, digests); + goto done; + } + + /* Check for regular expressions first. */ + if (sudoers_cmnd[0] == '^') { + ret = command_matches_regex(ctx, sudoers_cmnd, sudoers_args, real_root, + digests); + goto done; + } + + /* Check for pseudo-commands */ + if (sudoers_cmnd[0] != '/') { + /* + * Return true if sudoers_cmnd and cmnd match a pseudo-command AND + * a) there are no args in sudoers OR + * b) there are no args on command line and none req by sudoers OR + * c) there are args in sudoers and on command line and they match + */ + if (strcmp(sudoers_cmnd, "list") == 0 || + strcmp(sudoers_cmnd, "sudoedit") == 0) { + if (strcmp(ctx->user.cmnd, sudoers_cmnd) == 0 && + command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) { + /* No need to set ctx->user.cmnd since cmnd == sudoers_cmnd */ + ret = ALLOW; + } + } + goto done; + } + + if (has_meta(sudoers_cmnd)) { + /* + * If sudoers_cmnd has meta characters in it, we need to + * use glob(3) and/or fnmatch(3) to do the matching. + */ + if (def_fast_glob) { + ret = command_matches_fnmatch(ctx, sudoers_cmnd, sudoers_args, + real_root, digests); + } else { + ret = command_matches_glob(ctx, sudoers_cmnd, sudoers_args, + real_root, digests); + } + } else { + ret = command_matches_normal(ctx, sudoers_cmnd, sudoers_args, + real_root, digests); + } +done: + /* Restore root. */ + if (runchroot != NULL) + (void)unpivot_root(&pivot_state); + + /* Restore ctx->user.cmnd and ctx->user.cmnd_stat. */ + if (saved_user_cmnd != NULL) { + if (info != NULL) { + free(info->cmnd_path); + info->cmnd_path = ctx->user.cmnd; + if (ctx->user.cmnd_stat != NULL) + info->cmnd_stat = *ctx->user.cmnd_stat; + } else { + free(ctx->user.cmnd); + } + ctx->user.cmnd = saved_user_cmnd; + if (ctx->user.cmnd_stat != NULL) + *ctx->user.cmnd_stat = saved_user_stat; + } + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "user command \"%s%s%s\" matches sudoers command \"%s%s%s\"%s%s: %s", + ctx->user.cmnd, ctx->user.cmnd_args ? " " : "", + ctx->user.cmnd_args ? ctx->user.cmnd_args : "", sudoers_cmnd, + sudoers_args ? " " : "", sudoers_args ? sudoers_args : "", + runchroot ? ", chroot " : "", runchroot ? runchroot : "", + ret == ALLOW ? "ALLOW" : "DENY"); + debug_return_int(ret); +} |