aboutsummaryrefslogtreecommitdiffstats
path: root/safepath.c
diff options
context:
space:
mode:
authorKaz Kylheku <kaz@kylheku.com>2022-07-22 23:48:50 -0700
committerKaz Kylheku <kaz@kylheku.com>2022-07-22 23:48:50 -0700
commit60db02c71c6678d67c9e8b73c12ec7d88fd80df7 (patch)
tree47f041043d55e962e9a25c3f8615db1c6917abba /safepath.c
parente1bda2f448cdad3be2a66cac2df96e9b82f5a882 (diff)
downloadsafepath-60db02c71c6678d67c9e8b73c12ec7d88fd80df7.tar.gz
safepath-60db02c71c6678d67c9e8b73c12ec7d88fd80df7.tar.bz2
safepath-60db02c71c6678d67c9e8b73c12ec7d88fd80df7.zip
safepath: new project.
Diffstat (limited to 'safepath.c')
-rw-r--r--safepath.c384
1 files changed, 384 insertions, 0 deletions
diff --git a/safepath.c b/safepath.c
new file mode 100644
index 0000000..bc3246f
--- /dev/null
+++ b/safepath.c
@@ -0,0 +1,384 @@
+/*
+ * safepath: safe path traversal for POSIX systems
+ * Copyright 2022 Kaz Kylheku <kaz@kylheku.com>
+ *
+ * BSD-2 License
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <pwd.h>
+#include <grp.h>
+#include "safepath.h"
+
+/*
+ * Returns non-zero if st informs about an object that is not writable by
+ * anyone other than the real user ID of the process, or else the superuser.
+ */
+static int safe_group(gid_t gid)
+{
+ char buf_root[256], buf_real[256], buf_grp[256];
+ struct passwd pw_root, *pwr, pw_real, *pwu;
+ struct group grp, *pgr;
+ int i;
+
+ /* Obtain passwd info about root user, to get at the name. */
+ if (getpwuid_r(0, &pw_root, buf_root, sizeof buf_root, &pwr) < 0 ||
+ pwr == 0)
+ {
+ return 0;
+ }
+
+ /* Obtain passwd info about real user ID, to get at the name. */
+ if (getpwuid_r(getuid(), &pw_real, buf_real, sizeof buf_real, &pwu) < 0 ||
+ pwu == 0)
+ {
+ return 0;
+ }
+
+ /* Obtain group info. */
+ if (getgrgid_r(gid, &grp, buf_grp, sizeof buf_grp, &pgr) < 0 ||
+ pgr == 0)
+ {
+ return 0;
+ }
+
+ /* Check that the group contains no member names other than
+ * the root user or the real user.
+ */
+ for (i = 0; ; i++) {
+ if (pgr->gr_mem[i] == 0)
+ break;
+ if (strcmp(pgr->gr_mem[i], pwr->pw_name) != 0 &&
+ strcmp(pgr->gr_mem[i], pwu->pw_name) != 0)
+ return 0;
+ }
+
+ return 1;
+}
+
+/*
+ * Returns non-zero if st informs about an object that is not writable by
+ * anyone other than the real user ID of the process, or else the superuser.
+ */
+static int tamper_proof(const struct stat *st)
+{
+ /* Owner isn't caller or root; that owner could
+ * change the permissions to whatever they want
+ * and modify the object.
+ */
+ if (st->st_uid != 0 && st->st_uid != getuid())
+ return 0;
+
+ /* Ownership is good, but permissions are open; object is writable to
+ * group owner or others. Group writability could be safe, but it's
+ * complicated to check; we just reject it for clarity and simplicity.
+ */
+ if ((st->st_mode & (S_IWGRP | S_IWOTH)) != 0) {
+ /* OK, permissions are open. But is this a directory owned by
+ * root, which has the sticky bit, such as /tmp? That's OK.
+ */
+ if (S_ISDIR(st->st_mode) && (st->st_mode & S_ISVTX) != 0)
+ return 1;
+ /* Check for some situations of just the group permissions
+ * being open, not others.
+ */
+ if ((st->st_mode & (S_IWGRP | S_IWOTH)) == S_IWGRP) {
+ /* The group owner is the superuser group.
+ * That is OK.
+ */
+ if (st->st_gid == 0)
+ return 1;
+
+ /* Otherwise, we do a complicated check
+ */
+ return safe_group(st->st_gid);
+ }
+ return 0;
+ } else {
+ return 1;
+ }
+}
+
+static int safepath_err(int eno)
+{
+ switch (eno) {
+ case 0:
+ return SAFEPATH_OK;
+ case ENOENT:
+ return SAFEPATH_NOENT;
+ case EPERM:
+ case EACCES:
+ return SAFEPATH_PERM;
+ case ENOMEM:
+ return SAFEPATH_NOMEM;
+ case ELOOP:
+ return SAFEPATH_LOOP;
+ default:
+ return SAFEPATH_INVAL;
+ }
+}
+
+static void set_errno(int spres)
+{
+ switch (spres) {
+ case SAFEPATH_OK:
+ break;
+ case SAFEPATH_UNSAFE:
+ errno = EACCES;
+ break;
+ case SAFEPATH_PERM:
+ errno = EPERM;
+ break;
+ case SAFEPATH_NOENT:
+ errno = ENOENT;
+ break;
+ case SAFEPATH_INVAL:
+ errno = EINVAL;
+ break;
+ case SAFEPATH_NOMEM:
+ errno = ENOMEM;
+ break;
+ case SAFEPATH_LOOP:
+ errno = ELOOP;
+ break;
+ }
+}
+
+int safepath_check(const char *name)
+{
+ struct stat st;
+ const char *start = (*name == '/') ? "/" : ".";
+ size_t pos = (*name == '/') ? 1 : 0;
+ char *copy;
+ int ret = SAFEPATH_OK, count = 0;
+
+ /* empty name is invalid */
+ if (*name == 0) {
+ ret = SAFEPATH_INVAL;
+ goto out;
+ }
+
+ /* check starting directory */
+ if (stat(start, &st) < 0) {
+ ret = safepath_err(errno);
+ goto out;
+ }
+
+ if (!tamper_proof(&st)) {
+ ret = SAFEPATH_UNSAFE;
+ goto out;
+ }
+
+ /* check if that was the whole path */
+ if (name[pos] == 0) {
+ ret = SAFEPATH_OK;
+ goto out;
+ }
+
+ /* now process path */
+ if ((copy = strdup(name)) == 0) {
+ ret = SAFEPATH_NOMEM;
+ goto out;
+ }
+
+ while (copy[pos] != 0) {
+ size_t nxslash = pos + strcspn(copy + pos, "/");
+ int savechar = copy[nxslash];
+
+ /* consecutive slashes */
+ if (nxslash == pos) {
+ ret = SAFEPATH_INVAL;
+ goto free_out;
+ }
+
+ /* null terminate the path at the next slash */
+ copy[nxslash] = 0;
+
+ /* use lstat in case the component is a symlink */
+ if (lstat(copy, &st) < 0) {
+ ret = safepath_err(errno);
+ goto free_out;
+ }
+
+ /* If it is a symlink, we can trust it because we validated the
+ * previous component, which is the directory it lives in. However,
+ * we trust only that link, and not what it points to. It could
+ * point to another link which is not secured against tampering.
+ * Thus, we must implement symlink resolution right here ourselves,
+ * applying our rules to every step. Recursion helps.
+ */
+ if (S_ISLNK(st.st_mode)) {
+ char link[256];
+ int len;
+
+ if (++count > 8) {
+ ret = SAFEPATH_LOOP;
+ goto free_out;
+ }
+
+ if ((len = readlink(copy, link, sizeof link - 1)) < 0) {
+ ret = safepath_err(errno);
+ goto free_out;
+ }
+
+ link[len] = 0;
+
+ /* Resolve the symlink, using two different cases based
+ * on whether the target is absolute or relative.
+ * Either way it's string grafting.
+ */
+ if (link[0] == '/') {
+ /* If savechar is zero, we are working with the last
+ * component. If the last component is an absolute
+ * symlink, we just recurse on that symlink target.
+ * Otherwise, we must graft the remainder of the
+ * path onto the symlink target.
+ */
+ if (savechar == 0) {
+ free(copy);
+ if ((copy = strdup(link)) == NULL) {
+ ret = SAFEPATH_NOMEM;
+ goto out;
+ }
+ pos = 1;
+ continue;
+ } else {
+ size_t total = len + 1 + strlen(copy + nxslash + 1) + 1;
+ char *resolved = malloc(total);
+ if (resolved == NULL) {
+ ret = SAFEPATH_NOMEM;
+ goto free_out;
+ }
+ strcpy(resolved, link);
+ resolved[len] = '/';
+ strcpy(resolved + len + 1, copy + nxslash + 1);
+ free(copy);
+ copy = resolved;
+ pos = 1;
+ continue;
+ }
+ } else {
+ if (savechar == 0) {
+ size_t total = pos + len + 1;
+ char *resolved = malloc(total);
+ if (resolved == NULL) {
+ ret = SAFEPATH_NOMEM;
+ goto free_out;
+ }
+ memcpy(resolved, copy, pos);
+ strcpy(resolved + pos, link);
+ free(copy);
+ copy = resolved;
+ continue;
+ } else {
+ size_t total = pos + len + 1 + strlen(copy + nxslash + 1) + 1;
+ char *resolved = malloc(total);
+ if (resolved == NULL) {
+ ret = SAFEPATH_NOMEM;
+ goto free_out;
+ }
+ memcpy(resolved, copy, pos);
+ strcpy(resolved + pos, link);
+ resolved[pos + len] = '/';
+ strcpy(resolved + pos + len, copy + nxslash + 1);
+ free(copy);
+ copy = resolved;
+ continue;
+ }
+ }
+ }
+
+ /* Not symlink: check if it's safe
+ * and move to next component, if any.
+ */
+ if (!tamper_proof(&st)) {
+ ret = SAFEPATH_UNSAFE;
+ goto free_out;
+ }
+
+ /* Undo null termination */
+ copy[nxslash] = savechar;
+
+ /* Start search for next slash after current slash;
+ * but not if nxslash is actually at the end of the string
+ */
+ pos = nxslash + (savechar != 0);
+ }
+
+free_out:
+ free(copy);
+out:
+ return ret;
+}
+
+int safepath_open(const char *name, int flags)
+{
+ int res = safepath_check(name);
+
+ if (res == SAFEPATH_OK)
+ return open(name, flags);
+
+ set_errno(res);
+ return -1;
+}
+
+int safepath_open_mode(const char *name, int flags, mode_t mode)
+{
+ int res = safepath_check(name);
+
+ if (res == SAFEPATH_OK)
+ return open(name, flags, mode);
+
+ set_errno(res);
+ return -1;
+}
+
+/* STDIO wrappers */
+FILE* safepath_fopen(const char *name, const char *mode)
+{
+ int res = safepath_check(name);
+
+ if (res == SAFEPATH_OK)
+ return fopen(name, mode);
+
+ set_errno(res);
+ return 0;
+}
+
+FILE* safepath_freopen(const char *name, const char *mode, FILE *stream)
+{
+ int res = safepath_check(name);
+
+ if (res == SAFEPATH_OK)
+ return freopen(name, mode, stream);
+
+ set_errno(res);
+ return 0;
+}