/* * safepath: safe path traversal for POSIX systems * Copyright 2022 Kaz Kylheku * * 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 #include #include #include #include #include #include #include #include #include "safepath.h" /* * Regular expressions used by abs_path_check. */ static const char *bad_proc = { "^/proc/([0-9]+|self)/(cwd|root|map_files|fd/[0-9]+|task/[0-9]+/(cwd|root|fd/[0-9]+))($|/)" }; static regex_t bad_proc_rx; /* * 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 effective user ID, to get at the name. */ if (getpwuid_r(geteuid(), &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 != geteuid()) return 0; /* Ownership is good, but permissions are open; object is writable to * group owner or others. That could still be safe. */ if ((st->st_mode & (S_IWGRP | S_IWOTH)) != 0) { /* 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; } } /* * Get rid of .. and . components, without filesystem access. * Returns malloced string. */ static char *simplify_path(const char *ipath) { char *opath = malloc(strlen(ipath) + 1); if (opath != 0) { size_t ipos = 0, opos = 0; opath[ipos] = 0; if (ipath[ipos] == '/') { opath[opos++] = ipath[ipos++]; opath[ipos] = 0; } for (;;) { size_t complen = strcspn(ipath + ipos, "/"); if (complen == 2 && strncmp(ipath + ipos, "..", 2) == 0 && opos > 0) { int ppos = opos - 1; size_t pcomplen = 0; while (ppos > 0 && opath[ppos - 1] != '/') { ppos--; pcomplen++; } if (pcomplen > 0 && (pcomplen != 2 || strncmp(ipath + ppos, "..", 2) != 0)) { opos = ppos; opath[opos] = 0; goto nextcomp; } else { goto copy; } } else if ((complen == 1 && ipath[ipos] == '.') || complen == 0) { goto nextcomp; } copy: strncat(opath + opos, ipath + ipos, complen); opos += complen; if (ipath[ipos + complen]) { strcat(opath + opos, "/"); opos++; } nextcomp: ipos += complen; if (ipath[ipos] == 0) break; ipos++; } } return opath; } /* * Checks for some known system paths that can be attack vectors. */ static int abs_path_check(const char *abspath) { /* The /proc//cwd symlink cannot be trusted by root, because an * unprivileged user can use the "su" executable to point that anywhere. * Non-root cannot access that symlink, and so is safe from it. */ char *sabspath = simplify_path(abspath); int res = regexec(&bad_proc_rx, sabspath, 0, NULL, 0); free(sabspath); return res != 0; } static int safepath_err(int eno) { switch (eno) { case 0: return SAFEPATH_OK; case ENOENT: return SAFEPATH_NOENT; case ENOTDIR: return SAFEPATH_NOTDIR; 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_NOTDIR: errno = ENOTDIR; break; case SAFEPATH_INVAL: errno = EINVAL; break; case SAFEPATH_NOMEM: errno = ENOMEM; break; case SAFEPATH_LOOP: errno = ELOOP; break; } } /* * Must be called exactly once before safepath_check is used. * Returns 1 on success, 0 on failure. */ int safepath_init(void) { if (regcomp(&bad_proc_rx, bad_proc, REG_EXTENDED | REG_NOSUB) != 0) return 0; return 1; } /* * If safepath_init was was successfully called, this may be called once to * release the resources allocated by safepath_init. * The library may not be used after that. * A new call to safepath_init is permitted after safepath_cleanup. */ void safepath_cleanup(void) { regfree(&bad_proc_rx); memset(&bad_proc_rx, 0, sizeof bad_proc_rx); } int safepath_check(const char *name) { struct stat st; int abs = (*name == '/'); const char *start = abs ? "/" : "."; size_t pos = strspn(name, "/"); /* skip all leading slashes */ char *copy; int ret = SAFEPATH_OK, count = 0, root_checked = abs; /* empty name is invalid */ if (*name == 0) { ret = SAFEPATH_INVAL; goto out; } /* check absolute path for known vulnerabilities. */ if (abs && !abs_path_check(name)) { ret = SAFEPATH_UNSAFE; 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]; if (savechar) { while (copy[nxslash + 1] == '/') nxslash++; } /* 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: * replace the symlink component with its expansion and continue * checking the expansion, component by component. */ if (S_ISLNK(st.st_mode)) { char link[256]; int len; if (++count > 8) { ret = SAFEPATH_LOOP; goto free_out; } /* We check the symlink ownership and declare a symlink * not owned by us or root to be unsafe. This is particularly * important in the case when the previous component is a * sticky directory which we declared safe, similar to /tmp. * Multiple users can create symlinks in /tmp or a /tmp-like * directory, which could be used to subvert this function. */ if (st.st_uid != 0 && st.st_uid != geteuid()) { ret = SAFEPATH_UNSAFE; goto free_out; } /* A symlink with a link count > 1 is suspicious; it looks like a * hard link attack: an attacker hard linking a symlink into a * /tmp-like directory. */ if (st.st_nlink > 1) { ret = SAFEPATH_UNSAFE; goto free_out; } if ((len = readlink(copy, link, sizeof link)) < 0) { ret = safepath_err(errno); goto free_out; } if (len == 0) { ret = SAFEPATH_INVAL; goto free_out; } else if (len == sizeof link) { ret = SAFEPATH_TOOLONG; 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] == '/') { /* Check absolute path for known vulnerabilities. */ if (!abs_path_check(link)) { ret = SAFEPATH_UNSAFE; goto free_out; } /* We have to check the root directory, if we have * not done so before. */ if (!root_checked) { if (stat("/", &st) < 0) { ret = safepath_err(errno); goto free_out; } root_checked = 1; } /* If savechar is zero, we are working with the last component. * If the last component is an absolute symlink, we just replace * the path with the target, and iterate. 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 = strspn(copy, "/"); 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); if (link[len - 1] != '/') { resolved[len] = '/'; strcpy(resolved + len + 1, copy + nxslash + 1); } else { strcpy(resolved + len, copy + nxslash + 1); } free(copy); copy = resolved; pos = strspn(copy, "/"); 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); if (link[len - 1] != '/') { resolved[pos + len] = '/'; strcpy(resolved + pos + len + 1, copy + nxslash + 1); } else { 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; } const char *safepath_strerr(int err) { const char *str[] = { [SAFEPATH_OK] = "path appears safe", [SAFEPATH_UNSAFE] = "path contains untrusted component", [SAFEPATH_PERM] = "path contains inaccessible component", [SAFEPATH_NOENT] = "path contains nonexistent component", [SAFEPATH_NOTDIR] = "path contains non-directory component", [SAFEPATH_INVAL] = "path is syntactically invalid", [SAFEPATH_NOMEM] = "out of memory", [SAFEPATH_LOOP] = "too many symlink resolutions", [SAFEPATH_TOOLONG] = "path component or symlink target too long" }; const char *ret = "SAFEPATH_BAD_ERROR_CODE"; if (err >= 0 && err <= (int) (sizeof str / sizeof str[0]) && str[err] != 0) { ret = str[err]; } 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; }