Pablo Greco 48fc63
From 0e64363bb21a07fa318017ea8c90597db63a9545 Mon Sep 17 00:00:00 2001
Pablo Greco 48fc63
From: David Tardon <dtardon@redhat.com>
Pablo Greco 48fc63
Date: Thu, 3 Jan 2019 14:37:34 +0100
Pablo Greco 48fc63
Subject: [PATCH] fs-util: add new CHASE_SAFE flag to chase_symlinks()
Pablo Greco 48fc63
Pablo Greco 48fc63
When the flag is specified we won't transition to a privilege-owned
Pablo Greco 48fc63
file or directory from an unprivileged-owned one. This is useful when
Pablo Greco 48fc63
privileged code wants to load data from a file unprivileged users have
Pablo Greco 48fc63
write access to, and validates the ownership, but want's to make sure
Pablo Greco 48fc63
that no symlink games are played to read a root-owned system file
Pablo Greco 48fc63
belonging to a different context.
Pablo Greco 48fc63
Pablo Greco 48fc63
(cherry picked from commit f14f1806e329fe92d01f15c22a384702f0cb4ae0)
Pablo Greco 48fc63
Pablo Greco 48fc63
Related: #1663143
Pablo Greco 48fc63
---
Pablo Greco 48fc63
 src/shared/util.c    | 43 +++++++++++++++++++++++++++++++++++++++++++
Pablo Greco 48fc63
 src/shared/util.h    |  7 ++++---
Pablo Greco 48fc63
 src/test/test-util.c | 26 ++++++++++++++++++++++++++
Pablo Greco 48fc63
 3 files changed, 73 insertions(+), 3 deletions(-)
Pablo Greco 48fc63
Pablo Greco 48fc63
diff --git a/src/shared/util.c b/src/shared/util.c
Pablo Greco 48fc63
index 385551f2b3..fc4887920f 100644
Pablo Greco 48fc63
--- a/src/shared/util.c
Pablo Greco 48fc63
+++ b/src/shared/util.c
Pablo Greco 48fc63
@@ -9203,10 +9203,22 @@ int fd_is_fs_type(int fd, statfs_f_type_t magic_value) {
Pablo Greco 48fc63
         return is_fs_type(&s, magic_value);
Pablo Greco 48fc63
 }
Pablo Greco 48fc63
 
