diff options
Diffstat (limited to 'src/util/safe_open.c')
-rw-r--r-- | src/util/safe_open.c | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/src/util/safe_open.c b/src/util/safe_open.c new file mode 100644 index 0000000..c7a80cf --- /dev/null +++ b/src/util/safe_open.c @@ -0,0 +1,283 @@ +/*++ +/* NAME +/* safe_open 3 +/* SUMMARY +/* safely open or create regular file +/* SYNOPSIS +/* #include <safe_open.h> +/* +/* VSTREAM *safe_open(path, flags, mode, st, user, group, why) +/* const char *path; +/* int flags; +/* mode_t mode; +/* struct stat *st; +/* uid_t user; +/* gid_t group; +/* VSTRING *why; +/* DESCRIPTION +/* safe_open() carefully opens or creates a file in a directory +/* that may be writable by untrusted users. If a file is created +/* it is given the specified ownership and permission attributes. +/* If an existing file is opened it must not be a symbolic link, +/* it must not be a directory, and it must have only one hard link. +/* +/* Arguments: +/* .IP "path, flags, mode" +/* These arguments are the same as with open(2). The O_EXCL flag +/* must appear either in combination with O_CREAT, or not at all. +/* .sp +/* No change is made to the permissions of an existing file. +/* .IP st +/* Null pointer, or pointer to storage for the attributes of the +/* opened file. +/* .IP "user, group" +/* File ownership for a file created by safe_open(). Specify -1 +/* in order to disable user and/or group ownership change. +/* .sp +/* No change is made to the ownership of an existing file. +/* .IP why +/* A VSTRING pointer for diagnostics. +/* DIAGNOSTICS +/* Panic: interface violations. +/* +/* A null result means there was a problem. The nature of the +/* problem is returned via the \fIwhy\fR buffer; when an error +/* cannot be reported via \fIerrno\fR, the generic value EPERM +/* (operation not permitted) is used instead. +/* HISTORY +/* .fi +/* .ad +/* A safe open routine was discussed by Casper Dik in article +/* <2rdb0s$568@mail.fwi.uva.nl>, posted to comp.security.unix +/* (May 18, 1994). +/* +/* Olaf Kirch discusses how the lstat()/open()+fstat() test can +/* be fooled by delaying the open() until the inode found with +/* lstat() has been re-used for a sensitive file (article +/* <20000103212443.A5807@monad.swb.de> posted to bugtraq on +/* Jan 3, 2000). This can be a concern for a set-ugid process +/* that runs under the control of a user and that can be +/* manipulated with start/stop signals. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <stdlib.h> +#include <unistd.h> +#include <errno.h> + +/* Utility library. */ + +#include <msg.h> +#include <vstream.h> +#include <vstring.h> +#include <stringops.h> +#include <safe_open.h> +#include <warn_stat.h> + +/* safe_open_exist - open existing file */ + +static VSTREAM *safe_open_exist(const char *path, int flags, + struct stat * fstat_st, VSTRING *why) +{ + struct stat local_statbuf; + struct stat lstat_st; + int saved_errno; + VSTREAM *fp; + + /* + * Open an existing file. + */ + if ((fp = vstream_fopen(path, flags & ~(O_CREAT | O_EXCL), 0)) == 0) { + saved_errno = errno; + vstring_sprintf(why, "cannot open file: %m"); + errno = saved_errno; + return (0); + } + + /* + * Examine the modes from the open file: it must have exactly one hard + * link (so that someone can't lure us into clobbering a sensitive file + * by making a hard link to it), and it must be a non-symlink file. + */ + if (fstat_st == 0) + fstat_st = &local_statbuf; + if (fstat(vstream_fileno(fp), fstat_st) < 0) { + msg_fatal("%s: bad open file status: %m", path); + } else if (fstat_st->st_nlink != 1) { + vstring_sprintf(why, "file has %d hard links", + (int) fstat_st->st_nlink); + errno = EPERM; + } else if (S_ISDIR(fstat_st->st_mode)) { + vstring_sprintf(why, "file is a directory"); + errno = EISDIR; + } + + /* + * Look up the file again, this time using lstat(). Compare the fstat() + * (open file) modes with the lstat() modes. If there is any difference, + * either we followed a symlink while opening an existing file, someone + * quickly changed the number of hard links, or someone replaced the file + * after the open() call. The link and mode tests aren't really necessary + * in daemon processes. Set-uid programs, on the other hand, can be + * slowed down by arbitrary amounts, and there it would make sense to + * compare even more file attributes, such as the inode generation number + * on systems that have one. + * + * Grr. Solaris /dev/whatever is a symlink. We'll have to make an exception + * for symlinks owned by root. NEVER, NEVER, make exceptions for symlinks + * owned by a non-root user. This would open a security hole when + * delivering mail to a world-writable mailbox directory. + * + * Sebastian Krahmer of SuSE brought to my attention that some systems have + * changed their semantics of link(symlink, newpath), such that the + * result is a hardlink to the symlink. For this reason, we now also + * require that the symlink's parent directory is writable only by root. + */ + else if (lstat(path, &lstat_st) < 0) { + vstring_sprintf(why, "file status changed unexpectedly: %m"); + errno = EPERM; + } else if (S_ISLNK(lstat_st.st_mode)) { + if (lstat_st.st_uid == 0) { + VSTRING *parent_buf = vstring_alloc(100); + const char *parent_path = sane_dirname(parent_buf, path); + struct stat parent_st; + int parent_ok; + + parent_ok = (stat(parent_path, &parent_st) == 0 /* not lstat */ + && parent_st.st_uid == 0 + && (parent_st.st_mode & (S_IWGRP | S_IWOTH)) == 0); + vstring_free(parent_buf); + if (parent_ok) + return (fp); + } + vstring_sprintf(why, "file is a symbolic link"); + errno = EPERM; + } else if (fstat_st->st_dev != lstat_st.st_dev + || fstat_st->st_ino != lstat_st.st_ino +#ifdef HAS_ST_GEN + || fstat_st->st_gen != lstat_st.st_gen +#endif + || fstat_st->st_nlink != lstat_st.st_nlink + || fstat_st->st_mode != lstat_st.st_mode) { + vstring_sprintf(why, "file status changed unexpectedly"); + errno = EPERM; + } + + /* + * We are almost there... + */ + else { + return (fp); + } + + /* + * End up here in case of fstat()/lstat() problems or inconsistencies. + */ + vstream_fclose(fp); + return (0); +} + +/* safe_open_create - create new file */ + +static VSTREAM *safe_open_create(const char *path, int flags, mode_t mode, + struct stat * st, uid_t user, gid_t group, VSTRING *why) +{ + VSTREAM *fp; + + /* + * Create a non-existing file. This relies on O_CREAT | O_EXCL to not + * follow symbolic links. + */ + if ((fp = vstream_fopen(path, flags | (O_CREAT | O_EXCL), mode)) == 0) { + vstring_sprintf(why, "cannot create file exclusively: %m"); + return (0); + } + + /* + * Optionally look up the file attributes. + */ + if (st != 0 && fstat(vstream_fileno(fp), st) < 0) + msg_fatal("%s: bad open file status: %m", path); + + /* + * Optionally change ownership after creating a new file. If there is a + * problem we should not attempt to delete the file. Something else may + * have opened the file in the mean time. + */ +#define CHANGE_OWNER(user, group) (user != (uid_t) -1 || group != (gid_t) -1) + + if (CHANGE_OWNER(user, group) + && fchown(vstream_fileno(fp), user, group) < 0) { + msg_warn("%s: cannot change file ownership: %m", path); + } + + /* + * We are almost there... + */ + else { + return (fp); + } + + /* + * End up here in case of trouble. + */ + vstream_fclose(fp); + return (0); +} + +/* safe_open - safely open or create file */ + +VSTREAM *safe_open(const char *path, int flags, mode_t mode, + struct stat * st, uid_t user, gid_t group, VSTRING *why) +{ + VSTREAM *fp; + + switch (flags & (O_CREAT | O_EXCL)) { + + /* + * Open an existing file, carefully. + */ + case 0: + return (safe_open_exist(path, flags, st, why)); + + /* + * Create a new file, carefully. + */ + case O_CREAT | O_EXCL: + return (safe_open_create(path, flags, mode, st, user, group, why)); + + /* + * Open an existing file or create a new one, carefully. When opening + * an existing file, we are prepared to deal with "no file" errors + * only. When creating a file, we are prepared for "file exists" + * errors only. Any other error means we better give up trying. + */ + case O_CREAT: + fp = safe_open_exist(path, flags, st, why); + if (fp == 0 && errno == ENOENT) { + fp = safe_open_create(path, flags, mode, st, user, group, why); + if (fp == 0 && errno == EEXIST) + fp = safe_open_exist(path, flags, st, why); + } + return (fp); + + /* + * Interface violation. Sorry, but we must be strict. + */ + default: + msg_panic("safe_open: O_EXCL flag without O_CREAT flag"); + } +} |