diff options
author | Kaz Kylheku <kaz@kylheku.com> | 2022-07-22 23:48:50 -0700 |
---|---|---|
committer | Kaz Kylheku <kaz@kylheku.com> | 2022-07-22 23:48:50 -0700 |
commit | 60db02c71c6678d67c9e8b73c12ec7d88fd80df7 (patch) | |
tree | 47f041043d55e962e9a25c3f8615db1c6917abba | |
parent | e1bda2f448cdad3be2a66cac2df96e9b82f5a882 (diff) | |
download | safepath-60db02c71c6678d67c9e8b73c12ec7d88fd80df7.tar.gz safepath-60db02c71c6678d67c9e8b73c12ec7d88fd80df7.tar.bz2 safepath-60db02c71c6678d67c9e8b73c12ec7d88fd80df7.zip |
safepath: new project.
-rw-r--r-- | Makefile | 8 | ||||
-rw-r--r-- | README.md | 61 | ||||
-rw-r--r-- | safepath.c | 384 | ||||
-rw-r--r-- | safepath.h | 59 | ||||
-rw-r--r-- | testsp.c | 49 |
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; + } +} |