From 4dc5700a6dd69a100a5e4618225d46c5363a240f Mon Sep 17 00:00:00 2001 From: "Todd C. Miller" Date: Wed, 21 Jan 2004 22:25:10 +0000 Subject: [PATCH] Add a new flag, -e, that makes it possible to give users the ability to edit files with the editor of their choice as the invoking user, not the runas user. Temporary files are used for the actual edit and the temp file is copied over the original after the editor is done. --- Makefile.in | 8 +- parse.c | 28 ++++- parse.lex | 6 ++ sudo.c | 71 ++++++++----- sudo_edit.c | 288 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 366 insertions(+), 35 deletions(-) create mode 100644 sudo_edit.c diff --git a/Makefile.in b/Makefile.in index 23402b235..13f26daaa 100644 --- a/Makefile.in +++ b/Makefile.in @@ -120,8 +120,8 @@ SRCS = alloc.c alloca.c check.c closefrom.c def_data.c defaults.c env.c err.c \ fileops.c find_path.c fnmatch.c getcwd.c getprogname.c getspwuid.c \ goodpath.c interfaces.c lex.yy.c lsearch.c logging.c parse.c parse.lex \ parse.yacc set_perms.c sigaction.c snprintf.c strcasecmp.c strerror.c \ - strlcat.c strlcpy.c sudo.c sudo.tab.c testsudoers.c tgetpass.c utime.c \ - visudo.c zero_bytes.c $(AUTH_SRCS) + strlcat.c strlcpy.c sudo.c sudo.tab.c sudo_edit.c testsudoers.c \ + tgetpass.c utime.c visudo.c zero_bytes.c $(AUTH_SRCS) AUTH_SRCS = auth/afs.c auth/aix_auth.c auth/bsdauth.c auth/dce.c auth/fwtk.c \ auth/kerb4.c auth/kerb5.c auth/pam.c auth/passwd.c auth/rfc1938.c \ @@ -138,8 +138,8 @@ AUTH_OBJS = sudo_auth.o @AUTH_OBJS@ PARSEOBJS = sudo.tab.o lex.yy.o alloc.o defaults.o SUDOBJS = check.o env.o getspwuid.o goodpath.o fileops.o find_path.o \ - interfaces.o logging.o parse.o set_perms.o sudo.o tgetpass.o \ - zero_bytes.o $(AUTH_OBJS) $(PARSEOBJS) + interfaces.o logging.o parse.o set_perms.o sudo.o sudo_edit.o \ + tgetpass.o zero_bytes.o $(AUTH_OBJS) $(PARSEOBJS) VISUDOBJS = visudo.o fileops.o goodpath.o find_path.o $(PARSEOBJS) diff --git a/parse.c b/parse.c index bcfdc668a..2f653f919 100644 --- a/parse.c +++ b/parse.c @@ -260,9 +260,27 @@ command_matches(cmnd, cmnd_args, path, sudoers_args) char buf[MAXPATHLEN]; static char *cmnd_base; - /* Don't bother with pseudo commands like "validate" */ - if (strchr(cmnd, '/') == NULL) - return(FALSE); + /* Check for pseudo-commands */ + if (*cmnd != '/') { + /* + * Return true if cmnd is "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(cmnd, "sudoedit") != 0) + return(FALSE); + if (!sudoers_args || + (!cmnd_args && sudoers_args && !strcmp("\"\"", sudoers_args)) || + (sudoers_args && + fnmatch(sudoers_args, cmnd_args ? cmnd_args : "", 0) == 0)) { + if (safe_cmnd) + free(safe_cmnd); + safe_cmnd = estrdup(path); + return(TRUE); + } else + return(FALSE); + } plen = strlen(path); @@ -299,8 +317,8 @@ command_matches(cmnd, cmnd_args, path, sudoers_args) return(FALSE); if (!sudoers_args || (!cmnd_args && sudoers_args && !strcmp("\"\"", sudoers_args)) || - (sudoers_args && fnmatch(sudoers_args, cmnd_args ? cmnd_args : "", - 0) == 0)) { + (sudoers_args && + fnmatch(sudoers_args, cmnd_args ? cmnd_args : "", 0) == 0)) { if (safe_cmnd) free(safe_cmnd); safe_cmnd = estrdup(user_cmnd); diff --git a/parse.lex b/parse.lex index 2c5600867..599d33636 100644 --- a/parse.lex +++ b/parse.lex @@ -292,6 +292,12 @@ EXEC[[:blank:]]*: { BEGIN INITIAL; } +sudoedit { + BEGIN GOTCMND; + LEXTRACE("COMMAND "); + fill_cmnd(yytext, yyleng); + } /* sudo -e */ + \/(\\[\,:= \t#]|[^\,:=\\ \t\n#])+ { /* directories can't have args... */ if (yytext[yyleng - 1] == '/') { diff --git a/sudo.c b/sudo.c index e8e0d0a8b..0b287952c 100644 --- a/sudo.c +++ b/sudo.c @@ -121,6 +121,7 @@ static void set_loginclass __P((struct passwd *)); static void usage __P((int)); static void usage_excl __P((int)); static struct passwd *get_authpw __P((void)); +extern int sudo_edit __P((int, char **)); extern void list_matches __P((void)); extern char **rebuild_env __P((char **, int, int)); extern char **zero_env __P((char **)); @@ -220,6 +221,8 @@ main(argc, argv, envp) pwflag = 0; if (sudo_mode & MODE_SHELL) user_cmnd = "shell"; + else if (sudo_mode & MODE_EDIT) + user_cmnd = "sudoedit"; else switch (sudo_mode) { case MODE_VERSION: @@ -344,8 +347,11 @@ main(argc, argv, envp) if (!(validated & FLAG_NOPASS)) check_user(validated & FLAG_CHECK_USER); - /* Build up custom environment that avoids any nasty bits. */ - new_environ = rebuild_env(envp, sudo_mode, (validated & FLAG_NOEXEC)); + /* Build a new environment that avoids any nasty bits if we have a cmnd. */ + if (sudo_mode & MODE_RUN) + new_environ = rebuild_env(envp, sudo_mode, (validated & FLAG_NOEXEC)); + else + new_environ = envp; if (validated & VALIDATE_OK) { /* Finally tell the user if the command did not exist. */ @@ -382,17 +388,18 @@ main(argc, argv, envp) (void) setrlimit(RLIMIT_CORE, &corelimit); #endif /* RLIMIT_CORE && !SUDO_DEVEL */ - /* Become specified user or root. */ - set_perms(PERM_FULL_RUNAS); + /* Become specified user or root if executing a command. */ + if (sudo_mode & MODE_RUN) + set_perms(PERM_FULL_RUNAS); /* Close the password and group files */ endpwent(); endgrent(); - /* Install the new environment. */ + /* Install the real environment. */ environ = new_environ; - if ((sudo_mode & MODE_LOGIN_SHELL)) { + if (sudo_mode & MODE_LOGIN_SHELL) { char *p; /* Convert /bin/sh -> -sh so shell knows it is a login shell */ @@ -412,6 +419,9 @@ main(argc, argv, envp) (void) sigaction(SIGTSTP, &saved_sa_tstp, NULL); (void) sigaction(SIGCHLD, &saved_sa_chld, NULL); + if (sudo_mode & MODE_EDIT) + exit(sudo_edit(NewArgc, NewArgv)); + #ifndef PROFILING if ((sudo_mode & MODE_BACKGROUND) && fork() > 0) exit(0); @@ -571,14 +581,16 @@ init_vars(sudo_mode) set_perms(PERM_ROOT); /* - * If we were given the '-i' or '-s' options (run shell) we need to redo + * If we were given the '-e', '-i' or '-s' options we need to redo * NewArgv and NewArgc. */ - if ((sudo_mode & MODE_SHELL)) { + if ((sudo_mode & (MODE_SHELL | MODE_EDIT))) { char **dst, **src = NewArgv; NewArgv = (char **) emalloc2((++NewArgc + 1), sizeof(char *)); - if (sudo_mode & MODE_LOGIN_SHELL) + if (sudo_mode & MODE_EDIT) + NewArgv[0] = "sudoedit"; + else if (sudo_mode & MODE_LOGIN_SHELL) NewArgv[0] = runas_pw->pw_shell; else if (user_shell && *user_shell) NewArgv[0] = user_shell; @@ -594,16 +606,19 @@ init_vars(sudo_mode) set_loginclass(sudo_user.pw); /* Resolve the path and return. */ - if ((sudo_mode & MODE_RUN)) { - /* XXX - default_runas may be modified during parsing of sudoers */ - set_perms(PERM_RUNAS); - rval = find_path(NewArgv[0], &user_cmnd, user_path); - set_perms(PERM_ROOT); - if (rval != FOUND) { - /* Failed as root, try as invoking user. */ - set_perms(PERM_USER); + rval = FOUND; + if (sudo_mode & (MODE_RUN | MODE_EDIT)) { + if (sudo_mode & MODE_RUN) { + /* XXX - default_runas may be modified during parsing of sudoers */ + set_perms(PERM_RUNAS); rval = find_path(NewArgv[0], &user_cmnd, user_path); set_perms(PERM_ROOT); + if (rval != FOUND) { + /* Failed as root, try as invoking user. */ + set_perms(PERM_USER); + rval = find_path(NewArgv[0], &user_cmnd, user_path); + set_perms(PERM_ROOT); + } } /* set user_args */ @@ -611,8 +626,8 @@ init_vars(sudo_mode) char *to, **from; size_t size, n; - /* If MODE_SHELL not set then NewArgv is contiguous so just count */ - if (!(sudo_mode & MODE_SHELL)) { + /* If we didn't realloc NewArgv it is contiguous so just count. */ + if (!(sudo_mode & (MODE_SHELL | MODE_EDIT))) { size = (size_t) (NewArgv[NewArgc-1] - NewArgv[1]) + strlen(NewArgv[NewArgc-1]) + 1; } else { @@ -620,7 +635,7 @@ init_vars(sudo_mode) size += strlen(*from) + 1; } - /* alloc and copy. */ + /* Alloc and build up user_args. */ user_args = (char *) emalloc(size); for (to = user_args, from = NewArgv + 1; *from; from++) { n = strlcpy(to, *from, size - (to - user_args)); @@ -631,8 +646,7 @@ init_vars(sudo_mode) } *--to = '\0'; } - } else - rval = FOUND; + } return(rval); } @@ -709,6 +723,11 @@ parse_args(argc, argv) case 'b': rval |= MODE_BACKGROUND; break; + case 'e': + rval = MODE_EDIT; + if (excl && excl != 'e') + usage_excl(1); + break; case 'v': rval = MODE_VALIDATE; if (excl && excl != 'v') @@ -787,7 +806,8 @@ parse_args(argc, argv) NewArgv++; } - if (NewArgc > 0 && !(rval & MODE_RUN)) + if ((NewArgc == 0 && (rval & MODE_EDIT)) || + (NewArgc > 0 && !(rval & (MODE_RUN | MODE_EDIT)))) usage(1); return(rval); @@ -1041,8 +1061,7 @@ static void usage_excl(exit_val) int exit_val; { - (void) fprintf(stderr, - "Only one of the -h, -k, -K, -l, -s, -v or -V options may be used\n"); + warnx("Only one of the -e, -h, -k, -K, -l, -s, -v or -V options may be used"); usage(exit_val); } @@ -1055,7 +1074,7 @@ usage(exit_val) { (void) fprintf(stderr, "usage: sudo -K | -L | -V | -h | -k | -l | -v\n"); - (void) fprintf(stderr, "usage: sudo [-HPSb]%s%s [-p prompt] [-u username|#uid]\n { -i | -s | }\n", + (void) fprintf(stderr, "usage: sudo [-HPSb]%s%s [-p prompt] [-u username|#uid]\n { -e file [...] | -i | -s | }\n", #ifdef HAVE_BSD_AUTH_H " [-a auth_type]", #else diff --git a/sudo_edit.c b/sudo_edit.c new file mode 100644 index 000000000..b6bfd9b49 --- /dev/null +++ b/sudo_edit.c @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2004 Todd C. Miller + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * 4. Products derived from this software may not be called "Sudo" nor + * may "Sudo" appear in their names without specific prior written + * permission from the author. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#ifdef STDC_HEADERS +# include +# include +#else +# ifdef HAVE_STDLIB_H +# include +# endif +#endif /* STDC_HEADERS */ +#ifdef HAVE_STRING_H +# include +#else +# ifdef HAVE_STRINGS_H +# include +# endif +#endif /* HAVE_STRING_H */ +#ifdef HAVE_UNISTD_H +# include +#endif /* HAVE_UNISTD_H */ +#ifdef HAVE_ERR_H +# include +#else +# include "emul/err.h" +#endif /* HAVE_ERR_H */ +#include +#include +#include +#include + +#include "sudo.h" + +#ifndef lint +static const char rcsid[] = "$Sudo$"; +#endif /* lint */ + +/* + * Wrapper to allow users to edit privileged files with their own uid. + */ +int sudo_edit(argc, argv) + int argc; + char **argv; +{ + ssize_t nread, nwritten; + pid_t pid; + const char *tmpdir; + char **nargv, **ap, *editor, *cp; + char buf[BUFSIZ]; + int i, ac, ofd, tfd, nargc, rval; + struct stat sb; + struct tempfile { + char *tfile; + char *ofile; + time_t omtime; /* XXX - use st_mtimespec / st_mtim? */ + off_t osize; + } *tf; + + /* + * Find our temporary directory, one of /var/tmp, /usr/tmp, or /tmp + */ + if (stat(_PATH_VARTMP, &sb) == 0 && S_ISDIR(sb.st_mode)) + tmpdir = _PATH_VARTMP; + else if (stat(_PATH_USRTMP, &sb) == 0 && S_ISDIR(sb.st_mode)) + tmpdir = _PATH_USRTMP; + else + tmpdir = _PATH_TMP; + + /* + * For each file specified, by the user, make a tempoary version + * and copy the contents of the original to it. We make these files + * as root so the user can't steal them out from under us until we are + * done writing (and at that point the user will be able to edit the + * file anyway). + * XXX - It would be nice to lock the original files but that means + * keeping an fd open for each file. + */ + tf = emalloc2(argc - 1, sizeof(*tf)); + memset(tf, 0, (argc - 1) * sizeof(*tf)); + for (i = 0, ap = argv + 1; i < argc - 1 && *ap != NULL; i++, ap++) { + set_perms(PERM_RUNAS); + ofd = open(*ap, O_RDONLY, 0644); + if (ofd != -1) { +#ifdef HAVE_FSTAT + if (fstat(ofd, &sb) != 0) { +#else + if (stat(tf[i].ofile, &sb) != 0) { +#endif + close(ofd); + ofd = -1; + } + } + set_perms(PERM_ROOT); + if (ofd == -1) { + if (errno != ENOENT) { + warn("%s", *ap); + argc--; + i--; + continue; + } + sb.st_mtime = 0; + sb.st_size = 0; + } + tf[i].ofile = *ap; + tf[i].omtime = sb.st_mtime; + tf[i].osize = sb.st_size; + if ((cp = strrchr(tf[i].ofile, '/')) != NULL) + cp++; + else + cp = tf[i].ofile; + easprintf(&tf[i].tfile, "%s%s.XXXXXXXX", tmpdir, cp); + if ((tfd = mkstemp(tf[i].tfile)) == -1) { + warn("mkstemp"); + goto cleanup; + } + if (ofd != -1) { + while ((nread = read(ofd, buf, sizeof(buf))) != 0) { + if ((nwritten = write(tfd, buf, nread)) != nread) { + if (nwritten == -1) + warn("%s", tf[i].tfile); + else + warnx("%s: short write", tf[i].tfile); + goto cleanup; + } + } + } +#ifdef HAVE_FCHOWN + fchown(tfd, user_uid, user_gid); +#else + chown(tf[i].tfile, user_uid, user_gid); +#endif + if (ofd != -1) + close(ofd); + close(tfd); + touch(tf[i].tfile, tf[i].omtime); + } + if (argc == 1) + return(1); /* no files readable, you lose */ + + /* + * Determine which editor to use. We don't bother restricting this + * based on def_env_editor or def_editor since the editor runs with + * the uid of the invoking user, not the runas (privileged) user. + */ + if (((editor = getenv("VISUAL")) != NULL && *editor != '\0') || + ((editor = getenv("EDITOR")) != NULL && *editor != '\0')) { + editor = estrdup(editor); + } else { + editor = estrdup(def_editor); + if ((cp = strchr(editor, ':')) != NULL) + *cp = '\0'; /* def_editor could be a path */ + } + + /* + * Allocate space for the new argument vector and fill it in. + * The EDITOR and VISUAL environment variables may contain command + * line args so look for those and alloc space for them too. + */ + nargc = argc; + for (cp = editor + 1; *cp != '\0'; cp++) { + if (isblank((unsigned char)cp[0]) && !isblank((unsigned char)cp[-1])) + nargc++; + } + nargv = (char **) emalloc2(nargc + 1, sizeof(char *)); + ac = 0; + for ((cp = strtok(editor, " \t")); cp != NULL; (cp = strtok(NULL, " \t"))) + nargv[ac++] = cp; + for (i = 0; i < argc - 1 && ac < nargc; ) + nargv[ac++] = tf[i++].tfile; + nargv[ac] = NULL; + + /* + * Fork and exec the editor as with the invoking user's creds. + */ + pid = fork(); + if (pid == -1) { + warn("fork"); + goto cleanup; + } else if (pid == 0) { + /* child */ + set_perms(PERM_FULL_USER); + execvp(nargv[0], nargv); + warn("unable to execute %s", nargv[0]); + _exit(127); + } + + /* In parent, wait for child to finish. */ +#ifdef sudo_waitpid + pid = sudo_waitpid(pid, &i, 0); +#else + pid = wait(&i); +#endif + rval = pid == -1 ? -1 : (i >> 8); + + /* Copy contents of temp files to real ones */ + for (i = 0; i < argc - 1; i++) { + /* XXX - open file with PERM_USER for nfs? */ + if ((tfd = open(tf[i].tfile, O_RDONLY, 0644)) == -1) { + warn("unable to read edited file %s, cannot update %s", + tf[i].tfile, tf[i].ofile); + continue; + } +#ifdef HAVE_FSTAT + if (fstat(tfd, &sb) == 0) { +#else + if (stat(tf[i].tfile, &sb) == 0) { +#endif + if (tf[i].osize == sb.st_size && tf[i].omtime == sb.st_mtime) { + warnx("%s unchanged", tf[i].ofile); + unlink(tf[i].tfile); + close(tfd); + continue; + } + } + set_perms(PERM_RUNAS); + ofd = open(tf[i].ofile, O_WRONLY|O_TRUNC|O_CREAT, 0644); + set_perms(PERM_ROOT); + if (ofd == -1) { + warn("unable to save to %s, contents of edit session saved in %s", + tf[i].ofile, tf[i].tfile); + close(tfd); + continue; + } + while ((nread = read(tfd, buf, sizeof(buf))) != 0) { + if ((nwritten = write(ofd, buf, nread)) != nread) { + if (nwritten == -1) + warn("%s", tf[i].ofile); + else + warnx("%s: short write", tf[i].ofile); + break; + } + } + if (nread == 0) + unlink(tf[i].tfile); + else + warn("unable to save to %s, contents of edit session saved in %s", + tf[i].ofile, tf[i].tfile); + close(ofd); + close(tfd); + } + + return(rval); +cleanup: + /* Clean up temp files and return. */ + for (i = 0; i < argc - 1; i++) { + if (tf[i].tfile != NULL) + unlink(tf[i].tfile); + } + return(1); +} -- 2.40.0