aboutsummaryrefslogtreecommitdiffstats
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
parente1bda2f448cdad3be2a66cac2df96e9b82f5a882 (diff)
downloadsafepath-60db02c71c6678d67c9e8b73c12ec7d88fd80df7.tar.gz
safepath-60db02c71c6678d67c9e8b73c12ec7d88fd80df7.tar.bz2
safepath-60db02c71c6678d67c9e8b73c12ec7d88fd80df7.zip
safepath: new project.
-rw-r--r--Makefile8
-rw-r--r--README.md61
-rw-r--r--safepath.c384
-rw-r--r--safepath.h59
-rw-r--r--testsp.c49
5 files changed, 561 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..b9d484f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,8 @@
+DIAG_FLAGS ?= -Wall -W -Wstrict-prototypes -Wmissing-prototypes -pedantic
+OPT_FLAGS ?= -O2
+CFLAGS ?= $(OPT_FLAGS) $(DIAG_FLAGS)
+
+testsp: safepath.o
+
+clean:
+ rm -f testsp safepath.o
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3bf9b79
--- /dev/null
+++ b/README.md
@@ -0,0 +1,61 @@
+## What is `safepath`?
+
+`safepath` is tiny library written in C, targeting POSIX systems.
+
+Its core functionality is concentrated in a single function `safepath_check`.
+
+`safepath_check` performs one function: it takes a filename consisting of
+one or more path components, and returns an indication whether that name
+is safe for the process to use.
+
+Safe means that the pathname doesn't contain any component which could be
+tampered with by a user other than the real user ID of the caller, or else
+root.
+
+## What is significance of this check?
+
+Checking the permissions of an object is insufficient. Suppose we are
+a superuser process such as a server trying to access some application-
+specific password file `/data/path/to/passwd`.
+
+If we check that the permissions and ownership on this `passwd` file are all
+right (root owns it, and it's not writable to anyone), that is not by itself
+secure. If any of the directories in the path are open, an attacker could,
+for instance, insert a symbolic link like `/data/path/to -> /malicious/to`
+where `/malicious/to/passwd` is another link to `/etc/passwd`.
+
+## How does `safepath_check` defend against this sort of thing?
+
+`safepath_check` processes the path from left to right, very carefully.
+Component by component it checks that every element is not writable to
+anyone but the calling user (identified by `getuid`) or else root (which
+is implicitly trusted).
+
+`safepath_check` begins by validating the `/` (root) direcgtory if the
+input is an absolute path, or else the current directory `.` (dot) if
+the path is relative. It then goes from there.
+
+If `safepath_check` encounters an insecure directory, it stops and reports it.
+
+## What about symlinks?
+
+If `safepath_check` encounters a symlink, it performs its own symlink
+resolution, carefully. It reads the symlink target, grafts it in place of the
+symlink, and then starts checking the substituted path in the same way.
+
+`safepatch_check` stops after 8 levels of symlink indirection, reporting
+a loop. This is stricter than most systems' threshold for reporting `ELOOP`
+in name lookup.
+
+`safepath_check` does not rely on the operating system symlink resolution,
+which will transparently resolve multiple levels of symlink indirection. This
+is not safe: symlinks can point to other symlinks. A symlink which has
+tamper-proof permissions can point to a symlink which has weak permission and
+can be manipulated by a different user. Every level of symlink resolution must
+be performed by substitution, and a check of all the new components that are
+thus inserted into the path.
+
+## License
+
+`safepath` is offered under the two-clause BSD license. See the copyright
+header in the source files and the LICENSE file in the source tree.
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;
+}
diff --git a/safepath.h b/safepath.h
new file mode 100644
index 0000000..b6a8a39
--- /dev/null
+++ b/safepath.h
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+#ifndef SAFEPATH_3DFC_9950_H
+#define SAFEPATH_3DFC_9950_H
+
+#include <sys/types.h>
+#include <stdio.h>
+
+/*
+ * safepatch_check error codes
+ */
+enum {
+ SAFEPATH_OK, /* path appears safe */
+ SAFEPATH_UNSAFE, /* path traversible, unsafe */
+ SAFEPATH_PERM, /* path not traversible due to perms */
+ SAFEPATH_NOENT, /* component other than last doesn't exist */
+ SAFEPATH_INVAL, /* path is invalid */
+ SAFEPATH_NOMEM, /* out of memory */
+ SAFEPATH_LOOP, /* more than 8 levels of symlink */
+};
+
+int safepath_check(const char *name);
+
+/* Common POSIX API wrappers */
+int safepath_open(const char *name, int flags);
+int safepath_open_mode(const char *name, int flags, mode_t mode);
+
+/* STDIO wrappers */
+FILE* safepath_fopen(const char *name, const char *mode);
+FILE* safepath_freopen(const char *name, const char *mode, FILE *);
+
+#endif
diff --git a/testsp.c b/testsp.c
new file mode 100644
index 0000000..4691d31
--- /dev/null
+++ b/testsp.c
@@ -0,0 +1,49 @@
+/*
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include "safepath.h"
+
+int main(int argc, char **argv)
+{
+ (void) argc;
+
+ if (argv[0] && argv[1] && !argv[2]) {
+ int res = safepath_check(argv[1]);
+ printf("safepath_check(\"%s\") == %d\n", argv[1], res);
+ return res == SAFEPATH_OK ? 0 : EXIT_FAILURE;
+ } else if (argv[0]) {
+ printf("%s: requires exactly one argument\n", argv[0]);
+ return EXIT_FAILURE;
+ } else {
+ puts("testsp invoked with empty argument vector");
+ return EXIT_FAILURE;
+ }
+}