File sudo-CVE-2021-23239.patch of Package sudo.18794

# 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 {
openSUSE Build Service is sponsored by