Pablo Greco 48fc63
+static bool safe_transition(const struct stat *a, const struct stat *b) {
Pablo Greco 48fc63
+        /* Returns true if the transition from a to b is safe, i.e. that we never transition from unprivileged to
Pablo Greco 48fc63
+         * privileged files or directories. Why bother? So that unprivileged code can't symlink to privileged files
Pablo Greco 48fc63
+         * making us believe we read something safe even though it isn't safe in the specific context we open it in. */
Pablo Greco 48fc63
+
Pablo Greco 48fc63
+        if (a->st_uid == 0) /* Transitioning from privileged to unprivileged is always fine */
Pablo Greco 48fc63
+                return true;
Pablo Greco 48fc63
+
Pablo Greco 48fc63
+        return a->st_uid == b->st_uid; /* Otherwise we need to stay within the same UID */
Pablo Greco 48fc63
+}
Pablo Greco 48fc63
+
Pablo Greco 48fc63
 int chase_symlinks(const char *path, const char *original_root, unsigned flags, char **ret) {
Pablo Greco 48fc63
         _cleanup_free_ char *buffer = NULL, *done = NULL, *root = NULL;
Pablo Greco 48fc63
         _cleanup_close_ int fd = -1;
Pablo Greco 48fc63
         unsigned max_follow = 32; /* how many symlinks to follow before giving up and returning ELOOP */
Pablo Greco 48fc63
+        struct stat previous_stat;
Pablo Greco 48fc63
         bool exists = true;
Pablo Greco 48fc63
         char *todo;
Pablo Greco 48fc63
         int r;
Pablo Greco 48fc63
@@ -9250,6 +9262,11 @@ int chase_symlinks(const char *path, const char *original_root, unsigned flags,
Pablo Greco 48fc63
         if (fd < 0)
Pablo Greco 48fc63
                 return -errno;
Pablo Greco 48fc63
 
Pablo Greco 48fc63
+        if (flags & CHASE_SAFE) {
Pablo Greco 48fc63
+                if (fstat(fd, &previous_stat) < 0)
Pablo Greco 48fc63
+                        return -errno;
Pablo Greco 48fc63
+        }
Pablo Greco 48fc63
+
Pablo Greco 48fc63
         todo = buffer;
Pablo Greco 48fc63
         for (;;) {
Pablo Greco 48fc63
                 _cleanup_free_ char *first = NULL;
Pablo Greco 48fc63
@@ -9313,6 +9330,16 @@ int chase_symlinks(const char *path, const char *original_root, unsigned flags,
Pablo Greco 48fc63
                         if (fd_parent < 0)
Pablo Greco 48fc63
                                 return -errno;
Pablo Greco 48fc63
 
Pablo Greco 48fc63
+                        if (flags & CHASE_SAFE) {
Pablo Greco 48fc63
+                                if (fstat(fd_parent, &st) < 0)
Pablo Greco 48fc63
+                                        return -errno;
Pablo Greco 48fc63
+
Pablo Greco 48fc63
+                                if (!safe_transition(&previous_stat, &st))
Pablo Greco 48fc63
+                                        return -EPERM;
Pablo Greco 48fc63
+
Pablo Greco 48fc63
+                                previous_stat = st;
Pablo Greco 48fc63
+                        }
Pablo Greco 48fc63
+
Pablo Greco 48fc63
                         safe_close(fd);
Pablo Greco 48fc63
                         fd = fd_parent;
Pablo Greco 48fc63
 
Pablo Greco 48fc63
@@ -9347,6 +9374,12 @@ int chase_symlinks(const char *path, const char *original_root, unsigned flags,
Pablo Greco 48fc63
 
Pablo Greco 48fc63
                 if (fstat(child, &st) < 0)
Pablo Greco 48fc63
                         return -errno;
Pablo Greco 48fc63
+                if ((flags & CHASE_SAFE) &&
Pablo Greco 48fc63
+                    !safe_transition(&previous_stat, &st))
Pablo Greco 48fc63
+                        return -EPERM;
Pablo Greco 48fc63
+
Pablo Greco 48fc63
+                previous_stat = st;
Pablo Greco 48fc63
+
Pablo Greco 48fc63
                 if ((flags & CHASE_NO_AUTOFS) &&
Pablo Greco 48fc63
                     fd_is_fs_type(child, AUTOFS_SUPER_MAGIC) > 0)
Pablo Greco 48fc63
                         return -EREMOTE;
Pablo Greco 48fc63
@@ -9379,6 +9412,16 @@ int chase_symlinks(const char *path, const char *original_root, unsigned flags,
Pablo Greco 48fc63
 
Pablo Greco 48fc63
                                 free(done);
Pablo Greco 48fc63
 
Pablo Greco 48fc63
+                                if (flags & CHASE_SAFE) {
Pablo Greco 48fc63
+                                        if (fstat(fd, &st) < 0)
Pablo Greco 48fc63
+                                                return -errno;
Pablo Greco 48fc63
+
Pablo Greco 48fc63
+                                        if (!safe_transition(&previous_stat, &st))
Pablo Greco 48fc63
+                                                return -EPERM;
Pablo Greco 48fc63
+
Pablo Greco 48fc63
+                                        previous_stat = st;
Pablo Greco 48fc63
+                                }
Pablo Greco 48fc63
+
Pablo Greco 48fc63
                                 /* Note that we do not revalidate the root, we take it as is. */
Pablo Greco 48fc63
                                 if (isempty(root))
Pablo Greco 48fc63
                                         done = NULL;
Pablo Greco 48fc63
diff --git a/src/shared/util.h b/src/shared/util.h
Pablo Greco 48fc63
index 915c7439e8..fa3e2e3009 100644
Pablo Greco 48fc63
--- a/src/shared/util.h
Pablo Greco 48fc63
+++ b/src/shared/util.h
Pablo Greco 48fc63
@@ -1157,9 +1157,10 @@ bool is_fs_type(const struct statfs *s, statfs_f_type_t magic_value) _pure_;
Pablo Greco 48fc63
 int fd_is_fs_type(int fd, statfs_f_type_t magic_value);
Pablo Greco 48fc63
 
Pablo Greco 48fc63
 enum {
Pablo Greco 48fc63
-        CHASE_PREFIX_ROOT = 1,   /* If set, the specified path will be prefixed by the specified root before beginning the iteration */
Pablo Greco 48fc63
-        CHASE_NONEXISTENT = 2,   /* If set, it's OK if the path doesn't actually exist. */
Pablo Greco 48fc63
-        CHASE_NO_AUTOFS = 4,     /* If set, return -EREMOTE if autofs mount point found */
Pablo Greco 48fc63
+        CHASE_PREFIX_ROOT = 1U << 0,   /* If set, the specified path will be prefixed by the specified root before beginning the iteration */
Pablo Greco 48fc63
+        CHASE_NONEXISTENT = 1U << 1,   /* If set, it's OK if the path doesn't actually exist. */
Pablo Greco 48fc63
+        CHASE_NO_AUTOFS   = 1U << 2,   /* If set, return -EREMOTE if autofs mount point found */
Pablo Greco 48fc63
+        CHASE_SAFE        = 1U << 3,   /* If set, return EPERM if we ever traverse from unprivileged to privileged files or directories */
Pablo Greco 48fc63
 };
Pablo Greco 48fc63
 
Pablo Greco 48fc63
 int chase_symlinks(const char *path_with_prefix, const char *root, unsigned flags, char **ret);
Pablo Greco 48fc63
diff --git a/src/test/test-util.c b/src/test/test-util.c
Pablo Greco 48fc63
index 397c45a9f4..e5a646ec20 100644
Pablo Greco 48fc63
--- a/src/test/test-util.c
Pablo Greco 48fc63
+++ b/src/test/test-util.c
Pablo Greco 48fc63
@@ -2113,6 +2113,32 @@ static void test_chase_symlinks(void) {
Pablo Greco 48fc63
         r = chase_symlinks(p, NULL, 0, &result);
Pablo Greco 48fc63
         assert_se(r == -ENOENT);
Pablo Greco 48fc63
 
Pablo Greco 48fc63
+        if (geteuid() == 0) {
Pablo Greco 48fc63
+                p = strjoina(temp, "/priv1");
Pablo Greco 48fc63
+                assert_se(mkdir(p, 0755) >= 0);
Pablo Greco 48fc63
+
Pablo Greco 48fc63
+                q = strjoina(p, "/priv2");
Pablo Greco 48fc63
+                assert_se(mkdir(q, 0755) >= 0);
Pablo Greco 48fc63
+
Pablo Greco 48fc63
+                assert_se(chase_symlinks(q, NULL, CHASE_SAFE, NULL) >= 0);
Pablo Greco 48fc63
+
Pablo Greco 48fc63
+                assert_se(chown(q, 65534, 65534) >= 0);
Pablo Greco 48fc63
+                assert_se(chase_symlinks(q, NULL, CHASE_SAFE, NULL) >= 0);
Pablo Greco 48fc63
+
Pablo Greco 48fc63
+                assert_se(chown(p, 65534, 65534) >= 0);
Pablo Greco 48fc63
+                assert_se(chase_symlinks(q, NULL, CHASE_SAFE, NULL) >= 0);
Pablo Greco 48fc63
+
Pablo Greco 48fc63
+                assert_se(chown(q, 0, 0) >= 0);
Pablo Greco 48fc63
+                assert_se(chase_symlinks(q, NULL, CHASE_SAFE, NULL) == -EPERM);
Pablo Greco 48fc63
+
Pablo Greco 48fc63
+                assert_se(rmdir(q) >= 0);
Pablo Greco 48fc63
+                assert_se(symlink("/etc/passwd", q) >= 0);
Pablo Greco 48fc63
+                assert_se(chase_symlinks(q, NULL, CHASE_SAFE, NULL) == -EPERM);
Pablo Greco 48fc63
+
Pablo Greco 48fc63
+                assert_se(chown(p, 0, 0) >= 0);
Pablo Greco 48fc63
+                assert_se(chase_symlinks(q, NULL, CHASE_SAFE, NULL) >= 0);
Pablo Greco 48fc63
+        }
Pablo Greco 48fc63
+
Pablo Greco 48fc63
         assert_se(rm_rf_dangerous(temp, false, true, false) >= 0);
Pablo Greco 48fc63
 }
Pablo Greco 48fc63