Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-12-SP4:Update
sudo.27028
sudo-CVE-2021-23239.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File sudo-CVE-2021-23239.patch of Package sudo.27028
# HG changeset patch # User Todd C. Miller <Todd.Miller@sudo.ws> # Date 1609953360 25200 # Node ID ea19d0073c02951bbbf35342dd63304da83edce8 # Parent f1ca39a0d87089d005b78a2556e2b1a2dc17f672 Fix potential directory existing info leak in sudoedit. When creating a new file, sudoedit checks to make sure the parent directory exists so it can provide the user with a sensible error message. However, this could be used to test for the existence of directories not normally accessible to the user by pointing to them with a symbolic link when the parent directory is controlled by the user. Problem reported by Matthias Gerstner of SUSE. Index: sudo-1.8.10p3/src/sudo_edit.c =================================================================== --- sudo-1.8.10p3.orig/src/sudo_edit.c +++ sudo-1.8.10p3/src/sudo_edit.c @@ -53,6 +53,8 @@ #if defined(HAVE_SETRESUID) || defined(HAVE_SETREUID) || defined(HAVE_SETEUID) +static char edit_tmpdir[MAX(sizeof(_PATH_VARTMP), sizeof(_PATH_TMP))]; + static void switch_user(uid_t euid, gid_t egid, int ngroups, GETGROUPS_T *groups) { @@ -80,6 +82,407 @@ switch_user(uid_t euid, gid_t egid, int } /* + * Construct a temporary file name for file and return an + * open file descriptor. The temporary file name is stored + * in tfile which the caller is responsible for freeing. + */ +static int +sudo_edit_mktemp(const char *ofile, char **tfile) +{ + const char *cp, *suff; + int len, tfd; + debug_decl(sudo_edit_mktemp, SUDO_DEBUG_EDIT) + + if ((cp = strrchr(ofile, '/')) != NULL) + cp++; + else + cp = ofile; + suff = strrchr(cp, '.'); + if (suff != NULL) { + len = asprintf(tfile, "%s/%.*sXXXXXXXX%s", edit_tmpdir, + (int)(size_t)(suff - cp), cp, suff); + } else { + len = asprintf(tfile, "%s/%s.XXXXXXXX", edit_tmpdir, cp); + } + if (len == -1) { + warningx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_int(-1); + } + tfd = mkstemps(*tfile, suff ? strlen(suff) : 0); + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "%s -> %s, fd %d", ofile, *tfile, tfd); + debug_return_int(tfd); +} + +#ifndef HAVE_OPENAT +static int +sudo_openat(int dfd, const char *path, int flags, mode_t mode) +{ + int fd, odfd; + debug_decl(sudo_openat, SUDO_DEBUG_EDIT) + + if (dfd == AT_FDCWD) + debug_return_int(open(path, flags, mode)); + + /* Save cwd */ + if ((odfd = open(".", O_RDONLY)) == -1) + debug_return_int(-1); + + if (fchdir(dfd) == -1) { + close(odfd); + debug_return_int(-1); + } + + fd = open(path, flags, mode); + + /* Restore cwd */ + if (fchdir(odfd) == -1) + fatal(_("unable to restore current working directory")); + close(odfd); + + debug_return_int(fd); +} +#define openat sudo_openat +#endif /* HAVE_OPENAT */ + +#ifdef O_NOFOLLOW +static int +sudo_edit_openat_nofollow(int dfd, char *path, int oflags, mode_t mode) +{ + debug_decl(sudo_edit_openat_nofollow, SUDO_DEBUG_EDIT) + + debug_return_int(openat(dfd, path, oflags|O_NOFOLLOW, mode)); +} +#else +/* + * Returns true if fd and path don't match or path is a symlink. + * Used on older systems without O_NOFOLLOW. + */ +static bool +sudo_edit_is_symlink(int fd, char *path) +{ + struct stat sb1, sb2; + debug_decl(sudo_edit_is_symlink, SUDO_DEBUG_EDIT) + + /* + * Treat [fl]stat() failure like there was a symlink. + */ + if (fstat(fd, &sb1) == -1 || lstat(path, &sb2) == -1) + debug_return_bool(true); + + /* + * Make sure we did not open a link and that what we opened + * matches what is currently on the file system. + */ + if (S_ISLNK(sb2.st_mode) || + sb1.st_dev != sb2.st_dev || sb1.st_ino != sb2.st_ino) { + debug_return_bool(true); + } + + debug_return_bool(false); +} + +static int +sudo_edit_openat_nofollow(int dfd, char *path, int oflags, mode_t mode) +{ + int fd = -1, odfd = -1; + struct stat sb; + debug_decl(sudo_edit_openat_nofollow, SUDO_DEBUG_EDIT) + + /* Save cwd and chdir to dfd */ + if ((odfd = open(".", O_RDONLY)) == -1) + debug_return_int(-1); + if (fchdir(dfd) == -1) { + close(odfd); + debug_return_int(-1); + } + + /* + * Check if path is a symlink. This is racey but we detect whether + * we lost the race in sudo_edit_is_symlink() after the open. + */ + if (lstat(path, &sb) == -1 && errno != ENOENT) + goto done; + if (S_ISLNK(sb.st_mode)) { + errno = ELOOP; + goto done; + } + + fd = open(path, oflags, mode); + if (fd == -1) + goto done; + + /* + * Post-open symlink check. This will leave a zero-length file if + * O_CREAT was specified but it is too dangerous to try and remove it. + */ + if (sudo_edit_is_symlink(fd, path)) { + close(fd); + fd = -1; + errno = ELOOP; + } + +done: + /* Restore cwd */ + if (odfd != -1) { + if (fchdir(odfd) == -1) + fatal(_("unable to restore current working directory")); + close(odfd); + } + + debug_return_int(fd); +} +#endif /* O_NOFOLLOW */ + +#ifdef HAVE_FACCESSAT +/* + * Returns true if the open directory fd is writable by the user. + */ +static int +dir_is_writable(int dfd, struct user_details *ud, struct command_details *cd) +{ + debug_decl(dir_is_writable, SUDO_DEBUG_EDIT) + int rc; + + /* Change uid/gid/groups to invoking user, usually needs root perms. */ + if (cd->euid != ROOT_UID) { + if (seteuid(ROOT_UID) != 0) + fatal("seteuid(ROOT_UID)"); + } + switch_user(ud->uid, ud->gid, ud->ngroups, ud->groups); + + /* Access checks are done using the euid/egid and group vector. */ + rc = faccessat(dfd, ".", W_OK, AT_EACCESS); + + /* Change uid/gid/groups back to target user, may need root perms. */ + if (ud->uid != ROOT_UID) { + if (seteuid(ROOT_UID) != 0) + fatal("seteuid(ROOT_UID)"); + } + switch_user(cd->euid, cd->egid, cd->ngroups, cd->groups); + + if (rc == 0) + debug_return_int(true); + if (errno == EACCES) + debug_return_int(false); + debug_return_int(-1); +} +#else +static bool +group_matches(gid_t target, gid_t gid, int ngroups, GETGROUPS_T *groups) +{ + int i; + debug_decl(group_matches, SUDO_DEBUG_EDIT) + + if (target == gid) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "user gid %u matches directory gid %u", (unsigned int)gid, + (unsigned int)target); + debug_return_bool(true); + } + for (i = 0; i < ngroups; i++) { + if (target == groups[i]) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "user gid %u matches directory gid %u", (unsigned int)gid, + (unsigned int)target); + debug_return_bool(true); + } + } + debug_return_bool(false); +} + +/* + * Returns true if the open directory fd is writable by the user. + */ +static int +dir_is_writable(int dfd, struct user_details *ud, struct command_details *cd) +{ + struct stat sb; + debug_decl(dir_is_writable, SUDO_DEBUG_EDIT) + + if (fstat(dfd, &sb) == -1) + debug_return_int(-1); + + /* If the user owns the dir we always consider it writable. */ + if (sb.st_uid == ud->uid) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "user uid %u matches directory uid %u", (unsigned int)ud->uid, + (unsigned int)sb.st_uid); + debug_return_int(true); + } + + /* Other writable? */ + if (ISSET(sb.st_mode, S_IWOTH)) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "directory is writable by other"); + debug_return_int(true); + } + + /* Group writable? */ + if (ISSET(sb.st_mode, S_IWGRP)) { + if (group_matches(sb.st_gid, ud->gid, ud->ngroups, ud->groups)) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "directory is writable by one of the user's groups"); + debug_return_int(true); + } + } + + errno = EACCES; + debug_return_int(false); +} +#endif /* HAVE_FACCESSAT */ + +/* + * Directory open flags for use with openat(2). + * Use O_SEARCH/O_PATH and/or O_DIRECTORY where possible. + */ +#if defined(O_SEARCH) +# define DIR_OPEN_FLAGS (O_SEARCH|O_DIRECTORY) +#elif defined(O_PATH) +# define DIR_OPEN_FLAGS (O_PATH|O_DIRECTORY) +#elif defined(O_DIRECTORY) +# define DIR_OPEN_FLAGS (O_RDONLY|O_DIRECTORY) +#else +# define DIR_OPEN_FLAGS (O_RDONLY|O_NONBLOCK) +#endif + +static int +sudo_edit_open_nonwritable(char *path, int oflags, mode_t mode, + struct command_details *command_details) +{ + const int dflags = DIR_OPEN_FLAGS; + int dfd, fd, is_writable; + debug_decl(sudo_edit_open_nonwritable, SUDO_DEBUG_EDIT) + + if (path[0] == '/') { + dfd = open("/", dflags); + path++; + } else { + dfd = open(".", dflags); + if (path[0] == '.' && path[1] == '/') + path += 2; + } + if (dfd == -1) + debug_return_int(-1); + + for (;;) { + char *slash; + int subdfd; + + /* + * Look up one component at a time, avoiding symbolic links in + * writable directories. + */ + is_writable = dir_is_writable(dfd, &user_details, command_details); + if (is_writable == -1) { + close(dfd); + debug_return_int(-1); + } + + while (path[0] == '/') + path++; + slash = strchr(path, '/'); + if (slash == NULL) + break; + *slash = '\0'; + if (is_writable) + subdfd = sudo_edit_openat_nofollow(dfd, path, dflags, 0); + else + subdfd = openat(dfd, path, dflags, 0); + *slash = '/'; /* restore path */ + close(dfd); + if (subdfd == -1) + debug_return_int(-1); + path = slash + 1; + dfd = subdfd; + } + + if (is_writable) { + close(dfd); + errno = EISDIR; + debug_return_int(-1); + } + + /* + * For "sudoedit /" we will receive ENOENT from openat() and sudoedit + * will try to create a file with an empty name. We treat an empty + * path as the cwd so sudoedit can give a sensible error message. + */ + fd = openat(dfd, *path ? path : ".", oflags, mode); + close(dfd); + debug_return_int(fd); +} + +#ifdef O_NOFOLLOW +static int +sudo_edit_open(char *path, int oflags, mode_t mode, + struct command_details *command_details) +{ + const int sflags = command_details ? command_details->flags : 0; + int fd; + debug_decl(sudo_edit_open, SUDO_DEBUG_EDIT) + + if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW)) + oflags |= O_NOFOLLOW; + if (ISSET(sflags, CD_SUDOEDIT_CHECKDIR) && user_details.uid != ROOT_UID) { + fd = sudo_edit_open_nonwritable(path, oflags|O_NONBLOCK, mode, + command_details); + } else { + fd = open(path, oflags|O_NONBLOCK, mode); + } + if (fd != -1 && !ISSET(oflags, O_NONBLOCK)) + (void) fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK); + debug_return_int(fd); +} +#else +static int +sudo_edit_open(char *path, int oflags, mode_t mode, + struct command_details *command_details) +{ + const int sflags = command_details ? command_details->flags : 0; + struct stat sb; + int fd; + debug_decl(sudo_edit_open, SUDO_DEBUG_EDIT) + + /* + * Check if path is a symlink. This is racey but we detect whether + * we lost the race in sudo_edit_is_symlink() after the file is opened. + */ + if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW)) { + if (lstat(path, &sb) == -1 && errno != ENOENT) + debug_return_int(-1); + if (S_ISLNK(sb.st_mode)) { + errno = ELOOP; + debug_return_int(-1); + } + } + + if (ISSET(sflags, CD_SUDOEDIT_CHECKDIR) && user_details.uid != ROOT_UID) { + fd = sudo_edit_open_nonwritable(path, oflags|O_NONBLOCK, mode, + command_details); + } else { + fd = open(path, oflags|O_NONBLOCK, mode); + } + if (fd == -1) + debug_return_int(-1); + if (!ISSET(oflags, O_NONBLOCK)) + (void) fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK); + + /* + * Post-open symlink check. This will leave a zero-length file if + * O_CREAT was specified but it is too dangerous to try and remove it. + */ + if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW) && sudo_edit_is_symlink(fd, path)) { + close(fd); + fd = -1; + errno = ELOOP; + } + + debug_return_int(fd); +} +#endif /* O_NOFOLLOW */ + +/* * Wrapper to allow users to edit privileged files with their own uid. */ int @@ -90,8 +493,9 @@ sudo_edit(struct command_details *comman const char *tmpdir; char *cp, *suff, **nargv, **ap, **files = NULL; char buf[BUFSIZ]; - int rc, i, j, ac, ofd, tfd, nargc, rval, tmplen; + int rc, i, j, ac, ofd, tfd, nargc, tmplen; int editor_argc = 0, nfiles = 0; + int rval = -1; struct stat sb; struct timeval tv, tv1, tv2; struct tempfile { @@ -155,8 +559,36 @@ sudo_edit(struct command_details *comman command_details->ngroups, command_details->groups); if ((ofd = open(files[i], O_RDONLY, 0644)) != -1 || errno == ENOENT) { if (ofd == -1) { - memset(&sb, 0, sizeof(sb)); /* new file */ - rc = 0; + /* + * New file, verify parent dir exists unless in cwd. + * This fails early so the user knows ahead of time if the + * edit won't succeed. Additional checks are performed + * when copying the temporary file back to the origin. + */ + char *slash = strrchr(files[i], '/'); + if (slash != NULL && slash != files[i]) { + const int sflags = command_details->flags; + const int serrno = errno; + int dfd; + + /* + * The parent directory is allowed to be a symbolic + * link as long as *its* parent is not writable. + */ + slash = '\0'; + SET(command_details->flags, CD_SUDOEDIT_FOLLOW); + dfd = sudo_edit_open(files[i], DIR_OPEN_FLAGS, + S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, command_details); + command_details->flags = sflags; + if (dfd != -1) { + if (fstat(dfd, &sb) == 0 && S_ISDIR(sb.st_mode)) { + memset(&sb, 0, sizeof(sb)); + rc = 0; + } + close(dfd); + } + *slash = '/'; + errno = serrno; } else { rc = fstat(ofd, &sb); } @@ -312,14 +744,16 @@ sudo_edit(struct command_details *comman } else if (nread < 0) { warning(U_("unable to read temporary file")); warningx(U_("contents of edit session left in %s"), tf[i].tfile); + close(ofd); + close(tfd); + debug_return_int(-1); } else { warning(U_("unable to write to %s"), tf[i].ofile); warningx(U_("contents of edit session left in %s"), tf[i].tfile); } close(ofd); } - debug_return_int(rval); - + } cleanup: /* Clean up temp files and return. */ if (tf != NULL) { @@ -328,7 +762,8 @@ cleanup: unlink(tf[i].tfile); } } - debug_return_int(1); + + debug_return_int(rval); } #else /* HAVE_SETRESUID || HAVE_SETREUID || HAVE_SETEUID */ Index: sudo-1.8.10p3/src/sudo.h =================================================================== --- sudo-1.8.10p3.orig/src/sudo.h +++ sudo-1.8.10p3/src/sudo.h @@ -122,6 +122,9 @@ struct user_details { #define CD_USE_PTY 0x1000 #define CD_SET_UTMP 0x2000 #define CD_EXEC_BG 0x4000 +#define CD_SUDOEDIT_COPY 0x08000 +#define CD_SUDOEDIT_FOLLOW 0x10000 +#define CD_SUDOEDIT_CHECKDIR 0x20000 #define CD_SET_GROUPS 0x40000 struct preserved_fd {
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor