]> gitweb.michael.orlitzky.com - apply-default-acl.git/commitdiff
Naively ignore hard links to avoid security mishaps.
authorMichael Orlitzky <michael@orlitzky.com>
Thu, 22 Feb 2018 23:00:11 +0000 (18:00 -0500)
committerMichael Orlitzky <michael@orlitzky.com>
Fri, 23 Feb 2018 00:07:36 +0000 (19:07 -0500)
If an attacker can introduce a hard link into a directory with a
default ACL, then he may be able to trick the user into applying that
default ACL to the target of the hard link which lives somewhere else
entirely. That can be exploited to gain access to the user's files,
and is hard to detect.

To avoid that problem entirely, great care must be taken. For now, a
naive check of the target path is implemented to ensure that (at the
start of the routine) it has only one name on the filesystem. This
still admits a race condition, but is an improvement.

The new behavior is now documented in the man page, and a test has
been added to ensure that pre-existing hard links are ignored.

doc/man/apply-default-acl.1
run-tests.sh
src/apply-default-acl.c

index 75c7a757a17ffcdfb7c34aaaece9ec8dd0b0992e..bfbb724a8a7bea5121daa92e4051defa6442375a 100644 (file)
@@ -11,8 +11,8 @@ apply-default-acl \- Apply default POSIX ACLs to files and directories.
 
 .P
 If the directory containing \fIpath\fR has a default ACL, the ACL on
-\fIpath\fR is replaced with that default. Symbolic links are not
-followed.
+\fIpath\fR is replaced with that default. Neither symbolic nor hard
+links are followed.
 
 .P
 By default, a heuristic is used to determine whether or not the
index d055560fc8d0b8a90f992583d54f53f3f717ab29..7f7ba8fb41f456a54b9155e9aec26640ef285a98 100755 (executable)
@@ -730,3 +730,22 @@ other::r--
 EOF
 )
 compare
+
+
+# Ensure that hard links are ignored.
+TESTNUM=30
+TARGET="${TESTDIR}/foo"
+LINK2TARGET="${TESTDIR}/bar"
+touch "${TARGET}"
+ln "${TARGET}" "${LINK2TARGET}"
+setfacl --default --modify user:${USERS[0]}:rwx "${TESTDIR}"
+"${BIN}" "${LINK2TARGET}"
+ACTUAL=$( getfacl --omit-header "${TARGET}" )
+EXPECTED=$(cat <<EOF
+user::rw-
+group::r--
+other::r--
+
+EOF
+)
+compare
index 19505282c2acda17960281e966a9d347a5bc1611..b89a732d9251556357dc9d52396aa657af80e15d 100644 (file)
@@ -64,6 +64,31 @@ mode_t get_mode(const char* path) {
 
 
 
+/**
+ * @brief Determine if the given path might refer to an (unsafe) hard link.
+ *
+ * @param path
+ *   The path to test.
+ *
+ * @return true if we are certain that @c path does not refer to a hard
+ *   link, and false otherwise. In case of error, false is returned,
+ *   because we are not sure that @c path is not a hard link.
+ */
+bool is_hardlink_safe(const char* path) {
+  if (path == NULL) {
+    return false;
+  }
+  struct stat s;
+  int result = lstat(path, &s);
+  if (result == 0) {
+    return (s.st_nlink == 1 || S_ISDIR(s.st_mode));
+  }
+  else {
+    return false;
+  }
+}
+
+
 /**
  * @brief Determine whether or not the given path is a regular file.
  *
@@ -678,6 +703,32 @@ int apply_default_acl(const char* path, bool no_exec_mask) {
     return ACL_ERROR;
   }
 
+  /* Refuse to operate on hard links, which can be abused by an
+   * attacker to trick us into changing the ACL on a file we didn't
+   * intend to; namely the "target" of the hard link. To truly prevent
+   * that sort of mischief, we should be using file descriptors for
+   * the target and its parent directory. Then modulo a tiny race
+   * condition, we would be sure that "path" and "parent" don't change
+   * their nature between the time that we test them and when we
+   * utilize them. For contrast, the same attacker is free to replace
+   * "path" with a hard link after is_hardlink_safe() has returned
+   * "true" below.
+   *
+   * Unfortunately, our API is lacking in this area. For example,
+   * acl_set_fd() is only capable of setting the ACL_TYPE_ACCESS list,
+   * and not the ACL_TYPE_DEFAULT. Apparently the only way to operate
+   * on default ACLs is through the path name, which is inherently
+   * unreliable since the acl_*_file() calls themselves might follow
+   * links (both hard and symbolic).
+   *
+   * Some improvement could still be made by using descriptors where
+   * possible -- this would shrink the exploit window -- but for now
+   * we use a naive implementation that only keeps honest men honest.
+  */
+  if (!is_hardlink_safe(path)) {
+    return ACL_FAILURE;
+  }
+
   if (!is_regular_file(path) && !is_directory(path)) {
     return ACL_FAILURE;
   }