Fix potential TOCTOU when creating time stamp directory and file.

This commit is contained in:
Todd C. Miller
2022-09-21 19:08:09 -06:00
parent 8c482bfeb2
commit cbd52e705c
5 changed files with 223 additions and 118 deletions

View File

@@ -1,7 +1,7 @@
/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 2013-2018 Todd C. Miller <Todd.Miller@sudo.ws>
* Copyright (c) 2013-2022 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
@@ -247,6 +247,8 @@ sudo_dso_public const char *sudo_logpri2str_v1(int num);
/* mkdir_parents.c */
sudo_dso_public bool sudo_mkdir_parents_v1(const char *path, uid_t uid, gid_t gid, mode_t mode, bool quiet);
#define sudo_mkdir_parents(_a, _b, _c, _d, _e) sudo_mkdir_parents_v1((_a), (_b), (_c), (_d), (_e))
sudo_dso_public int sudo_open_parent_dir_v1(const char *path, uid_t uid, gid_t gid, mode_t mode, bool quiet);
#define sudo_open_parent_dir(_a, _b, _c, _d, _e) sudo_open_parent_dir_v1((_a), (_b), (_c), (_d), (_e))
/* mmap_alloc.c */
sudo_dso_public void *sudo_mmap_alloc_v1(size_t size) sudo_malloclike;
@@ -295,6 +297,10 @@ sudo_dso_public int sudo_secure_dir_v1(const char *path, uid_t uid, gid_t gid, s
#define sudo_secure_dir(_a, _b, _c, _d) sudo_secure_dir_v1((_a), (_b), (_c), (_d))
sudo_dso_public int sudo_secure_file_v1(const char *path, uid_t uid, gid_t gid, struct stat *sbp);
#define sudo_secure_file(_a, _b, _c, _d) sudo_secure_file_v1((_a), (_b), (_c), (_d))
sudo_dso_public int sudo_secure_open_file_v1(const char *path, uid_t uid, gid_t gid, int *error);
#define sudo_secure_open_file(_a, _b, _c, _d) sudo_secure_open_file_v1((_a), (_b), (_c), (_d))
sudo_dso_public int sudo_secure_open_dir_v1(const char *path, uid_t uid, gid_t gid, int *error);
#define sudo_secure_open_dir(_a, _b, _c, _d) sudo_secure_open_dir_v1((_a), (_b), (_c), (_d))
/* setgroups.c */
sudo_dso_public int sudo_setgroups_v1(int ngids, const GETGROUPS_T *gids);

View File

@@ -1,7 +1,7 @@
/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 2009-2021 Todd C. Miller <Todd.Miller@sudo.ws>
* Copyright (c) 2009-2022 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
@@ -76,16 +76,17 @@ is_dir(int dfd, const char *name, int namelen, bool quiet)
}
/*
* Create any parent directories needed by path (but not path itself).
* Create any parent directories needed by path (but not path itself)
* and return an open fd for the parent directory or -1 on error.
*/
bool
sudo_mkdir_parents_v1(const char *path, uid_t uid, gid_t gid, mode_t mode, bool quiet)
int
sudo_open_parent_dir_v1(const char *path, uid_t uid, gid_t gid, mode_t mode,
bool quiet)
{
const char *cp, *ep, *pathend;
char name[PATH_MAX];
bool ret = false;
int parentfd;
debug_decl(sudo_mkdir_parents, SUDO_DEBUG_UTIL);
debug_decl(sudo_open_parent_dir, SUDO_DEBUG_UTIL);
/* Starting parent dir is either root or cwd. */
cp = path;
@@ -100,7 +101,7 @@ sudo_mkdir_parents_v1(const char *path, uid_t uid, gid_t gid, mode_t mode, bool
if (parentfd == -1) {
if (!quiet)
sudo_warn(U_("unable to open %s"), *path == '/' ? "/" : ".");
debug_return_bool(false);
debug_return_int(-1);
}
/* Iterate over path components, skipping the last one. */
@@ -129,7 +130,7 @@ reopen:
sudo_warn(U_("unable to open %.*s"),
(int)(ep - path), path);
}
goto done;
goto bad;
}
if (mkdirat(parentfd, name, mode) == 0) {
dfd = openat(parentfd, name, O_RDONLY|O_NONBLOCK|O_NOFOLLOW, 0);
@@ -138,12 +139,12 @@ reopen:
sudo_warn(U_("unable to open %.*s"),
(int)(ep - path), path);
}
goto done;
goto bad;
}
/* Make sure the path we created is still a directory. */
if (!is_dir(dfd, path, ep - path, quiet)) {
close(dfd);
goto done;
goto bad;
}
if (uid != (uid_t)-1 && gid != (gid_t)-1) {
if (fchown(dfd, uid, gid) != 0) {
@@ -159,22 +160,39 @@ reopen:
sudo_warn(U_("unable to mkdir %.*s"),
(int)(ep - path), path);
}
goto done;
goto bad;
}
} else {
/* Already exists, make sure it is a directory. */
if (!is_dir(dfd, path, ep - path, quiet)) {
close(dfd);
goto done;
goto bad;
}
}
close(parentfd);
parentfd = dfd;
}
ret = true;
done:
debug_return_int(parentfd);
bad:
if (parentfd != -1)
close(parentfd);
debug_return_bool(ret);
debug_return_int(-1);
}
/*
* Create any parent directories needed by path (but not path itself).
*/
bool
sudo_mkdir_parents_v1(const char *path, uid_t uid, gid_t gid, mode_t mode,
bool quiet)
{
int fd;
debug_decl(sudo_mkdir_parents, SUDO_DEBUG_UTIL);
fd = sudo_open_parent_dir(path, uid, gid, mode, quiet);
if (fd == -1)
debug_return_bool(false);
close(fd);
debug_return_bool(true);
}

