summaryrefslogtreecommitdiffstats
path: root/src/lib/restrict-access.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/restrict-access.c')
-rw-r--r--src/lib/restrict-access.c531
1 files changed, 531 insertions, 0 deletions
diff --git a/src/lib/restrict-access.c b/src/lib/restrict-access.c
new file mode 100644
index 0000000..a8fc47d
--- /dev/null
+++ b/src/lib/restrict-access.c
@@ -0,0 +1,531 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#define _GNU_SOURCE /* setresgid() */
+#include <stdio.h> /* for AIX */
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "lib.h"
+#include "str.h"
+#include "restrict-access.h"
+#include "env-util.h"
+#include "ipwd.h"
+
+#include <time.h>
+#ifdef HAVE_PR_SET_DUMPABLE
+# include <sys/prctl.h>
+#endif
+
+static gid_t process_primary_gid = (gid_t)-1;
+static gid_t process_privileged_gid = (gid_t)-1;
+static bool process_using_priv_gid = FALSE;
+static char *chroot_dir = NULL;
+
+void restrict_access_init(struct restrict_access_settings *set)
+{
+ i_zero(set);
+
+ set->uid = (uid_t)-1;
+ set->gid = (gid_t)-1;
+ set->privileged_gid = (gid_t)-1;
+}
+
+static const char *get_uid_str(uid_t uid)
+{
+ struct passwd pw;
+ const char *ret;
+ int old_errno = errno;
+
+ if (i_getpwuid(uid, &pw) <= 0)
+ ret = dec2str(uid);
+ else
+ ret = t_strdup_printf("%s(%s)", dec2str(uid), pw.pw_name);
+ errno = old_errno;
+ return ret;
+}
+
+static const char *get_gid_str(gid_t gid)
+{
+ struct group group;
+ const char *ret;
+ int old_errno = errno;
+
+ if (i_getgrgid(gid, &group) <= 0)
+ ret = dec2str(gid);
+ else
+ ret = t_strdup_printf("%s(%s)", dec2str(gid), group.gr_name);
+ errno = old_errno;
+ return ret;
+}
+
+static void restrict_init_groups(gid_t primary_gid, gid_t privileged_gid,
+ const char *gid_source)
+{
+ string_t *str;
+
+ if (privileged_gid == (gid_t)-1) {
+ if (primary_gid == getgid() && primary_gid == getegid()) {
+ /* everything is already set */
+ return;
+ }
+
+ if (setgid(primary_gid) == 0)
+ return;
+
+ str = t_str_new(128);
+ str_printfa(str, "setgid(%s", get_gid_str(primary_gid));
+ if (gid_source != NULL)
+ str_printfa(str, " from %s", gid_source);
+ str_printfa(str, ") failed with euid=%s, gid=%s, egid=%s: %m "
+ "(This binary should probably be called with "
+ "process group set to %s instead of %s)",
+ get_uid_str(geteuid()),
+ get_gid_str(getgid()), get_gid_str(getegid()),
+ get_gid_str(primary_gid), get_gid_str(getegid()));
+ i_fatal("%s", str_c(str));
+ }
+
+ if (getegid() != 0 && primary_gid == getgid() &&
+ primary_gid == getegid()) {
+ /* privileged_gid is hopefully in saved ID. if not,
+ there's nothing we can do about it. */
+ return;
+ }
+
+#ifdef HAVE_SETRESGID
+ if (setresgid(primary_gid, primary_gid, privileged_gid) != 0) {
+ i_fatal("setresgid(%s,%s,%s) failed with euid=%s: %m",
+ get_gid_str(primary_gid), get_gid_str(primary_gid),
+ get_gid_str(privileged_gid), get_uid_str(geteuid()));
+ }
+#else
+ if (geteuid() == 0) {
+ /* real, effective, saved -> privileged_gid */
+ if (setgid(privileged_gid) < 0) {
+ i_fatal("setgid(%s) failed: %m",
+ get_gid_str(privileged_gid));
+ }
+ }
+ /* real, effective -> primary_gid
+ saved -> keep */
+ if (setregid(primary_gid, primary_gid) != 0) {
+ i_fatal("setregid(%s,%s) failed with euid=%s: %m",
+ get_gid_str(primary_gid), get_gid_str(privileged_gid),
+ get_uid_str(geteuid()));
+ }
+#endif
+}
+
+gid_t *restrict_get_groups_list(unsigned int *gid_count_r)
+{
+ gid_t *gid_list;
+ int ret, gid_count;
+
+ if ((gid_count = getgroups(0, NULL)) < 0)
+ i_fatal("getgroups() failed: %m");
+
+ /* @UNSAFE */
+ gid_list = t_new(gid_t, gid_count+1); /* +1 in case gid_count=0 */
+ if ((ret = getgroups(gid_count, gid_list)) < 0)
+ i_fatal("getgroups() failed: %m");
+
+ *gid_count_r = ret;
+ return gid_list;
+}
+
+static void drop_restricted_groups(const struct restrict_access_settings *set,
+ gid_t *gid_list, unsigned int *gid_count,
+ bool *have_root_group)
+{
+ /* @UNSAFE */
+ unsigned int i, used;
+
+ for (i = 0, used = 0; i < *gid_count; i++) {
+ if (gid_list[i] >= set->first_valid_gid &&
+ (set->last_valid_gid == 0 ||
+ gid_list[i] <= set->last_valid_gid)) {
+ if (gid_list[i] == 0)
+ *have_root_group = TRUE;
+ gid_list[used++] = gid_list[i];
+ }
+ }
+ *gid_count = used;
+}
+
+static gid_t get_group_id(const char *name)
+{
+ struct group group;
+ gid_t gid;
+
+ if (str_to_gid(name, &gid) == 0)
+ return gid;
+
+ switch (i_getgrnam(name, &group)) {
+ case -1:
+ i_fatal("getgrnam(%s) failed: %m", name);
+ case 0:
+ i_fatal("unknown group name in extra_groups: %s", name);
+ default:
+ return group.gr_gid;
+ }
+}
+
+static void fix_groups_list(const struct restrict_access_settings *set,
+ bool preserve_existing, bool *have_root_group)
+{
+ gid_t gid, *gid_list, *gid_list2;
+ const char *const *tmp, *empty = NULL;
+ unsigned int i, gid_count;
+ bool add_primary_gid;
+
+ /* if we're using a privileged GID, we can temporarily drop our
+ effective GID. we still want to be able to use its privileges,
+ so add it to supplementary groups. */
+ add_primary_gid = process_privileged_gid != (gid_t)-1;
+
+ tmp = set->extra_groups == NULL ? &empty :
+ t_strsplit_spaces(set->extra_groups, ", ");
+
+ if (preserve_existing) {
+ gid_list = restrict_get_groups_list(&gid_count);
+ drop_restricted_groups(set, gid_list, &gid_count,
+ have_root_group);
+ /* see if the list already contains the primary GID */
+ for (i = 0; i < gid_count; i++) {
+ if (gid_list[i] == process_primary_gid) {
+ add_primary_gid = FALSE;
+ break;
+ }
+ }
+ } else {
+ gid_list = NULL;
+ gid_count = 0;
+ }
+ if (gid_count == 0) {
+ /* Some OSes don't like an empty groups list,
+ so use the primary GID as the only one. */
+ gid_list = t_new(gid_t, 2);
+ gid_list[0] = process_primary_gid;
+ gid_count = 1;
+ add_primary_gid = FALSE;
+ }
+
+ if (*tmp != NULL || add_primary_gid) {
+ /* @UNSAFE: add extra groups and/or primary GID to gids list */
+ gid_list2 = t_new(gid_t, gid_count + str_array_length(tmp) + 1);
+ memcpy(gid_list2, gid_list, gid_count * sizeof(gid_t));
+ for (; *tmp != NULL; tmp++) {
+ gid = get_group_id(*tmp);
+ if (gid != process_primary_gid)
+ gid_list2[gid_count++] = gid;
+ }
+ if (add_primary_gid)
+ gid_list2[gid_count++] = process_primary_gid;
+ gid_list = gid_list2;
+ }
+
+ if (setgroups(gid_count, gid_list) < 0) {
+ if (errno == EINVAL) {
+ i_fatal("setgroups(%s) failed: Too many extra groups",
+ set->extra_groups == NULL ? "" :
+ set->extra_groups);
+ } else {
+ i_fatal("setgroups() failed: %m");
+ }
+ }
+}
+
+static const char *
+get_setuid_error_str(const struct restrict_access_settings *set, uid_t target_uid)
+{
+ string_t *str = t_str_new(128);
+
+ str_printfa(str, "setuid(%s", get_uid_str(target_uid));
+ if (set->uid_source != NULL)
+ str_printfa(str, " from %s", set->uid_source);
+ str_printfa(str, ") failed with euid=%s: %m ",
+ get_uid_str(geteuid()));
+ if (errno == EAGAIN) {
+ str_append(str, "(ulimit -u reached)");
+ } else {
+ str_printfa(str, "(This binary should probably be called with "
+ "process user set to %s instead of %s)",
+ get_uid_str(target_uid), get_uid_str(geteuid()));
+ }
+ return str_c(str);
+}
+
+void restrict_access(const struct restrict_access_settings *set,
+ enum restrict_access_flags flags, const char *home)
+{
+ bool is_root, have_root_group, preserve_groups = FALSE;
+ bool allow_root_gid;
+ bool allow_root = (flags & RESTRICT_ACCESS_FLAG_ALLOW_ROOT) != 0;
+ uid_t target_uid = set->uid;
+
+ is_root = geteuid() == 0;
+
+ if (!is_root &&
+ !set->allow_setuid_root &&
+ getuid() == 0) {
+ /* recover current effective UID */
+ if (target_uid == (uid_t)-1)
+ target_uid = geteuid();
+ else
+ i_assert(target_uid > 0);
+ /* try to elevate to root */
+ if (seteuid(0) < 0)
+ i_fatal("seteuid(0) failed: %m");
+ is_root = TRUE;
+ }
+
+ /* set the primary/privileged group */
+ process_primary_gid = set->gid;
+ process_privileged_gid = set->privileged_gid;
+ if (process_privileged_gid == process_primary_gid) {
+ /* a pointless configuration, ignore it */
+ process_privileged_gid = (gid_t)-1;
+ }
+
+ have_root_group = process_primary_gid == 0;
+ if (process_primary_gid != (gid_t)-1 ||
+ process_privileged_gid != (gid_t)-1) {
+ if (process_primary_gid == (gid_t)-1)
+ process_primary_gid = getegid();
+ restrict_init_groups(process_primary_gid,
+ process_privileged_gid, set->gid_source);
+ } else {
+ if (process_primary_gid == (gid_t)-1)
+ process_primary_gid = getegid();
+ }
+
+ /* set system user's groups */
+ if (set->system_groups_user != NULL && is_root) {
+ if (initgroups(set->system_groups_user,
+ process_primary_gid) < 0) {
+ i_fatal("initgroups(%s, %s) failed: %m",
+ set->system_groups_user,
+ get_gid_str(process_primary_gid));
+ }
+ preserve_groups = TRUE;
+ }
+
+ /* add extra groups. if we set system user's groups, drop the
+ restricted groups at the same time. */
+ if (is_root) T_BEGIN {
+ fix_groups_list(set, preserve_groups,
+ &have_root_group);
+ } T_END;
+
+ /* chrooting */
+ if (set->chroot_dir != NULL) {
+ /* kludge: localtime() must be called before chroot(),
+ or the timezone isn't known */
+ time_t t = 0;
+ (void)localtime(&t);
+
+ if (chroot(set->chroot_dir) != 0)
+ i_fatal("chroot(%s) failed: %m", set->chroot_dir);
+ /* makes static analyzers happy, and is more secure */
+ if (chdir("/") != 0)
+ i_fatal("chdir(/) failed: %m");
+
+ chroot_dir = i_strdup(set->chroot_dir);
+
+ if (home != NULL) {
+ if (chdir(home) < 0) {
+ i_error("chdir(%s) failed: %m", home);
+ }
+ }
+ }
+
+ /* uid last */
+ if (target_uid != (uid_t)-1) {
+ if (setuid(target_uid) != 0)
+ i_fatal("%s", get_setuid_error_str(set, target_uid));
+ }
+
+ /* verify that we actually dropped the privileges */
+ if ((target_uid != (uid_t)-1 && target_uid != 0) || !allow_root) {
+ if (setuid(0) == 0) {
+ if (!allow_root &&
+ (target_uid == 0 || target_uid == (uid_t)-1))
+ i_fatal("This process must not be run as root");
+
+ i_fatal("We couldn't drop root privileges");
+ }
+ }
+
+ if (set->first_valid_gid != 0)
+ allow_root_gid = FALSE;
+ else if (process_primary_gid == 0 || process_privileged_gid == 0)
+ allow_root_gid = TRUE;
+ else
+ allow_root_gid = FALSE;
+
+ if (!allow_root_gid && target_uid != 0 &&
+ (target_uid != (uid_t)-1 || !is_root)) {
+ if (getgid() == 0 || getegid() == 0 || setgid(0) == 0) {
+ if (process_primary_gid == 0)
+ i_fatal("GID 0 isn't permitted");
+ i_fatal("We couldn't drop root group privileges "
+ "(wanted=%s, gid=%s, egid=%s)",
+ get_gid_str(process_primary_gid),
+ get_gid_str(getgid()), get_gid_str(getegid()));
+ }
+ }
+}
+
+void restrict_access_set_env(const struct restrict_access_settings *set)
+{
+ if (set->system_groups_user != NULL &&
+ *set->system_groups_user != '\0')
+ env_put("RESTRICT_USER", set->system_groups_user);
+ if (set->chroot_dir != NULL && *set->chroot_dir != '\0')
+ env_put("RESTRICT_CHROOT", set->chroot_dir);
+
+ if (set->uid != (uid_t)-1)
+ env_put("RESTRICT_SETUID", dec2str(set->uid));
+ if (set->gid != (gid_t)-1)
+ env_put("RESTRICT_SETGID", dec2str(set->gid));
+ if (set->privileged_gid != (gid_t)-1)
+ env_put("RESTRICT_SETGID_PRIV", dec2str(set->privileged_gid));
+ if (set->extra_groups != NULL && *set->extra_groups != '\0')
+ env_put("RESTRICT_SETEXTRAGROUPS", set->extra_groups);
+
+ if (set->first_valid_gid != 0)
+ env_put("RESTRICT_GID_FIRST", dec2str(set->first_valid_gid));
+ if (set->last_valid_gid != 0)
+ env_put("RESTRICT_GID_LAST", dec2str(set->last_valid_gid));
+}
+
+static const char *null_if_empty(const char *str)
+{
+ return str == NULL || *str == '\0' ? NULL : str;
+}
+
+void restrict_access_get_env(struct restrict_access_settings *set_r)
+{
+ const char *value;
+
+ restrict_access_init(set_r);
+ if ((value = getenv("RESTRICT_SETUID")) != NULL) {
+ if (str_to_uid(value, &set_r->uid) < 0)
+ i_fatal("Invalid uid: %s", value);
+ }
+ if ((value = getenv("RESTRICT_SETGID")) != NULL) {
+ if (str_to_gid(value, &set_r->gid) < 0)
+ i_fatal("Invalid gid: %s", value);
+ }
+ if ((value = getenv("RESTRICT_SETGID_PRIV")) != NULL) {
+ if (str_to_gid(value, &set_r->privileged_gid) < 0)
+ i_fatal("Invalid privileged_gid: %s", value);
+ }
+ if ((value = getenv("RESTRICT_GID_FIRST")) != NULL) {
+ if (str_to_gid(value, &set_r->first_valid_gid) < 0)
+ i_fatal("Invalid first_valid_gid: %s", value);
+ }
+ if ((value = getenv("RESTRICT_GID_LAST")) != NULL) {
+ if (str_to_gid(value, &set_r->last_valid_gid) < 0)
+ i_fatal("Invalid last_value_gid: %s", value);
+ }
+
+ set_r->extra_groups = null_if_empty(getenv("RESTRICT_SETEXTRAGROUPS"));
+ set_r->system_groups_user = null_if_empty(getenv("RESTRICT_USER"));
+ set_r->chroot_dir = null_if_empty(getenv("RESTRICT_CHROOT"));
+}
+
+void restrict_access_by_env(enum restrict_access_flags flags, const char *home)
+{
+ struct restrict_access_settings set;
+
+ restrict_access_get_env(&set);
+ restrict_access(&set, flags, home);
+
+ /* clear the environment, so we don't fail if we get back here */
+ env_remove("RESTRICT_SETUID");
+ if (process_privileged_gid == (gid_t)-1) {
+ /* if we're dropping privileges before executing and
+ a privileged group is set, the groups must be fixed
+ after exec */
+ env_remove("RESTRICT_SETGID");
+ env_remove("RESTRICT_SETGID_PRIV");
+ }
+ env_remove("RESTRICT_GID_FIRST");
+ env_remove("RESTRICT_GID_LAST");
+ if (getuid() != 0)
+ env_remove("RESTRICT_SETEXTRAGROUPS");
+ else {
+ /* Preserve RESTRICT_SETEXTRAGROUPS, so if we're again dropping
+ more privileges we'll still preserve the extra groups. This
+ mainly means preserving service { extra_groups } for lmtp
+ and doveadm accesses. */
+ }
+ env_remove("RESTRICT_USER");
+ env_remove("RESTRICT_CHROOT");
+}
+
+const char *restrict_access_get_current_chroot(void)
+{
+ return chroot_dir;
+}
+
+void restrict_access_set_dumpable(bool allow ATTR_UNUSED)
+{
+#ifdef HAVE_PR_SET_DUMPABLE
+ if (prctl(PR_SET_DUMPABLE, allow ? 1 : 0, 0, 0, 0) < 0)
+ i_error("prctl(PR_SET_DUMPABLE) failed: %m");
+#endif
+}
+
+bool restrict_access_get_dumpable(void)
+{
+#ifdef HAVE_PR_SET_DUMPABLE
+ bool allow = FALSE;
+ if (prctl(PR_GET_DUMPABLE, &allow, 0, 0, 0) < 0)
+ i_error("prctl(PR_GET_DUMPABLE) failed: %m");
+ return allow;
+#endif
+ return TRUE;
+}
+
+void restrict_access_allow_coredumps(bool allow)
+{
+ if (getenv("PR_SET_DUMPABLE") != NULL) {
+ restrict_access_set_dumpable(allow);
+ }
+}
+
+int restrict_access_use_priv_gid(void)
+{
+ i_assert(!process_using_priv_gid);
+
+ if (process_privileged_gid == (gid_t)-1)
+ return 0;
+ if (setegid(process_privileged_gid) < 0) {
+ i_error("setegid(privileged) failed: %m");
+ return -1;
+ }
+ process_using_priv_gid = TRUE;
+ return 0;
+}
+
+void restrict_access_drop_priv_gid(void)
+{
+ if (!process_using_priv_gid)
+ return;
+
+ if (setegid(process_primary_gid) < 0)
+ i_fatal("setegid(primary) failed: %m");
+ process_using_priv_gid = FALSE;
+}
+
+bool restrict_access_have_priv_gid(void)
+{
+ return process_privileged_gid != (gid_t)-1;
+}
+
+void restrict_access_deinit(void)
+{
+ i_free(chroot_dir);
+}