summaryrefslogtreecommitdiffstats
path: root/src/util/safe_open.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/util/safe_open.c')
-rw-r--r--src/util/safe_open.c283
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");
+ }
+}