View File

@@ -1,7 +1,7 @@
/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 2012, 2014-2016 Todd C. Miller <Todd.Miller@sudo.ws>
* Copyright (c) 2012, 2014-2022 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
@@ -24,7 +24,9 @@
#include <config.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include "sudo_compat.h"
#include "sudo_util.h"
@@ -34,16 +36,11 @@
* Verify that path is the right type and not writable by other users.
*/
static int
sudo_secure_path(const char *path, unsigned int type, uid_t uid, gid_t gid, struct stat *sb)
sudo_check_secure(struct stat *sb, unsigned int type, uid_t uid, gid_t gid)
{
struct stat stat_buf;
int ret = SUDO_PATH_MISSING;
debug_decl(sudo_secure_path, SUDO_DEBUG_UTIL);
int ret = SUDO_PATH_SECURE;
debug_decl(sudo_check_secure, SUDO_DEBUG_UTIL);
if (sb == NULL)
sb = &stat_buf;
if (path != NULL && stat(path, sb) == 0) {
if ((sb->st_mode & S_IFMT) != type) {
ret = SUDO_PATH_BAD_TYPE;
} else if (uid != (uid_t)-1 && sb->st_uid != uid) {
@@ -53,28 +50,89 @@ sudo_secure_path(const char *path, unsigned int type, uid_t uid, gid_t gid, stru
} else if (ISSET(sb->st_mode, S_IWGRP) &&
(gid == (gid_t)-1 || sb->st_gid != gid)) {
ret = SUDO_PATH_GROUP_WRITABLE;
} else {
ret = SUDO_PATH_SECURE;
}
}
debug_return_int(ret);
}
/*
* Verify that path is the right type and not writable by other users.
*/
static int
sudo_secure_path(const char *path, unsigned int type, uid_t uid, gid_t gid,
struct stat *sb)
{
int ret = SUDO_PATH_MISSING;
struct stat stat_buf;
debug_decl(sudo_secure_path, SUDO_DEBUG_UTIL);
if (sb == NULL)
sb = &stat_buf;
if (path != NULL && stat(path, sb) == 0)
ret = sudo_check_secure(sb, type, uid, gid);
debug_return_int(ret);
}
/*
* Verify that path is a regular file and not writable by other users.
*/
int
sudo_secure_file_v1(const char *path, uid_t uid, gid_t gid, struct stat *st)
sudo_secure_file_v1(const char *path, uid_t uid, gid_t gid, struct stat *sb)
{
return sudo_secure_path(path, S_IFREG, uid, gid, st);
return sudo_secure_path(path, S_IFREG, uid, gid, sb);
}
/*
* Verify that path is a directory and not writable by other users.
*/
int
sudo_secure_dir_v1(const char *path, uid_t uid, gid_t gid, struct stat *st)
sudo_secure_dir_v1(const char *path, uid_t uid, gid_t gid, struct stat *sb)
{
return sudo_secure_path(path, S_IFDIR, uid, gid, st);
return sudo_secure_path(path, S_IFDIR, uid, gid, sb);
}
/*
* Open path read-only as long as it is not writable by other users.
* Returns an open file descriptor on success, else -1.
* Sets error to SUDO_PATH_SECURE on success, and a value < 0 on failure.
*/
static int
sudo_secure_open(const char *path, int type, uid_t uid, gid_t gid, int *error)
{
struct stat sb;
int fd;
debug_decl(sudo_secure_open, SUDO_DEBUG_UTIL);
fd = open(path, O_RDONLY|O_NONBLOCK);
if (fd == -1 || fstat(fd, &sb) != 0) {
if (fd != -1)
close(fd);
*error = SUDO_PATH_MISSING;
debug_return_int(-1);
}
*error = sudo_check_secure(&sb, type, uid, gid);
if (*error == SUDO_PATH_SECURE) {
(void)fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
} else {
/* Not secure, caller can check error flag. */
close(fd);
fd = -1;
}
debug_return_int(fd);
}
int
sudo_secure_open_file_v1(const char *path, uid_t uid, gid_t gid, int *error)
{
return sudo_secure_open(path, S_IFREG, uid, gid, error);
}
int
sudo_secure_open_dir_v1(const char *path, uid_t uid, gid_t gid, int *error)
{
return sudo_secure_open(path, S_IFDIR, uid, gid, error);
}

View File

@@ -116,6 +116,7 @@ sudo_mmap_free_v1
sudo_mmap_protect_v1
sudo_mmap_strdup_v1
sudo_new_key_val_v1
sudo_open_parent_dir_v1
sudo_parse_gids_v1
sudo_parseln_v1
sudo_parseln_v2
@@ -127,6 +128,8 @@ sudo_rcstr_dup
sudo_regex_compile_v1
sudo_secure_dir_v1
sudo_secure_file_v1
sudo_secure_open_dir_v1
sudo_secure_open_file_v1
sudo_setgroups_v1
sudo_str2logfac_v1
sudo_str2logpri_v1

View File

@@ -181,66 +181,76 @@ ts_find_record(int fd, struct timestamp_entry *key, struct timestamp_entry *entr
/*
* Create a directory and any missing parent directories with the
* specified mode.
* Returns true on success.
* Returns false on failure and displays a warning to stderr.
* Returns an fd usable with the *at() functions on success.
* Returns -1 on failure, setting errno.
*/
static bool
static int
ts_mkdirs(const char *path, uid_t owner, gid_t group, mode_t mode,
mode_t parent_mode, bool quiet)
{
bool ret;
int parentfd, fd = -1;
const char *base;
mode_t omask;
debug_decl(ts_mkdirs, SUDOERS_DEBUG_AUTH);
/* Child directory we will create. */
base = sudo_basename(path);
/* umask must not be more restrictive than the file modes. */
omask = umask(ACCESSPERMS & ~(mode|parent_mode));
ret = sudo_mkdir_parents(path, owner, group, parent_mode, quiet);
if (ret) {
parentfd = sudo_open_parent_dir(path, owner, group, parent_mode, quiet);
if (parentfd != -1) {
/* Create final path component. */
sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
"mkdir %s, mode 0%o, uid %d, gid %d", path, (unsigned int)mode,
(int)owner, (int)group);
if (mkdir(path, mode) != 0 && errno != EEXIST) {
if (mkdirat(parentfd, base, mode) != 0 && errno != EEXIST) {
if (!quiet)
sudo_warn(U_("unable to mkdir %s"), path);
ret = false;
} else {
if (chown(path, owner, group) != 0) {
fd = openat(parentfd, base, O_RDONLY|O_NONBLOCK, 0);
if (fd == -1) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to open %s", __func__, path);
} else if (fchown(fd, owner, group) != 0) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to chown %d:%d %s", __func__,
(int)owner, (int)group, path);
}
}
close(parentfd);
}
umask(omask);
debug_return_bool(ret);
debug_return_int(fd);
}
/*
* Check that path is owned by timestamp_uid and not writable by
* group or other. If path is missing and make_it is true, create
* the directory and its parent dirs.
* Returns true on success or false on failure, setting errno.
*
* Returns an fd usable with the *at() functions on success.
* Returns -1 on failure, setting errno.
*/
static bool
ts_secure_dir(char *path, bool make_it, bool quiet)
static int
ts_secure_opendir(const char *path, bool make_it, bool quiet)
{
struct stat sb;
bool ret = false;
debug_decl(ts_secure_dir, SUDOERS_DEBUG_AUTH);
int error, fd = -1;
debug_decl(ts_secure_opendir, SUDOERS_DEBUG_AUTH);
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "checking %s", path);
switch (sudo_secure_dir(path, timestamp_uid, -1, &sb)) {
case SUDO_PATH_SECURE:
ret = true;
break;
fd = sudo_secure_open_dir(path, timestamp_uid, timestamp_gid, &error);
if (fd == -1) {
switch (error) {
case SUDO_PATH_MISSING:
if (make_it && ts_mkdirs(path, timestamp_uid, timestamp_gid, S_IRWXU,
S_IRWXU|S_IXGRP|S_IXOTH, quiet)) {
ret = true;
if (make_it) {
fd = ts_mkdirs(path, timestamp_uid, timestamp_gid, S_IRWXU,
S_IRWXU|S_IXGRP|S_IXOTH, quiet);
if (fd != -1)
break;
}
errno = ENOENT;
if (!quiet)
sudo_warn("%s", path);
break;
case SUDO_PATH_BAD_TYPE:
errno = ENOTDIR;
@@ -249,9 +259,8 @@ ts_secure_dir(char *path, bool make_it, bool quiet)
break;
case SUDO_PATH_WRONG_OWNER:
if (!quiet) {
sudo_warnx(U_("%s is owned by uid %u, should be %u"),
path, (unsigned int) sb.st_uid,
(unsigned int) timestamp_uid);
sudo_warnx(U_("%s: wrong owner, should be uid %u\n"),
path, (unsigned int)timestamp_uid);
}
errno = EACCES;
break;
@@ -260,8 +269,17 @@ ts_secure_dir(char *path, bool make_it, bool quiet)
sudo_warnx(U_("%s is group writable"), path);
errno = EACCES;
break;
default:
if (!quiet) {
sudo_warnx("%s: internal error, unexpected error %d",
__func__, error);
errno = EINVAL;
}
debug_return_bool(ret);
break;
}
}
debug_return_int(fd);
}
/*
@@ -271,15 +289,15 @@ ts_secure_dir(char *path, bool make_it, bool quiet)
* Returns TIMESTAMP_OPEN_ERROR or TIMESTAMP_PERM_ERROR on error.
*/
static int
ts_open(const char *path, int flags)
ts_openat(int dfd, const char *path, int flags)
{
bool uid_changed = false;
int fd;
debug_decl(ts_open, SUDOERS_DEBUG_AUTH);
debug_decl(ts_openat, SUDOERS_DEBUG_AUTH);
if (timestamp_uid != 0)
uid_changed = set_perms(PERM_TIMESTAMP);
fd = open(path, flags, S_IRUSR|S_IWUSR);
fd = openat(dfd, path, flags, S_IRUSR|S_IWUSR);
if (uid_changed && !restore_perms()) {
/* Unable to restore permissions, should not happen. */
if (fd != -1) {
@@ -406,7 +424,7 @@ timestamp_open(const char *user, pid_t sid)
{
struct ts_cookie *cookie;
char *fname = NULL;
int tries, fd = -1;
int tries, dfd = -1, fd = -1;
debug_decl(timestamp_open, SUDOERS_DEBUG_AUTH);
/* Zero timeout means don't use the time stamp file. */
@@ -416,7 +434,8 @@ timestamp_open(const char *user, pid_t sid)
}
/* Check the validity of timestamp dir and create if missing. */
if (!ts_secure_dir(def_timestampdir, true, false))
dfd = ts_secure_opendir(def_timestampdir, true, false);
if (dfd == -1)
goto bad;
/* Open time stamp file. */
@@ -427,7 +446,7 @@ timestamp_open(const char *user, pid_t sid)
for (tries = 1; ; tries++) {
struct stat sb;
fd = ts_open(fname, O_RDWR|O_CREAT);
fd = ts_openat(dfd, user, O_RDWR|O_CREAT);
switch (fd) {
case TIMESTAMP_OPEN_ERROR:
log_warning(SLOG_SEND_MAIL, N_("unable to open %s"), fname);
@@ -453,7 +472,7 @@ timestamp_open(const char *user, pid_t sid)
sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
"removing time stamp file that predates boot time");
close(fd);
unlink(fname);
unlinkat(dfd, user, 0);
continue;
}
}
@@ -473,9 +492,12 @@ timestamp_open(const char *user, pid_t sid)
cookie->sid = sid;
cookie->pos = -1;
close(dfd);
debug_return_ptr(cookie);
bad:
if (fd != -1)
if (dfd != -1)
close(dfd);
if (fd >= 0)
close(fd);
free(fname);
debug_return_ptr(NULL);
@@ -962,7 +984,7 @@ int
timestamp_remove(bool unlink_it)
{
struct timestamp_entry key, entry;
int fd = -1, ret = true;
int dfd = -1, fd = -1, ret = true;
char *fname = NULL;
debug_decl(timestamp_remove, SUDOERS_DEBUG_AUTH);
@@ -976,6 +998,13 @@ timestamp_remove(bool unlink_it)
}
#endif
dfd = open(def_timestampdir, O_RDONLY|O_NONBLOCK);
if (dfd == -1) {
if (errno != ENOENT)
ret = -1;
goto done;
}
if (asprintf(&fname, "%s/%s", def_timestampdir, user_name) == -1) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
ret = -1;
@@ -984,12 +1013,12 @@ timestamp_remove(bool unlink_it)
/* For "sudo -K" simply unlink the time stamp file. */
if (unlink_it) {
ret = unlink(fname) ? -1 : true;
ret = unlinkat(dfd, user_name, 0) ? -1 : true;
goto done;
}
/* Open time stamp file and lock it for exclusive access. */
fd = ts_open(fname, O_RDWR);
fd = ts_openat(dfd, user_name, O_RDWR);
switch (fd) {
case TIMESTAMP_OPEN_ERROR:
if (errno != ENOENT)
@@ -1023,7 +1052,9 @@ timestamp_remove(bool unlink_it)
}
done:
if (fd != -1)
if (dfd != -1)
close(dfd);
if (fd >= 0)
close(fd);
free(fname);
debug_return_int(ret);
@@ -1035,21 +1066,17 @@ done:
bool
already_lectured(void)
{
char status_file[PATH_MAX];
bool ret = false;
struct stat sb;
int len;
int dfd;
debug_decl(already_lectured, SUDOERS_DEBUG_AUTH);
if (ts_secure_dir(def_lecture_status_dir, false, true)) {
len = snprintf(status_file, sizeof(status_file), "%s/%s",
def_lecture_status_dir, user_name);
if (len > 0 && len < ssizeof(status_file)) {
debug_return_bool(stat(status_file, &sb) == 0);
dfd = ts_secure_opendir(def_lecture_status_dir, false, true);
if (dfd != -1) {
ret = fstatat(dfd, user_name, &sb, AT_SYMLINK_NOFOLLOW) == 0;
close(dfd);
}
log_warningx(SLOG_SEND_MAIL, N_("lecture status path too long: %s/%s"),
def_lecture_status_dir, user_name);
}
debug_return_bool(false);
debug_return_bool(ret);
}
/*
@@ -1059,24 +1086,16 @@ already_lectured(void)
int
set_lectured(void)
{
char lecture_status[PATH_MAX];
int len, fd, ret = false;
int dfd, fd, ret = false;
debug_decl(set_lectured, SUDOERS_DEBUG_AUTH);
len = snprintf(lecture_status, sizeof(lecture_status), "%s/%s",
def_lecture_status_dir, user_name);
if (len < 0 || len >= ssizeof(lecture_status)) {
log_warningx(SLOG_SEND_MAIL, N_("lecture status path too long: %s/%s"),
def_lecture_status_dir, user_name);
goto done;
}
/* Check the validity of lecture dir and create if missing. */
if (!ts_secure_dir(def_lecture_status_dir, true, false))
/* Check the validity of timestamp dir and create if missing. */
dfd = ts_secure_opendir(def_lecture_status_dir, true, false);
if (dfd == -1)
goto done;
/* Create lecture file. */
fd = ts_open(lecture_status, O_WRONLY|O_CREAT|O_EXCL);
fd = ts_openat(dfd, user_name, O_WRONLY|O_CREAT|O_EXCL);
switch (fd) {
case TIMESTAMP_OPEN_ERROR:
/* Failed to open, not a fatal error. */
@@ -1091,6 +1110,7 @@ set_lectured(void)
ret = true;
break;
}
close(dfd);
done:
debug_return_int(ret);