From: Todd C. Miller Date: Mon, 18 Feb 2019 18:35:52 +0000 (-0700) Subject: Split command match code out into match_command.c. X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=36d43734035915ab1e85406579ad7144b3e4bdfb;p=sudo Split command match code out into match_command.c. Also remove unused SUDOERS_NAME_MATCH code. --- diff --git a/MANIFEST b/MANIFEST index 08d7f25d3..748ffad53 100644 --- a/MANIFEST +++ b/MANIFEST @@ -332,6 +332,8 @@ plugins/sudoers/logging.h plugins/sudoers/logwrap.c plugins/sudoers/match.c plugins/sudoers/match_addr.c +plugins/sudoers/match_command.c +plugins/sudoers/match_digest.c plugins/sudoers/mkdefaults plugins/sudoers/mkdir_parents.c plugins/sudoers/parse.c diff --git a/plugins/sudoers/Makefile.in b/plugins/sudoers/Makefile.in index fe79ce9e9..7d32dccbb 100644 --- a/plugins/sudoers/Makefile.in +++ b/plugins/sudoers/Makefile.in @@ -153,9 +153,10 @@ AUTH_OBJS = sudo_auth.lo @AUTH_OBJS@ LIBPARSESUDOERS_OBJS = alias.lo audit.lo base64.lo defaults.lo digestname.lo \ filedigest.lo gentime.lo gmtoff.lo gram.lo hexchar.lo \ - match.lo match_addr.lo match_digest.lo pwutil.lo \ - pwutil_impl.lo rcstr.lo redblack.lo sudoers_debug.lo \ - timeout.lo timestr.lo toke.lo toke_util.lo + match.lo match_addr.lo match_command.lo match_digest.lo \ + pwutil.lo pwutil_impl.lo rcstr.lo redblack.lo \ + sudoers_debug.lo timeout.lo timestr.lo toke.lo \ + toke_util.lo LIBPARSESUDOERS_IOBJS = $(LIBPARSESUDOERS_OBJS:.lo=.i) passwd.i @@ -1758,23 +1759,21 @@ logwrap.i: $(srcdir)/logwrap.c $(devdir)/def_data.h \ logwrap.plog: logwrap.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/logwrap.c --i-file $< --output-file $@ match.lo: $(srcdir)/match.c $(devdir)/def_data.h $(devdir)/gram.h \ - $(incdir)/compat/fnmatch.h $(incdir)/compat/glob.h \ - $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ - $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ - $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \ - $(incdir)/sudo_queue.h $(incdir)/sudo_util.h $(srcdir)/defaults.h \ - $(srcdir)/logging.h $(srcdir)/parse.h $(srcdir)/sudo_nss.h \ - $(srcdir)/sudoers.h $(srcdir)/sudoers_debug.h \ + $(incdir)/compat/fnmatch.h $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(srcdir)/defaults.h $(srcdir)/logging.h $(srcdir)/parse.h \ + $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h $(srcdir)/sudoers_debug.h \ $(top_builddir)/config.h $(top_builddir)/pathnames.h $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/match.c match.i: $(srcdir)/match.c $(devdir)/def_data.h $(devdir)/gram.h \ - $(incdir)/compat/fnmatch.h $(incdir)/compat/glob.h \ - $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ - $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ - $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \ - $(incdir)/sudo_queue.h $(incdir)/sudo_util.h $(srcdir)/defaults.h \ - $(srcdir)/logging.h $(srcdir)/parse.h $(srcdir)/sudo_nss.h \ - $(srcdir)/sudoers.h $(srcdir)/sudoers_debug.h \ + $(incdir)/compat/fnmatch.h $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(srcdir)/defaults.h $(srcdir)/logging.h $(srcdir)/parse.h \ + $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h $(srcdir)/sudoers_debug.h \ $(top_builddir)/config.h $(top_builddir)/pathnames.h $(CC) -E -o $@ $(CPPFLAGS) $< match.plog: match.i @@ -1803,6 +1802,32 @@ match_addr.i: $(srcdir)/match_addr.c $(devdir)/def_data.h \ $(CC) -E -o $@ $(CPPFLAGS) $< match_addr.plog: match_addr.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/match_addr.c --i-file $< --output-file $@ +match_command.lo: $(srcdir)/match_command.c $(devdir)/def_data.h \ + $(devdir)/gram.h $(incdir)/compat/fnmatch.h \ + $(incdir)/compat/glob.h $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ + $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(srcdir)/defaults.h $(srcdir)/logging.h $(srcdir)/parse.h \ + $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \ + $(srcdir)/sudoers_debug.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/match_command.c +match_command.i: $(srcdir)/match_command.c $(devdir)/def_data.h \ + $(devdir)/gram.h $(incdir)/compat/fnmatch.h \ + $(incdir)/compat/glob.h $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ + $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(srcdir)/defaults.h $(srcdir)/logging.h $(srcdir)/parse.h \ + $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \ + $(srcdir)/sudoers_debug.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +match_command.plog: match_command.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/match_command.c --i-file $< --output-file $@ match_digest.lo: $(srcdir)/match_digest.c $(devdir)/def_data.h \ $(devdir)/gram.h $(incdir)/compat/stdbool.h \ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ diff --git a/plugins/sudoers/match.c b/plugins/sudoers/match.c index 3ced7b662..5023c05f3 100644 --- a/plugins/sudoers/match.c +++ b/plugins/sudoers/match.c @@ -39,19 +39,7 @@ #ifdef HAVE_STRINGS_H # include #endif /* HAVE_STRINGS_H */ -#if defined(HAVE_STDINT_H) -# include -#elif defined(HAVE_INTTYPES_H) -# include -#endif #include -#ifndef SUDOERS_NAME_MATCH -# ifdef HAVE_GLOB -# include -# else -# include "compat/glob.h" -# endif /* HAVE_GLOB */ -#endif /* SUDOERS_NAME_MATCH */ #ifdef HAVE_NETGROUP_H # include #else @@ -72,24 +60,8 @@ # include "compat/fnmatch.h" #endif /* HAVE_FNMATCH */ -#if !defined(O_EXEC) && defined(O_PATH) -# define O_EXEC O_PATH -#endif - static struct member_list empty = TAILQ_HEAD_INITIALIZER(empty); -static bool command_matches_dir(const char *sudoers_dir, size_t dlen, const struct command_digest *digest); -#ifndef SUDOERS_NAME_MATCH -static bool command_matches_glob(const char *sudoers_cmnd, const char *sudoers_args, const struct command_digest *digest); -#endif -static bool command_matches_fnmatch(const char *sudoers_cmnd, const char *sudoers_args, const struct command_digest *digest); -static bool command_matches_normal(const char *sudoers_cmnd, const char *sudoers_args, const struct command_digest *digest); - -/* - * Returns true if string 's' contains meta characters. - */ -#define has_meta(s) (strpbrk(s, "\\?*[]") != NULL) - /* * Check whether user described by pw matches member. * Returns ALLOW, DENY or UNSPEC. @@ -437,532 +409,6 @@ cmnd_matches(struct sudoers_parse_tree *parse_tree, const struct member *m) debug_return_int(matched); } -static bool -command_args_match(const char *sudoers_cmnd, const char *sudoers_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 || (!user_args && !strcmp("\"\"", sudoers_args))) - debug_return_bool(true); - - /* - * If args are specified in sudoers, they must match the user 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, user_args ? user_args : "", flags) == 0) - debug_return_bool(true); - - debug_return_bool(false); -} - -/* - * If path doesn't end in /, return true iff cmnd & path name the same inode; - * otherwise, return true if user_cmnd names one of the inodes in path. - */ -bool -command_matches(const char *sudoers_cmnd, const char *sudoers_args, const struct command_digest *digest) -{ - bool rc = false; - debug_decl(command_matches, SUDOERS_DEBUG_MATCH) - - /* Check for pseudo-commands */ - if (sudoers_cmnd[0] != '/') { - /* - * Return true if both sudoers_cmnd and user_cmnd are "sudoedit" 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, "sudoedit") == 0 && - strcmp(user_cmnd, "sudoedit") == 0 && - command_args_match(sudoers_cmnd, sudoers_args)) { - /* No need to set safe_cmnd since user_cmnd matches sudoers_cmnd */ - rc = true; - } - 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. - */ -#ifdef SUDOERS_NAME_MATCH - rc = command_matches_fnmatch(sudoers_cmnd, sudoers_args, digest); -#else - if (def_fast_glob) - rc = command_matches_fnmatch(sudoers_cmnd, sudoers_args, digest); - else - rc = command_matches_glob(sudoers_cmnd, sudoers_args, digest); -#endif - } else { - rc = command_matches_normal(sudoers_cmnd, sudoers_args, digest); - } -done: - sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, - "user command \"%s%s%s\" matches sudoers command \"%s%s%s\": %s", - user_cmnd, user_args ? " " : "", user_args ? user_args : "", - sudoers_cmnd, sudoers_args ? " " : "", sudoers_args ? sudoers_args : "", - rc ? "true" : "false"); - debug_return_bool(rc); -} - -/* - * 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) -{ - debug_decl(do_stat, SUDOERS_DEBUG_MATCH) - - if (fd != -1) - debug_return_bool(fstat(fd, sb) == 0); - debug_return_bool(stat(path, sb) == 0); -} - -/* - * 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 (read(fd, magic, 2) == 2) { - if (magic[0] == '#' && magic[1] == '!') - ret = true; - } - if (lseek(fd, (off_t)0, SEEK_SET) == -1) { - sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO, - "unable to rewind script fd"); - } - debug_return_int(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 *digest, int *fdp) -{ - int fd = -1; - debug_decl(open_cmnd, SUDOERS_DEBUG_MATCH) - - /* Only open the file for fdexec or for digest matching. */ - if (def_fdexec != always && digest == NULL) - debug_return_bool(true); - - fd = open(path, O_RDONLY|O_NONBLOCK); -# ifdef O_EXEC - if (fd == -1 && errno == EACCES && digest == NULL) { - /* 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(int fd) -{ - debug_decl(set_cmnd_fd, SUDOERS_DEBUG_MATCH) - - if (cmnd_fd != -1) - close(cmnd_fd); - - 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 flags; - - /* We can only use fexecve() on a script if /dev/fd/N exists. */ - (void)snprintf(fdpath, sizeof(fdpath), "/dev/fd/%d", fd); - if (stat(fdpath, &sb) != 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); - } - } - } - - cmnd_fd = fd; - - debug_return; -} - -static bool -command_matches_fnmatch(const char *sudoers_cmnd, const char *sudoers_args, - const struct command_digest *digest) -{ - struct stat sb; /* XXX - unused */ - int fd = -1; - debug_decl(command_matches_fnmatch, SUDOERS_DEBUG_MATCH) - - /* - * Return true 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 false. - */ - if (fnmatch(sudoers_cmnd, user_cmnd, FNM_PATHNAME) != 0) - debug_return_bool(false); - if (command_args_match(sudoers_cmnd, sudoers_args)) { - /* Open the file for fdexec or for digest matching. */ - if (!open_cmnd(user_cmnd, digest, &fd)) - goto bad; - if (!do_stat(fd, user_cmnd, &sb)) - goto bad; - /* Check digest of user_cmnd since sudoers_cmnd is a pattern. */ - if (digest != NULL && !digest_matches(fd, user_cmnd, digest)) - goto bad; - set_cmnd_fd(fd); - - /* No need to set safe_cmnd since user_cmnd matches sudoers_cmnd */ - debug_return_bool(true); -bad: - if (fd != -1) { - close(fd); - fd = -1; - } - debug_return_bool(false); - } - debug_return_bool(false); -} - -#ifndef SUDOERS_NAME_MATCH -static bool -command_matches_glob(const char *sudoers_cmnd, const char *sudoers_args, - const struct command_digest *digest) -{ - 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 user_base and basename(sudoers_cmnd) don't match. - */ - dlen = strlen(sudoers_cmnd); - if (sudoers_cmnd[dlen - 1] != '/') { - if ((base = strrchr(sudoers_cmnd, '/')) != NULL) { - base++; - if (!has_meta(base) && strcmp(user_base, base) != 0) - debug_return_bool(false); - } - } - /* - * Return true 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 false. - */ - if (glob(sudoers_cmnd, GLOB_NOSORT, NULL, &gl) != 0 || gl.gl_pathc == 0) { - globfree(&gl); - debug_return_bool(false); - } - /* If user_cmnd is fully-qualified, check for an exact match. */ - if (user_cmnd[0] == '/') { - for (ap = gl.gl_pathv; (cp = *ap) != NULL; ap++) { - if (fd != -1) { - close(fd); - fd = -1; - } - if (strcmp(cp, user_cmnd) != 0) - continue; - /* Open the file for fdexec or for digest matching. */ - if (!open_cmnd(cp, digest, &fd)) - continue; - if (!do_stat(fd, cp, &sudoers_stat)) - continue; - if (user_stat == NULL || - (user_stat->st_dev == sudoers_stat.st_dev && - user_stat->st_ino == sudoers_stat.st_ino)) { - /* There could be multiple matches, check digest early. */ - if (digest != NULL && !digest_matches(fd, cp, digest)) { - bad_digest = true; - continue; - } - free(safe_cmnd); - if ((safe_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, 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(cp, dlen, digest)) - debug_return_bool(true); - continue; - } - - /* Only proceed if user_base and basename(cp) match */ - if ((base = strrchr(cp, '/')) != NULL) - base++; - else - base = cp; - if (strcmp(user_base, base) != 0) - continue; - - /* Open the file for fdexec or for digest matching. */ - if (!open_cmnd(cp, digest, &fd)) - continue; - if (!do_stat(fd, cp, &sudoers_stat)) - continue; - if (user_stat == NULL || - (user_stat->st_dev == sudoers_stat.st_dev && - user_stat->st_ino == sudoers_stat.st_ino)) { - if (digest != NULL && !digest_matches(fd, cp, digest)) - continue; - free(safe_cmnd); - if ((safe_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(sudoers_cmnd, sudoers_args)) { - /* safe_cmnd was set above. */ - set_cmnd_fd(fd); - debug_return_bool(true); - } - } - if (fd != -1) - close(fd); - debug_return_bool(false); -} -#endif /* SUDOERS_NAME_MATCH */ - -#ifdef SUDOERS_NAME_MATCH -static bool -command_matches_normal(const char *sudoers_cmnd, const char *sudoers_args, const struct command_digest *digest) -{ - size_t dlen; - debug_decl(command_matches_normal, SUDOERS_DEBUG_MATCH) - - dlen = strlen(sudoers_cmnd); - - /* If it ends in '/' it is a directory spec. */ - if (sudoers_cmnd[dlen - 1] == '/') - debug_return_bool(command_matches_dir(sudoers_cmnd, dlen, digest)); - - if (strcmp(user_cmnd, sudoers_cmnd) == 0) { - if (command_args_match(sudoers_cmnd, sudoers_args)) { - /* XXX - check digest */ - free(safe_cmnd); - if ((safe_cmnd = strdup(sudoers_cmnd)) != NULL) - debug_return_bool(true); - sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); - } - } - debug_return_bool(false); -} - -#else /* !SUDOERS_NAME_MATCH */ - -static bool -command_matches_normal(const char *sudoers_cmnd, const char *sudoers_args, const struct command_digest *digest) -{ - 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_bool(command_matches_dir(sudoers_cmnd, dlen, digest)); - - /* Only proceed if user_base and basename(sudoers_cmnd) match */ - if ((base = strrchr(sudoers_cmnd, '/')) == NULL) - base = sudoers_cmnd; - else - base++; - if (strcmp(user_base, base) != 0) - debug_return_bool(false); - - /* Open the file for fdexec or for digest matching. */ - if (!open_cmnd(sudoers_cmnd, digest, &fd)) - goto bad; - if (!do_stat(fd, sudoers_cmnd, &sudoers_stat)) - goto bad; - - /* - * Return true if inode/device 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 (user_stat != NULL && - (user_stat->st_dev != sudoers_stat.st_dev || - user_stat->st_ino != sudoers_stat.st_ino)) - goto bad; - if (!command_args_match(sudoers_cmnd, sudoers_args)) - goto bad; - if (digest != NULL && !digest_matches(fd, sudoers_cmnd, digest)) { - /* XXX - log functions not available but we should log very loudly */ - goto bad; - } - free(safe_cmnd); - if ((safe_cmnd = strdup(sudoers_cmnd)) == NULL) { - sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); - goto bad; - } - set_cmnd_fd(fd); - debug_return_bool(true); -bad: - if (fd != -1) - close(fd); - debug_return_bool(false); -} -#endif /* SUDOERS_NAME_MATCH */ - -#ifdef SUDOERS_NAME_MATCH -/* - * Return true if user_cmnd begins with sudoers_dir, else false. - * Note that sudoers_dir include the trailing '/' - */ -static bool -command_matches_dir(const char *sudoers_dir, size_t dlen, - const struct command_digest *digest) -{ - debug_decl(command_matches_dir, SUDOERS_DEBUG_MATCH) - /* XXX - check digest */ - debug_return_bool(strncmp(user_cmnd, sudoers_dir, dlen) == 0); -} -#else /* !SUDOERS_NAME_MATCH */ -/* - * Return true if user_cmnd names one of the inodes in dir, else false. - */ -static bool -command_matches_dir(const char *sudoers_dir, size_t dlen, - const struct command_digest *digest) -{ - struct stat sudoers_stat; - struct dirent *dent; - char buf[PATH_MAX]; - int fd = -1; - DIR *dirp; - debug_decl(command_matches_dir, SUDOERS_DEBUG_MATCH) - - /* - * Grot through directory entries, looking for user_base. - */ - dirp = opendir(sudoers_dir); - if (dirp == NULL) - debug_return_bool(false); - - if (strlcpy(buf, sudoers_dir, sizeof(buf)) >= sizeof(buf)) { - closedir(dirp); - debug_return_bool(false); - } - while ((dent = readdir(dirp)) != NULL) { - if (fd != -1) { - close(fd); - fd = -1; - } - - /* ignore paths > PATH_MAX (XXX - log) */ - buf[dlen] = '\0'; - if (strlcat(buf, dent->d_name, sizeof(buf)) >= sizeof(buf)) - continue; - - /* only stat if basenames are the same */ - if (strcmp(user_base, dent->d_name) != 0) - continue; - - /* Open the file for fdexec or for digest matching. */ - if (!open_cmnd(buf, digest, &fd)) - continue; - if (!do_stat(fd, buf, &sudoers_stat)) - continue; - - if (user_stat == NULL || - (user_stat->st_dev == sudoers_stat.st_dev && - user_stat->st_ino == sudoers_stat.st_ino)) { - if (digest != NULL && !digest_matches(fd, buf, digest)) - continue; - free(safe_cmnd); - if ((safe_cmnd = strdup(buf)) == NULL) { - sudo_warnx(U_("%s: %s"), __func__, - U_("unable to allocate memory")); - dent = NULL; - } - break; - } - } - closedir(dirp); - - if (dent != NULL) { - set_cmnd_fd(fd); - debug_return_bool(true); - } - if (fd != -1) - close(fd); - debug_return_bool(false); -} -#endif /* SUDOERS_NAME_MATCH */ - /* * Returns true if the hostname matches the pattern, else false */ diff --git a/plugins/sudoers/match_command.c b/plugins/sudoers/match_command.c new file mode 100644 index 000000000..71993dfba --- /dev/null +++ b/plugins/sudoers/match_command.c @@ -0,0 +1,537 @@ +/* + * Copyright (c) 1996, 1998-2005, 2007-2019 + * Todd C. Miller + * + * 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 + +#include +#include +#include +#include +#ifdef HAVE_STRING_H +# include +#endif /* HAVE_STRING_H */ +#ifdef HAVE_STRINGS_H +# include +#endif /* HAVE_STRINGS_H */ +#include +#ifdef HAVE_GLOB +# include +#else +# include "compat/glob.h" +#endif /* HAVE_GLOB */ +#include +#include +#include + +#include "sudoers.h" +#include + +#ifdef HAVE_FNMATCH +# include +#else +# include "compat/fnmatch.h" +#endif /* HAVE_FNMATCH */ + +#if !defined(O_EXEC) && defined(O_PATH) +# define O_EXEC O_PATH +#endif + +static bool +command_args_match(const char *sudoers_cmnd, const char *sudoers_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 || (!user_args && !strcmp("\"\"", sudoers_args))) + debug_return_bool(true); + + /* + * If args are specified in sudoers, they must match the user 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, user_args ? user_args : "", flags) == 0) + debug_return_bool(true); + + debug_return_bool(false); +} + +/* + * 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) +{ + debug_decl(do_stat, SUDOERS_DEBUG_MATCH) + + if (fd != -1) + debug_return_bool(fstat(fd, sb) == 0); + debug_return_bool(stat(path, sb) == 0); +} + +/* + * 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 (read(fd, magic, 2) == 2) { + if (magic[0] == '#' && magic[1] == '!') + ret = true; + } + if (lseek(fd, (off_t)0, SEEK_SET) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO, + "unable to rewind script fd"); + } + debug_return_int(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 *digest, int *fdp) +{ + int fd = -1; + debug_decl(open_cmnd, SUDOERS_DEBUG_MATCH) + + /* Only open the file for fdexec or for digest matching. */ + if (def_fdexec != always && digest == NULL) + debug_return_bool(true); + + fd = open(path, O_RDONLY|O_NONBLOCK); +# ifdef O_EXEC + if (fd == -1 && errno == EACCES && digest == NULL) { + /* 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(int fd) +{ + debug_decl(set_cmnd_fd, SUDOERS_DEBUG_MATCH) + + if (cmnd_fd != -1) + close(cmnd_fd); + + 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 flags; + + /* We can only use fexecve() on a script if /dev/fd/N exists. */ + (void)snprintf(fdpath, sizeof(fdpath), "/dev/fd/%d", fd); + if (stat(fdpath, &sb) != 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); + } + } + } + + cmnd_fd = fd; + + debug_return; +} + +/* + * Return true if user_cmnd names one of the inodes in dir, else false. + */ +static bool +command_matches_dir(const char *sudoers_dir, size_t dlen, + const struct command_digest *digest) +{ + struct stat sudoers_stat; + struct dirent *dent; + char buf[PATH_MAX]; + int fd = -1; + DIR *dirp; + debug_decl(command_matches_dir, SUDOERS_DEBUG_MATCH) + + /* + * Grot through directory entries, looking for user_base. + */ + dirp = opendir(sudoers_dir); + if (dirp == NULL) + debug_return_bool(false); + + if (strlcpy(buf, sudoers_dir, sizeof(buf)) >= sizeof(buf)) { + closedir(dirp); + debug_return_bool(false); + } + while ((dent = readdir(dirp)) != NULL) { + if (fd != -1) { + close(fd); + fd = -1; + } + + /* ignore paths > PATH_MAX (XXX - log) */ + buf[dlen] = '\0'; + if (strlcat(buf, dent->d_name, sizeof(buf)) >= sizeof(buf)) + continue; + + /* only stat if basenames are the same */ + if (strcmp(user_base, dent->d_name) != 0) + continue; + + /* Open the file for fdexec or for digest matching. */ + if (!open_cmnd(buf, digest, &fd)) + continue; + if (!do_stat(fd, buf, &sudoers_stat)) + continue; + + if (user_stat == NULL || + (user_stat->st_dev == sudoers_stat.st_dev && + user_stat->st_ino == sudoers_stat.st_ino)) { + if (digest != NULL && !digest_matches(fd, buf, digest)) + continue; + free(safe_cmnd); + if ((safe_cmnd = strdup(buf)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + dent = NULL; + } + break; + } + } + closedir(dirp); + + if (dent != NULL) { + set_cmnd_fd(fd); + debug_return_bool(true); + } + if (fd != -1) + close(fd); + debug_return_bool(false); +} + +static bool +command_matches_fnmatch(const char *sudoers_cmnd, const char *sudoers_args, + const struct command_digest *digest) +{ + struct stat sb; /* XXX - unused */ + int fd = -1; + debug_decl(command_matches_fnmatch, SUDOERS_DEBUG_MATCH) + + /* + * Return true 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 false. + */ + if (fnmatch(sudoers_cmnd, user_cmnd, FNM_PATHNAME) != 0) + debug_return_bool(false); + if (command_args_match(sudoers_cmnd, sudoers_args)) { + /* Open the file for fdexec or for digest matching. */ + if (!open_cmnd(user_cmnd, digest, &fd)) + goto bad; + if (!do_stat(fd, user_cmnd, &sb)) + goto bad; + /* Check digest of user_cmnd since sudoers_cmnd is a pattern. */ + if (digest != NULL && !digest_matches(fd, user_cmnd, digest)) + goto bad; + set_cmnd_fd(fd); + + /* No need to set safe_cmnd since user_cmnd matches sudoers_cmnd */ + debug_return_bool(true); +bad: + if (fd != -1) { + close(fd); + fd = -1; + } + debug_return_bool(false); + } + debug_return_bool(false); +} + +static bool +command_matches_glob(const char *sudoers_cmnd, const char *sudoers_args, + const struct command_digest *digest) +{ + 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 user_base and basename(sudoers_cmnd) don't match. + */ + dlen = strlen(sudoers_cmnd); + if (sudoers_cmnd[dlen - 1] != '/') { + if ((base = strrchr(sudoers_cmnd, '/')) != NULL) { + base++; + if (!has_meta(base) && strcmp(user_base, base) != 0) + debug_return_bool(false); + } + } + /* + * Return true 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 false. + */ + if (glob(sudoers_cmnd, GLOB_NOSORT, NULL, &gl) != 0 || gl.gl_pathc == 0) { + globfree(&gl); + debug_return_bool(false); + } + /* If user_cmnd is fully-qualified, check for an exact match. */ + if (user_cmnd[0] == '/') { + for (ap = gl.gl_pathv; (cp = *ap) != NULL; ap++) { + if (fd != -1) { + close(fd); + fd = -1; + } + if (strcmp(cp, user_cmnd) != 0) + continue; + /* Open the file for fdexec or for digest matching. */ + if (!open_cmnd(cp, digest, &fd)) + continue; + if (!do_stat(fd, cp, &sudoers_stat)) + continue; + if (user_stat == NULL || + (user_stat->st_dev == sudoers_stat.st_dev && + user_stat->st_ino == sudoers_stat.st_ino)) { + /* There could be multiple matches, check digest early. */ + if (digest != NULL && !digest_matches(fd, cp, digest)) { + bad_digest = true; + continue; + } + free(safe_cmnd); + if ((safe_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, 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(cp, dlen, digest)) + debug_return_bool(true); + continue; + } + + /* Only proceed if user_base and basename(cp) match */ + if ((base = strrchr(cp, '/')) != NULL) + base++; + else + base = cp; + if (strcmp(user_base, base) != 0) + continue; + + /* Open the file for fdexec or for digest matching. */ + if (!open_cmnd(cp, digest, &fd)) + continue; + if (!do_stat(fd, cp, &sudoers_stat)) + continue; + if (user_stat == NULL || + (user_stat->st_dev == sudoers_stat.st_dev && + user_stat->st_ino == sudoers_stat.st_ino)) { + if (digest != NULL && !digest_matches(fd, cp, digest)) + continue; + free(safe_cmnd); + if ((safe_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(sudoers_cmnd, sudoers_args)) { + /* safe_cmnd was set above. */ + set_cmnd_fd(fd); + debug_return_bool(true); + } + } + if (fd != -1) + close(fd); + debug_return_bool(false); +} + +static bool +command_matches_normal(const char *sudoers_cmnd, const char *sudoers_args, const struct command_digest *digest) +{ + 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_bool(command_matches_dir(sudoers_cmnd, dlen, digest)); + + /* Only proceed if user_base and basename(sudoers_cmnd) match */ + if ((base = strrchr(sudoers_cmnd, '/')) == NULL) + base = sudoers_cmnd; + else + base++; + if (strcmp(user_base, base) != 0) + debug_return_bool(false); + + /* Open the file for fdexec or for digest matching. */ + if (!open_cmnd(sudoers_cmnd, digest, &fd)) + goto bad; + if (!do_stat(fd, sudoers_cmnd, &sudoers_stat)) + goto bad; + + /* + * Return true if inode/device 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 (user_stat != NULL && + (user_stat->st_dev != sudoers_stat.st_dev || + user_stat->st_ino != sudoers_stat.st_ino)) + goto bad; + if (!command_args_match(sudoers_cmnd, sudoers_args)) + goto bad; + if (digest != NULL && !digest_matches(fd, sudoers_cmnd, digest)) { + /* XXX - log functions not available but we should log very loudly */ + goto bad; + } + free(safe_cmnd); + if ((safe_cmnd = strdup(sudoers_cmnd)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto bad; + } + set_cmnd_fd(fd); + debug_return_bool(true); +bad: + if (fd != -1) + close(fd); + debug_return_bool(false); +} + +/* + * If path doesn't end in /, return true iff cmnd & path name the same inode; + * otherwise, return true if user_cmnd names one of the inodes in path. + */ +bool +command_matches(const char *sudoers_cmnd, const char *sudoers_args, const struct command_digest *digest) +{ + bool rc = false; + debug_decl(command_matches, SUDOERS_DEBUG_MATCH) + + /* Check for pseudo-commands */ + if (sudoers_cmnd[0] != '/') { + /* + * Return true if both sudoers_cmnd and user_cmnd are "sudoedit" 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, "sudoedit") == 0 && + strcmp(user_cmnd, "sudoedit") == 0 && + command_args_match(sudoers_cmnd, sudoers_args)) { + /* No need to set safe_cmnd since user_cmnd matches sudoers_cmnd */ + rc = true; + } + 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) + rc = command_matches_fnmatch(sudoers_cmnd, sudoers_args, digest); + else + rc = command_matches_glob(sudoers_cmnd, sudoers_args, digest); + } else { + rc = command_matches_normal(sudoers_cmnd, sudoers_args, digest); + } +done: + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "user command \"%s%s%s\" matches sudoers command \"%s%s%s\": %s", + user_cmnd, user_args ? " " : "", user_args ? user_args : "", + sudoers_cmnd, sudoers_args ? " " : "", sudoers_args ? sudoers_args : "", + rc ? "true" : "false"); + debug_return_bool(rc); +} diff --git a/plugins/sudoers/match_digest.c b/plugins/sudoers/match_digest.c index 001e0bfc7..8c0bf6176 100644 --- a/plugins/sudoers/match_digest.c +++ b/plugins/sudoers/match_digest.c @@ -36,27 +36,11 @@ #ifdef HAVE_STRINGS_H # include #endif /* HAVE_STRINGS_H */ -#if defined(HAVE_STDINT_H) -# include -#elif defined(HAVE_INTTYPES_H) -# include -#endif #include #include "sudoers.h" #include -#ifdef SUDOERS_NAME_MATCH -bool -digest_matches(int fd, const char *file, const struct command_digest *digest) -{ - debug_decl(digest_matches, SUDOERS_DEBUG_MATCH) - - /* Digests are not supported when matching only by name. */ - - debug_return_bool(false); -} -#else bool digest_matches(int fd, const char *file, const struct command_digest *digest) { @@ -118,4 +102,3 @@ done: free(file_digest); debug_return_bool(matched); } -#endif /* SUDOERS_NAME_MATCH */ diff --git a/plugins/sudoers/parse.h b/plugins/sudoers/parse.h index 0d6023d56..1c9d47679 100644 --- a/plugins/sudoers/parse.h +++ b/plugins/sudoers/parse.h @@ -18,8 +18,11 @@ #ifndef SUDOERS_PARSE_H #define SUDOERS_PARSE_H -/* Characters that must be quoted in sudoers */ -#define SUDOERS_QUOTED ":\\,=#\"" +/* Characters that must be quoted in sudoers. */ +#define SUDOERS_QUOTED ":\\,=#\"" + +/* Returns true if string 's' contains meta characters. */ +#define has_meta(s) (strpbrk(s, "\\?*[]") != NULL) #undef UNSPEC #define UNSPEC -1 @@ -297,13 +300,15 @@ void reparent_parse_tree(struct sudoers_parse_tree *new_tree); /* match_addr.c */ bool addr_matches(char *n); +/* match_command.c */ +bool command_matches(const char *sudoers_cmnd, const char *sudoers_args, const struct command_digest *digest); + /* match_digest.c */ bool digest_matches(int fd, const char *file, const struct command_digest *digest); /* match.c */ struct group; struct passwd; -bool command_matches(const char *sudoers_cmnd, const char *sudoers_args, const struct command_digest *digest); bool group_matches(const char *sudoers_group, const struct group *gr); bool hostname_matches(const char *shost, const char *lhost, const char *pattern); bool netgr_matches(const char *netgr, const char *lhost, const char *shost, const char *user);