From efa7a97bf01444f8bfdf25f488932912d3710974 Mon Sep 17 00:00:00 2001 From: mdw Date: Tue, 6 Apr 1999 20:12:06 +0000 Subject: [PATCH 1/1] Initial revision --- .cvsignore | 5 + .links | 4 + .skelrc | 8 + Makefile.am | 47 ++++++ Makefile.main | 82 ++++++++++ checkpath.c | 517 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ checkpath.h | 112 +++++++++++++ chkpath.1 | 110 +++++++++++++ chkpath.c | 217 ++++++++++++++++++++++++ configure.in | 42 +++++ path.c | 517 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ path.h | 112 +++++++++++++ tmpdir.1 | 85 ++++++++++ tmpdir.c | 392 ++++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 2250 insertions(+) create mode 100644 .cvsignore create mode 100644 .links create mode 100644 .skelrc create mode 100644 Makefile.am create mode 100644 Makefile.main create mode 100644 checkpath.c create mode 100644 checkpath.h create mode 100644 chkpath.1 create mode 100644 chkpath.c create mode 100644 configure.in create mode 100644 path.c create mode 100644 path.h create mode 100644 tmpdir.1 create mode 100644 tmpdir.c diff --git a/.cvsignore b/.cvsignore new file mode 100644 index 0000000..da4f443 --- /dev/null +++ b/.cvsignore @@ -0,0 +1,5 @@ +Makefile +build +Makefile.in +configure +aclocal.m4 diff --git a/.links b/.links new file mode 100644 index 0000000..bf86bc5 --- /dev/null +++ b/.links @@ -0,0 +1,4 @@ +COPYING +missing +mkinstalldirs +install-sh diff --git a/.skelrc b/.skelrc new file mode 100644 index 0000000..71fd9df --- /dev/null +++ b/.skelrc @@ -0,0 +1,8 @@ +;;; -*-emacs-lisp-*- + +(setq skel-alist + (append + '((full-title . "chkpath") + (program . "chkpath") + (author . "Mark Wooding")) + skel-alist)) diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..869726e --- /dev/null +++ b/Makefile.am @@ -0,0 +1,47 @@ +## -*-makefile-*- +## +## $Id: Makefile.am,v 1.1 1999/04/06 20:12:07 mdw Exp $ +## +## Makefile for chkpath +## +## (c) 1999 Mark Wooding +## + +##----- Licensing notice ---------------------------------------------------- +## +## This file is part of chkpath. +## +## chkpath is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## chkpath is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with chkpath; if not, write to the Free Software Foundation, +## Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +##----- Revision history ---------------------------------------------------- +## +## $Log: Makefile.am,v $ +## Revision 1.1 1999/04/06 20:12:07 mdw +## Initial revision +## + +AUTOMAKE_OPTIONS = foreign + +SUBDIRS = mLib + +bin_PROGRAMS = chkpath tmpdir +man_MANS = chkpath.1 tmpdir.1 +INCLUDES = -I$(srcdir)/mLib +LDADD = mLib/libmLib.a + +chkpath_SOURCES = chkpath.c path.c +tmpdir_SOURCES = tmpdir.c path.c + +##----- That's all, folks --------------------------------------------------- diff --git a/Makefile.main b/Makefile.main new file mode 100644 index 0000000..dc30435 --- /dev/null +++ b/Makefile.main @@ -0,0 +1,82 @@ +# Kludgy makefile + +# --- Fiddle with these if you like --- + +srcdir = @srcdir@ +prefix = /usr/local +exec_prefix = $(prefix) +bindir = $(exec_prefix)/bin +mandir = $(prefix)/man + +VPATH = @srcdir@ + +CC = gcc +LD = gcc +PACKAGE = chkpath +VERSION = 1.0.0 +CFLAGS = -O2 -g -DVERSION="\"$(VERSION)\"" -pedantic -Wall +LIBS = -lmLib +INSTALL = install +INSTALL_BINARY = $(INSTALL) -c -s +INSTALL_MAN = $(INSTALL) -c -m 644 + +# --- Don't fiddle any more --- + +PROGRAMS = chkpath tmpdir +MANPAGES = chkpath.1 tmpdir.1 +SOURCES = chkpath.c path.c tmpdir.c +DIST = Makefile Makefile.main $(SOURCES) $(MANPAGES) + +all: Makefile $(PROGRAMS) + +Makefile: $(srcdir)/Makefile.main + sed -e 's/@''srcdir@/$(srcdir)/' \ + $(srcdir)/Makefile.main >Makefile + +CHKPATH_OBJ = chkpath.o path.o +chkpath: $(CHKPATH_OBJ) + $(LD) $(CHKPATH_OBJ) $(LIBS) -o chkpath + +TMPDIR_OBJ = tmpdir.o path.o +tmpdir: $(TMPDIR_OBJ) + $(LD) $(TMPDIR_OBJ) $(LIBS) -o tmpdir + +install: $(PROGRAMS) + @for i in $(PROGRAMS); do \ + echo " $(INSTALL_BINARY) $$i $(bindir)/$$i"; \ + $(INSTALL_BINARY) $$i $(bindir)/$$i; \ + done + @for i in $(MANPAGES); do \ + mansec=`echo $$i | sed -e 's/^.*\.//'`; \ + echo " $(INSTALL_MAN) $(srcdir)/$$i $(mandir)/man$$mansec/$$i"; \ + $(INSTALL_MAN) $(srcdir)/$$i $(mandir)/man$$mansec/$$i; \ + done + +uninstall: + for i in $(PROGRAMS); do rm -f $(bindir)/$$i; done + for i in $(MANPAGES); do \ + mansec=`echo $$i | sed -e 's/^.*\.//'`; \ + rm -f $(mandir)/man$$mansec/$$i; \ + done + +arch: + mkdir $(ARCH) || true + cd $(ARCH); make -f ../Makefile.main Makefile srcdir=.. MAKEFLAGS= + +clean: + rm -f *.o *~ chkpath tmpdir + +distdir = $(PACKAGE)-$(VERSION) +distdir: + mkdir $(distdir) + for i in $(DIST); do \ + if [ -f $$i ]; then \ + ln $$i $(distdir)/$$i; \ + else \ + ln $(srcdir)/$$i $(distdir)/$$i; \ + fi; \ + done + +dist: distdir + GZIP=-9 tar chozf $(PACKAGE)-$(VERSION).tar.gz $(distdir) + rm -rf $(distdir) diff --git a/checkpath.c b/checkpath.c new file mode 100644 index 0000000..74eb243 --- /dev/null +++ b/checkpath.c @@ -0,0 +1,517 @@ +/* -*-c-*- + * + * $Id: checkpath.c,v 1.1 1999/04/06 20:12:07 mdw Exp $ + * + * Check a path for safety + * + * (c) 1999 Mark Wooding + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of chkpath. + * + * chkpath is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * chkpath is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with chkpath; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/*----- Revision history --------------------------------------------------* + * + * $Log: checkpath.c,v $ + * Revision 1.1 1999/04/06 20:12:07 mdw + * Initial revision + * + */ + +/*----- Header files ------------------------------------------------------*/ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#include "path.h" + +/*----- Data structures ---------------------------------------------------*/ + +/* --- An item in the directory list --- * + * + * Each directory becomes an element on a list which is manipulated in a + * stack-like way. + */ + +struct elt { + struct elt *e_link; /* Pointer to the next one along */ + size_t e_offset; /* Offset of name in path string */ + unsigned e_flags; /* Various useful flags */ + char e_name[1]; /* Name of the directory */ +}; + +enum { + f_sticky = 1 /* Directory has sticky bit set */ +}; + +enum { + f_last = 1 /* This is the final item to check */ +}; + +/*----- Static variables --------------------------------------------------*/ + +static struct elt rootnode = { 0, 0, 0 }; /* Root of the list */ +static struct elt *sp; /* Stack pointer for list */ +static dstr d; /* Current path string */ + +/*----- Main code ---------------------------------------------------------*/ + +/* --- @splitpath@ --- * + * + * Arguments: @const char *path@ = path string to break apart + * @struct elt *tail@ = tail block to attach to end of list + * + * Returns: Pointer to the new list head. + * + * Use: Breaks a path string into directories and adds each one + * as a node on the list, in the right order. These can then + * be pushed onto the directory stack as required. + */ + +static struct elt *splitpath(const char *path, struct elt *tail) +{ + struct elt *head, **ee = &head, *e; + + while (*path) { + size_t n; + + /* --- Either a leading `/', or a doubled one --- * + * + * Either way, ignore it. + */ + + if (*path == '/') { + path++; + continue; + } + + /* --- Skip to the next directory separator --- * + * + * Build a list element for it, and link it on. + */ + + n = strcspn(path, "/"); + e = xmalloc(sizeof(struct elt) + n + 1); + memcpy(e->e_name, path, n); + e->e_name[n] = 0; + e->e_flags = 0; + *ee = e; + ee = &e->e_link; + path += n; + } + + /* --- Done --- */ + + *ee = tail; + return (head); +} + +/* --- @pop@ --- * + * + * Arguments: --- + * + * Returns: --- + * + * Use: Removes the top item from the directory stack. + */ + +static void pop(void) +{ + if (sp->e_link) { + struct elt *e = sp->e_link; + d.len = sp->e_offset; + DPUTZ(&d); + sp = e; + } +} + +/* --- @popall@ --- * + * + * Arguments: --- + * + * Returns: --- + * + * Use: Removes all the items from the directory stack. + */ + +static void popall(void) +{ + while (sp->e_link) + pop(); +} + +/* --- @push@ --- * + * + * Arguments: @struct elt *e@ = pointer to directory element + * + * Returns: --- + * + * Use: Pushes a new subdirectory onto the stack. + */ + +static void push(struct elt *e) +{ + e->e_link = sp; + e->e_offset = d.len; + DPUTC(&d, '/'); + DPUTS(&d, e->e_name); + sp = e; +} + +/* --- @report@ --- * + * + * Arguments: @struct chkpath *cp@ = pointer to context + * @int what@ = what sort of report is this? + * @int verbose@ = how verbose is this? + * @const char *p@ = what path does it refer to? + * @const char *msg@ = the message to give to the user + * + * Returns: --- + * + * Use: Formats and presents messages to the client. + */ + +static void report(struct chkpath *cp, int what, int verbose, + const char *p, const char *msg, ...) +{ + /* --- Decide whether to bin this message --- */ + + if (!cp->cp_report || verbose > cp->cp_verbose || !(cp->cp_what & what)) + return; + + /* --- Format the message nicely --- */ + + if (cp->cp_what & CP_REPORT) { + dstr d; + va_list ap; + const char *q = msg; + size_t n; + int e = errno; + + dstr_create(&d); + va_start(ap, msg); + if (verbose > 1) + dstr_puts(&d, "[ "); + if (p) + dstr_putf(&d, "Path: %s: ", p); + while (*q) { + if (*q == '%') { + q++; + switch (*q) { + case 'e': + dstr_puts(&d, strerror(e)); + break; + case 'u': { + uid_t u = (uid_t)va_arg(ap, int); + struct passwd *pw = getpwuid(u); + if (pw) + dstr_putf(&d, "`%s'", pw->pw_name); + else + dstr_putf(&d, "%i", (int)u); + } break; + case 'g': { + gid_t g = (gid_t)va_arg(ap, int); + struct group *gr = getgrgid(g); + if (gr) + dstr_putf(&d, "`%s'", gr->gr_name); + else + dstr_putf(&d, "%i", (int)g); + } break; + case 's': { + const char *s = va_arg(ap, const char *); + dstr_puts(&d, s); + } break; + case '%': + dstr_putc(&d, '%'); + break; + default: + dstr_putc(&d, '%'); + dstr_putc(&d, *q); + break; + } + q++; + } else { + n = strcspn(q, "%"); + DPUTM(&d, q, n); + q += n; + } + } + if (verbose > 1) + dstr_puts(&d, " ]"); + DPUTZ(&d); + cp->cp_report(what, verbose, p, d.buf, cp->cp_arg); + dstr_destroy(&d); + va_end(ap); + } else + cp->cp_report(what, verbose, p, 0, cp->cp_arg); +} + +/* --- @sanity@ --- * + * + * Arguments: @const char *p@ = name of directory to check + * @struct stat *st@ = pointer to @stat@(2) block for it + * @struct chkpath *cp@ = pointer to caller parameters + * @unsigned f@ = various flags + * + * Returns: Zero if everything's OK, else bitmask of problems. + * + * Use: Performs the main load of sanity-checking on a directory. + */ + +static int sanity(const char *p, struct stat *st, + struct chkpath *cp, unsigned f) +{ + int bad = 0; + int sticky = (cp->cp_what & CP_STICKYOK) || !(f & f_last) ? 01000 : 0; + + /* --- Check for world-writability --- */ + + if ((cp->cp_what & CP_WRWORLD) && + (st->st_mode & (0002 | sticky)) == 0002) { + bad |= CP_WRWORLD; + report(cp, CP_WRWORLD, 1, p, "** world writable **"); + } + + /* --- Check for group-writability --- */ + + if ((cp->cp_what & (CP_WRGRP | CP_WROTHGRP)) && + (st->st_mode & (0020 | sticky)) == 0020) { + if (cp->cp_what & CP_WRGRP) { + bad |= CP_WRGRP; + report(cp, CP_WRGRP, 1, p, "writable by group %g", st->st_gid); + } else { + int i; + for (i = 0; i < cp->cp_gids; i++) { + if (st->st_gid == cp->cp_gid[i]) + goto good_gid; + } + bad |= CP_WROTHGRP; + report(cp, CP_WROTHGRP, 1, p, "writable by group %g", st->st_gid); + good_gid:; + } + } + + /* --- Check for user-writability --- */ + + if ((cp->cp_what & CP_WROTHUSR) && + st->st_uid != cp->cp_uid && + st->st_uid != 0) { + bad |= CP_WROTHUSR; + report(cp, CP_WROTHUSR, 1, p, "owner is user %u", st->st_uid); + } + + /* --- Done sanity check --- */ + + return (bad); +} + +/* --- @path_check@ --- * + * + * Arguments: @const char *p@ = directory name which needs checking + * @struct chkpath *cp@ = caller parameters for the check + * + * Returns: Zero if all is well, otherwise bitmask of problems. + * + * Use: Scrutinises a directory path to see what evil things other + * users could do to it. + */ + +int path_check(const char *p, struct chkpath *cp) +{ + char cwd[PATH_MAX]; + struct elt *e, *ee; + struct stat st; + int bad = 0; + + /* --- Initialise stack pointer and path string --- */ + + sp = &rootnode; + dstr_destroy(&d); + + /* --- Try to find the current directory --- */ + + if (!getcwd(cwd, sizeof(cwd))) { + report(cp, CP_ERROR, 0, 0, "can't find current directory: %e"); + return (CP_ERROR); + } + + /* --- Check that the root directory is OK --- */ + + if (stat("/", &st)) { + report(cp, CP_ERROR, 0, 0, "can't stat root directory: %e"); + return (CP_ERROR); + } + + report(cp, CP_REPORT, 3, p, "begin scan"); + bad |= sanity("/", &st, cp, 0); + + /* --- Get the initial list of things to process --- */ + + ee = splitpath(p, 0); + if (*p != '/') + ee = splitpath(cwd, ee); + + /* --- While there are list items which still need doing --- */ + + while (ee) { + e = ee->e_link; + + /* --- Strip off simple `.' elements --- */ + + if (strcmp(ee->e_name, ".") == 0) { + free(ee); + ee = e; + continue; + } + + /* --- Backtrack on `..' elements --- */ + + else if (strcmp(ee->e_name, "..") == 0) { + pop(); + free(ee); + ee = e; + continue; + } + + /* --- Everything else gets pushed on the end --- */ + + push(ee); + ee = e; + + /* --- Find out what sort of a thing this is --- */ + + if (lstat(d.buf, &st)) { + report(cp, CP_ERROR, 0, d.buf, "can't stat: %e"); + bad |= CP_ERROR; + break; + } + + /* --- Handle symbolic links specially --- */ + + if (S_ISLNK(st.st_mode)) { + char buf[PATH_MAX]; + int i; + + /* --- Resolve the link --- */ + + if ((i = readlink(d.buf, buf, sizeof(buf))) < 0) { + report(cp, CP_ERROR, 0, d.buf, "can't readlink: %e"); + bad |= CP_ERROR; + break; + } + buf[i] = 0; + report(cp, CP_SYMLINK, 2, d.buf, "symlink -> `%s'", buf); + + /* --- Handle sticky parents --- * + * + * If I make a symlink in a sticky directory, I can later modify it. + * However, nobody else can (except the owner of the directory, and + * we'll already have noticed that if we care). + */ + + if ((cp->cp_what & CP_WROTHUSR) && + (sp->e_link->e_flags & f_sticky) && + st.st_uid != cp->cp_uid && st.st_uid != 0) { + bad |= CP_WROTHUSR; + report(cp, CP_WROTHUSR, 1, d.buf, + "symlink modifiable by user %u", st.st_uid); + } + + /* --- Sort out what to do from here --- */ + + if (buf[0] == '/') + popall(); + else + pop(); + ee = splitpath(buf, ee); + continue; + } + + /* --- Run the sanity check on this path element --- */ + + bad |= sanity(d.buf, &st, cp, ee ? 0 : f_last); + + if (S_ISDIR(st.st_mode)) { + if (st.st_mode & 01000) + sp->e_flags |= f_sticky; + report(cp, CP_REPORT, 4, d.buf, "directory"); + continue; + } + + /* --- Something else I don't understand --- */ + + break; + } + + /* --- Check for leftover junk --- */ + + if (ee) { + if (!(bad & CP_ERROR)) + report(cp, CP_ERROR, 0, 0, "junk left over after reaching leaf"); + while (ee) { + e = ee->e_link; + free(ee); + ee = e; + } + } + + popall(); + return (bad); +} + +/* --- @path_setids@ --- * + * + * Arguments: @struct chkpath *cp@ = pointer to block to fill in + * + * Returns: Zero if OK, else @-1@. + * + * Use: Fills in the user ids and things in the structure. + */ + +void path_setids(struct chkpath *cp) +{ + int n, i; + gid_t g = getgid(); + + cp->cp_uid = getuid(); + n = getgroups(sizeof(cp->cp_gid) / sizeof(cp->cp_gid[0]), cp->cp_gid); + + for (i = 0; i < n; i++) { + if (cp->cp_gid[i] == g) + goto gid_ok; + } + cp->cp_gid[n++] = g; +gid_ok: + cp->cp_gids = n; +} + +/*----- That's all, folks -------------------------------------------------*/ diff --git a/checkpath.h b/checkpath.h new file mode 100644 index 0000000..e759c07 --- /dev/null +++ b/checkpath.h @@ -0,0 +1,112 @@ +/* -*-c-*- + * + * $Id: checkpath.h,v 1.1 1999/04/06 20:12:07 mdw Exp $ + * + * Check a path for safety + * + * (c) 1999 Mark Wooding + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of chkpath. + * + * chkpath is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * chkpath is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with chkpath; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/*----- Revision history --------------------------------------------------* + * + * $Log: checkpath.h,v $ + * Revision 1.1 1999/04/06 20:12:07 mdw + * Initial revision + * + */ + +#ifndef PATH_H +#define PATH_H + +#ifdef __cplusplus + extern "C" { +#endif + +/*----- Header files ------------------------------------------------------*/ + +#include +#include + +/*----- Data structures ---------------------------------------------------*/ + +/* --- Search request --- * + * + * This contains parameters from the caller to control what problems are + * looked for, and what to do when they're found. + */ + +struct chkpath { + uid_t cp_uid; /* Uid that's considered OK */ + gid_t cp_gid[NGROUPS_MAX + 1]; /* Array of groups that are OK */ + int cp_gids; /* Number of groups in the array */ + int cp_verbose; /* Verbosity level to spit up */ + int cp_what; /* What things to check for */ + void (*cp_report)(int /*what*/, int /*verb*/, + const char */*dir*/, const char */*msg*/, + void */*p*/); + void *cp_arg; /* Argument for cp_report */ +}; + +/* --- Flags for `@what@' fields in the above --- */ + +#define CP_ERROR 1 /* Error report */ +#define CP_WRWORLD 2 /* Check write by world */ +#define CP_WRGRP 4 /* Check write by any group */ +#define CP_WROTHGRP 8 /* Check write by other group */ +#define CP_WROTHUSR 16 /* Check write by other user */ +#define CP_SYMLINK 32 /* Report symbolic links */ +#define CP_REPORT 64 /* Make user-readable reports */ +#define CP_STICKYOK 128 /* Don't care if sticky is set */ + +/*----- Functions provided ------------------------------------------------*/ + +/* --- @path_check@ --- * + * + * Arguments: @const char *p@ = directory name which needs checking + * @struct chkpath *cp@ = caller parameters for the check + * + * Returns: Zero if all is well, otherwise bitmask of problems. + * + * Use: Scrutinises a directory path to see what evil things other + * users could do to it. + */ + +extern int path_check(const char */*p*/, struct chkpath */*cp*/); + +/* --- @path_setids@ --- * + * + * Arguments: @struct chkpath *cp@ = pointer to block to fill in + * + * Returns: --- + * + * Use: Fills in the user ids and things in the structure. + */ + +extern void path_setids(struct chkpath */*cp*/); + +/*----- That's all, folks -------------------------------------------------*/ + +#ifdef __cplusplus + } +#endif + +#endif diff --git a/chkpath.1 b/chkpath.1 new file mode 100644 index 0000000..5742f50 --- /dev/null +++ b/chkpath.1 @@ -0,0 +1,110 @@ +.TH chkpath 1 "6 April 1999" "Local tools" +.SH NAME +chkpath \- check a path string for security +.SH SYNOPSIS +.B chkpath +.RB [ \-vqstp ] +.RI [ path ...] +.SH USAGE +The +.B chkpath +command checks one or more path strings (i.e., lists of directories +separated by colons) for security. If no path strings are given, the +value of the +.B PATH +environment variable is examined. +.PP +Each directory in turn is broken into its consitituent parts and every +step which must be made through the filesystem to reach that directory +from the root is scrutinized for vulnerabilities. The checks made +against each directory and symbolic link along the way are as follows: +.IP 1. +No step should be a directory which is world-writable unless its sticky +bit is set, and it's not the final step. +.IP 2. +No step should be a directory which is group-writable unless its sticky +bit is set, and it's not the final step. (However, see the +.B \-t +option below.) +.IP 3. +No step should be a directory owned by another user (other than root). +.IP 4. +No step should be a symbolic link inside a sticky directory and owned by +another user. +.PP +The author is not aware of any weaknesses in this ruleset. The +objective is that nobody other than the user and the superuser should be +able to add or change the set of files available within the directories +of the path(s). +.SS OPTIONS +The following command line options are available: +.TP +.B "\-h, \-\-help" +Displays a relatively verbose message describing how to use +.BR chkpath . +.TP +.B "\-V, \-\-version" +Displays +.BR chkpath 's +version number. +.TP +.B "\-u, \-\-usage" +Displays a very terse usage summary. +.TP +.B "\-v, \-\-verbose" +Makes +.B chkpath +more verbose about what it's doing. This option has a cumulative +effect, so put more in for more verbosity. Note that verbose doesn't +mean the same as interesting. The default is to report problems with +directories and system errors. +.TP +.B "\-q, \-\-quiet" +Makes +.B chkpath +less verbose about what it's doing. This option, like +.BR \-v , +has a cumulative effect. Each +.B \-q +cancels out a +.B \-v +option. +.TP +.B "\-s, \-\-sticky" +Modifies the ruleset slightly so that any step through the filesystem is +OK, even if world- or group-writable (but not owned by someone else), as +long as the directory's sticky bit is set. The default is that sticky +directories are considered safe only if they're not the final step. +Turning this option on isn't recommended: if you use a sticky directory +in your path then other people can add malicious commands whose names +are common typos of standard ones. +.TP +.B "\-t, \-\-trust\-group" +Modifies the ruleset slightly so that +.B chkpath +doesn't warn about directories group-owned by groups you're a member +of. In other words, it trusts your fellow group-members +.IR "in their capacity as group-owners only" . +.B chkpath +will still warn about directories owned by people in your groups. +.TP +.B "\-p, \-\-print" +Writes on standard output a colon-separated list of the directories +which +.B chkpath +considered `safe'. This can be used to filter out unsafe directories in +an automatic way: +.RS 10 +.nf +.ft B +.sp 1 +PATH=`chkpath -qqp` +.ft R +.fi +.RE +.SH BUGS +None known. +.SH SEE ALSO +.BR tmpdir (1). +.SH AUTHOR +Mark Wooding (mdw@nsict.org). diff --git a/chkpath.c b/chkpath.c new file mode 100644 index 0000000..e90b0d7 --- /dev/null +++ b/chkpath.c @@ -0,0 +1,217 @@ +/* -*-c-*- + * + * $Id: chkpath.c,v 1.1 1999/04/06 20:12:07 mdw Exp $ + * + * Check a user's file search path + * + * (c) 1999 Mark Wooding + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of chkpath. + * + * chkpath is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * chkpath is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with chkpath; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/*----- Revision history --------------------------------------------------* + * + * $Log: chkpath.c,v $ + * Revision 1.1 1999/04/06 20:12:07 mdw + * Initial revision + * + */ + +/*----- Header files ------------------------------------------------------*/ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "path.h" + +/*----- Main code ---------------------------------------------------------*/ + +static void report(int what, int verbose, + const char *p, const char *msg, + void *arg) +{ + moan("%s", msg); +} + +/* --- @usage@ --- */ + +static void usage(FILE *fp) +{ + fprintf(fp, "Usage: %s [-vqstp] [PATH...]\n", QUIS); +} + +/* --- @version@ --- */ + +static void version(FILE *fp) +{ + fprintf(fp, "%s version %s\n", QUIS, VERSION); +} + +/* --- @help@ --- */ + +static void help(FILE *fp) +{ + version(fp); + putc('\n', fp); + usage(fp); + fputs("\n\ +Checks a path string (by default the PATH variable) for security. It\n\ +ensures that only `root' or the calling user can write to all the parent\n\ +directories of the path elements, so nobody can maliciously replace the\n\ +binaries unexpectedly.\n\ +\n\ +Options provided are:\n\ +\n\ +-h, --help Display this help message.\n\ +-V, --version Display the program's version number.\n\ +-u, --usage Show a terse usage summary.\n\ +\n\ +-v, --verbose Be verbose about the search progress (cumulative).\n\ +-q, --quiet Be quiet about the search progress (cumulative).\n\ +-s, --sticky Consider sticky directories secure against\n\ + modification by world and group (not recommended).\n\ +-t, --trust-group Consider other members of your group trustworthy.\n\ +-p, --print Write the secure path elements to standard output.\n\ +", + fp); +} + +int main(int argc, char *argv[]) +{ + int bad = 0; + int i; + char *p, *q, *path; + struct chkpath cp; + int f = 0; + + enum { + f_print = 1, + f_colon = 2 + }; + + /* --- Initialize the world --- */ + + ego(argv[0]); + + /* --- Set up path scanning defaults --- */ + + cp.cp_verbose = 1; + cp.cp_what = (CP_WRWORLD | CP_WRGRP | CP_WROTHUSR | + CP_ERROR | CP_REPORT | CP_SYMLINK); + cp.cp_report = report; + cp.cp_arg = 0; + path_setids(&cp); + + /* --- Parse the options --- */ + + for (;;) { + static struct option opts[] = { + { "help", 0, 0, 'h' }, + { "version", 0, 0, 'V' }, + { "usage", 0, 0, 'u' }, + { "verbose", 0, 0, 'v' }, + { "quiet", 0, 0, 'q' }, + { "sticky", 0, 0, 's' }, + { "trust-group", 0, 0, 't' }, + { "print", 0, 0, 'p' }, + { 0, 0, 0, 0 } + }; + int i = mdwopt(argc, argv, "hVu vqstp", opts, 0, 0, 0); + + if (i < 0) + break; + switch (i) { + case 'h': + help(stdout); + exit(0); + case 'V': + version(stdout); + exit(0); + case 'u': + usage(stdout); + exit(0); + case 'v': + cp.cp_verbose++; + break; + case 'q': + if (cp.cp_verbose) + cp.cp_verbose--; + break; + case 's': + cp.cp_what |= CP_STICKYOK; + break; + case 't': + cp.cp_what = (cp.cp_what & ~CP_WRGRP) | CP_WROTHGRP; + break; + case 'p': + f |= f_print; + break; + default: + bad = 1; + break; + } + } + + if (bad) { + usage(stderr); + exit(1); + } + + /* --- Sort out what needs doing --- */ + + if (optind == argc) { + path = getenv("PATH"); + argv = &path; + argc = 1; + optind = 0; + } + + for (i = optind; i < argc; i++) { + p = xstrdup(argv[i]); + q = strtok(p, ":"); + while (q) { + int b = path_check(q, &cp); + if (!b && (f & f_print)) { + if (f & f_colon) + putchar(':'); + fputs(q, stdout); + f |= f_colon; + } + bad |= b; + q = strtok(0, ":"); + } + free(p); + } + + if (f & f_colon) + putchar('\n'); + + return (bad); +} + +/*----- That's all, folks -------------------------------------------------*/ diff --git a/configure.in b/configure.in new file mode 100644 index 0000000..57e4ee8 --- /dev/null +++ b/configure.in @@ -0,0 +1,42 @@ +dnl -*-fundamental-*- +dnl +dnl $Id: configure.in,v 1.1 1999/04/06 20:12:08 mdw Exp $ +dnl +dnl Configurator for chkpath +dnl +dnl (c) 1999 Mark Wooding +dnl + +dnl ----- Licensing notice -------------------------------------------------- +dnl +dnl This file is part of chkpath. +dnl +dnl chkpath is free software; you can redistribute it and/or modify +dnl it under the terms of the GNU General Public License as published by +dnl the Free Software Foundation; either version 2 of the License, or +dnl (at your option) any later version. +dnl +dnl chkpath is distributed in the hope that it will be useful, +dnl but WITHOUT ANY WARRANTY; without even the implied warranty of +dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +dnl GNU General Public License for more details. +dnl +dnl You should have received a copy of the GNU General Public License +dnl along with chkpath; if not, write to the Free Software Foundation, +dnl Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +dnl ----- Revision history -------------------------------------------------- +dnl +dnl $Log: configure.in,v $ +dnl Revision 1.1 1999/04/06 20:12:08 mdw +dnl Initial revision +dnl + +AC_INIT(chkpath.c) +AM_INIT_AUTOMAKE(chkpath, 1.0.0) +AC_PROG_CC +mdw_GCC_FLAGS +AC_CONFIG_SUBDIRS(mLib) +AC_OUTPUT(Makefile) + +dnl ----- That's all, folks ------------------------------------------------- diff --git a/path.c b/path.c new file mode 100644 index 0000000..fd6271b --- /dev/null +++ b/path.c @@ -0,0 +1,517 @@ +/* -*-c-*- + * + * $Id: path.c,v 1.1 1999/04/06 20:12:07 mdw Exp $ + * + * Check a path for safety + * + * (c) 1999 Mark Wooding + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of chkpath. + * + * chkpath is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * chkpath is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with chkpath; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/*----- Revision history --------------------------------------------------* + * + * $Log: path.c,v $ + * Revision 1.1 1999/04/06 20:12:07 mdw + * Initial revision + * + */ + +/*----- Header files ------------------------------------------------------*/ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#include "path.h" + +/*----- Data structures ---------------------------------------------------*/ + +/* --- An item in the directory list --- * + * + * Each directory becomes an element on a list which is manipulated in a + * stack-like way. + */ + +struct elt { + struct elt *e_link; /* Pointer to the next one along */ + size_t e_offset; /* Offset of name in path string */ + unsigned e_flags; /* Various useful flags */ + char e_name[1]; /* Name of the directory */ +}; + +enum { + f_sticky = 1 /* Directory has sticky bit set */ +}; + +enum { + f_last = 1 /* This is the final item to check */ +}; + +/*----- Static variables --------------------------------------------------*/ + +static struct elt rootnode = { 0, 0, 0 }; /* Root of the list */ +static struct elt *sp; /* Stack pointer for list */ +static dstr d; /* Current path string */ + +/*----- Main code ---------------------------------------------------------*/ + +/* --- @splitpath@ --- * + * + * Arguments: @const char *path@ = path string to break apart + * @struct elt *tail@ = tail block to attach to end of list + * + * Returns: Pointer to the new list head. + * + * Use: Breaks a path string into directories and adds each one + * as a node on the list, in the right order. These can then + * be pushed onto the directory stack as required. + */ + +static struct elt *splitpath(const char *path, struct elt *tail) +{ + struct elt *head, **ee = &head, *e; + + while (*path) { + size_t n; + + /* --- Either a leading `/', or a doubled one --- * + * + * Either way, ignore it. + */ + + if (*path == '/') { + path++; + continue; + } + + /* --- Skip to the next directory separator --- * + * + * Build a list element for it, and link it on. + */ + + n = strcspn(path, "/"); + e = xmalloc(sizeof(struct elt) + n + 1); + memcpy(e->e_name, path, n); + e->e_name[n] = 0; + e->e_flags = 0; + *ee = e; + ee = &e->e_link; + path += n; + } + + /* --- Done --- */ + + *ee = tail; + return (head); +} + +/* --- @pop@ --- * + * + * Arguments: --- + * + * Returns: --- + * + * Use: Removes the top item from the directory stack. + */ + +static void pop(void) +{ + if (sp->e_link) { + struct elt *e = sp->e_link; + d.len = sp->e_offset; + DPUTZ(&d); + sp = e; + } +} + +/* --- @popall@ --- * + * + * Arguments: --- + * + * Returns: --- + * + * Use: Removes all the items from the directory stack. + */ + +static void popall(void) +{ + while (sp->e_link) + pop(); +} + +/* --- @push@ --- * + * + * Arguments: @struct elt *e@ = pointer to directory element + * + * Returns: --- + * + * Use: Pushes a new subdirectory onto the stack. + */ + +static void push(struct elt *e) +{ + e->e_link = sp; + e->e_offset = d.len; + DPUTC(&d, '/'); + DPUTS(&d, e->e_name); + sp = e; +} + +/* --- @report@ --- * + * + * Arguments: @struct chkpath *cp@ = pointer to context + * @int what@ = what sort of report is this? + * @int verbose@ = how verbose is this? + * @const char *p@ = what path does it refer to? + * @const char *msg@ = the message to give to the user + * + * Returns: --- + * + * Use: Formats and presents messages to the client. + */ + +static void report(struct chkpath *cp, int what, int verbose, + const char *p, const char *msg, ...) +{ + /* --- Decide whether to bin this message --- */ + + if (!cp->cp_report || verbose > cp->cp_verbose || !(cp->cp_what & what)) + return; + + /* --- Format the message nicely --- */ + + if (cp->cp_what & CP_REPORT) { + dstr d; + va_list ap; + const char *q = msg; + size_t n; + int e = errno; + + dstr_create(&d); + va_start(ap, msg); + if (verbose > 1) + dstr_puts(&d, "[ "); + if (p) + dstr_putf(&d, "Path: %s: ", p); + while (*q) { + if (*q == '%') { + q++; + switch (*q) { + case 'e': + dstr_puts(&d, strerror(e)); + break; + case 'u': { + uid_t u = (uid_t)va_arg(ap, int); + struct passwd *pw = getpwuid(u); + if (pw) + dstr_putf(&d, "`%s'", pw->pw_name); + else + dstr_putf(&d, "%i", (int)u); + } break; + case 'g': { + gid_t g = (gid_t)va_arg(ap, int); + struct group *gr = getgrgid(g); + if (gr) + dstr_putf(&d, "`%s'", gr->gr_name); + else + dstr_putf(&d, "%i", (int)g); + } break; + case 's': { + const char *s = va_arg(ap, const char *); + dstr_puts(&d, s); + } break; + case '%': + dstr_putc(&d, '%'); + break; + default: + dstr_putc(&d, '%'); + dstr_putc(&d, *q); + break; + } + q++; + } else { + n = strcspn(q, "%"); + DPUTM(&d, q, n); + q += n; + } + } + if (verbose > 1) + dstr_puts(&d, " ]"); + DPUTZ(&d); + cp->cp_report(what, verbose, p, d.buf, cp->cp_arg); + dstr_destroy(&d); + va_end(ap); + } else + cp->cp_report(what, verbose, p, 0, cp->cp_arg); +} + +/* --- @sanity@ --- * + * + * Arguments: @const char *p@ = name of directory to check + * @struct stat *st@ = pointer to @stat@(2) block for it + * @struct chkpath *cp@ = pointer to caller parameters + * @unsigned f@ = various flags + * + * Returns: Zero if everything's OK, else bitmask of problems. + * + * Use: Performs the main load of sanity-checking on a directory. + */ + +static int sanity(const char *p, struct stat *st, + struct chkpath *cp, unsigned f) +{ + int bad = 0; + int sticky = (cp->cp_what & CP_STICKYOK) || !(f & f_last) ? 01000 : 0; + + /* --- Check for world-writability --- */ + + if ((cp->cp_what & CP_WRWORLD) && + (st->st_mode & (0002 | sticky)) == 0002) { + bad |= CP_WRWORLD; + report(cp, CP_WRWORLD, 1, p, "** world writable **"); + } + + /* --- Check for group-writability --- */ + + if ((cp->cp_what & (CP_WRGRP | CP_WROTHGRP)) && + (st->st_mode & (0020 | sticky)) == 0020) { + if (cp->cp_what & CP_WRGRP) { + bad |= CP_WRGRP; + report(cp, CP_WRGRP, 1, p, "writable by group %g", st->st_gid); + } else { + int i; + for (i = 0; i < cp->cp_gids; i++) { + if (st->st_gid == cp->cp_gid[i]) + goto good_gid; + } + bad |= CP_WROTHGRP; + report(cp, CP_WROTHGRP, 1, p, "writable by group %g", st->st_gid); + good_gid:; + } + } + + /* --- Check for user-writability --- */ + + if ((cp->cp_what & CP_WROTHUSR) && + st->st_uid != cp->cp_uid && + st->st_uid != 0) { + bad |= CP_WROTHUSR; + report(cp, CP_WROTHUSR, 1, p, "owner is user %u", st->st_uid); + } + + /* --- Done sanity check --- */ + + return (bad); +} + +/* --- @path_check@ --- * + * + * Arguments: @const char *p@ = directory name which needs checking + * @struct chkpath *cp@ = caller parameters for the check + * + * Returns: Zero if all is well, otherwise bitmask of problems. + * + * Use: Scrutinises a directory path to see what evil things other + * users could do to it. + */ + +int path_check(const char *p, struct chkpath *cp) +{ + char cwd[PATH_MAX]; + struct elt *e, *ee; + struct stat st; + int bad = 0; + + /* --- Initialise stack pointer and path string --- */ + + sp = &rootnode; + dstr_destroy(&d); + + /* --- Try to find the current directory --- */ + + if (!getcwd(cwd, sizeof(cwd))) { + report(cp, CP_ERROR, 0, 0, "can't find current directory: %e"); + return (CP_ERROR); + } + + /* --- Check that the root directory is OK --- */ + + if (stat("/", &st)) { + report(cp, CP_ERROR, 0, 0, "can't stat root directory: %e"); + return (CP_ERROR); + } + + report(cp, CP_REPORT, 3, p, "begin scan"); + bad |= sanity("/", &st, cp, 0); + + /* --- Get the initial list of things to process --- */ + + ee = splitpath(p, 0); + if (*p != '/') + ee = splitpath(cwd, ee); + + /* --- While there are list items which still need doing --- */ + + while (ee) { + e = ee->e_link; + + /* --- Strip off simple `.' elements --- */ + + if (strcmp(ee->e_name, ".") == 0) { + free(ee); + ee = e; + continue; + } + + /* --- Backtrack on `..' elements --- */ + + else if (strcmp(ee->e_name, "..") == 0) { + pop(); + free(ee); + ee = e; + continue; + } + + /* --- Everything else gets pushed on the end --- */ + + push(ee); + ee = e; + + /* --- Find out what sort of a thing this is --- */ + + if (lstat(d.buf, &st)) { + report(cp, CP_ERROR, 0, d.buf, "can't stat: %e"); + bad |= CP_ERROR; + break; + } + + /* --- Handle symbolic links specially --- */ + + if (S_ISLNK(st.st_mode)) { + char buf[PATH_MAX]; + int i; + + /* --- Resolve the link --- */ + + if ((i = readlink(d.buf, buf, sizeof(buf))) < 0) { + report(cp, CP_ERROR, 0, d.buf, "can't readlink: %e"); + bad |= CP_ERROR; + break; + } + buf[i] = 0; + report(cp, CP_SYMLINK, 2, d.buf, "symlink -> `%s'", buf); + + /* --- Handle sticky parents --- * + * + * If I make a symlink in a sticky directory, I can later modify it. + * However, nobody else can (except the owner of the directory, and + * we'll already have noticed that if we care). + */ + + if ((cp->cp_what & CP_WROTHUSR) && + (sp->e_link->e_flags & f_sticky) && + st.st_uid != cp->cp_uid && st.st_uid != 0) { + bad |= CP_WROTHUSR; + report(cp, CP_WROTHUSR, 1, d.buf, + "symlink modifiable by user %u", st.st_uid); + } + + /* --- Sort out what to do from here --- */ + + if (buf[0] == '/') + popall(); + else + pop(); + ee = splitpath(buf, ee); + continue; + } + + /* --- Run the sanity check on this path element --- */ + + bad |= sanity(d.buf, &st, cp, ee ? 0 : f_last); + + if (S_ISDIR(st.st_mode)) { + if (st.st_mode & 01000) + sp->e_flags |= f_sticky; + report(cp, CP_REPORT, 4, d.buf, "directory"); + continue; + } + + /* --- Something else I don't understand --- */ + + break; + } + + /* --- Check for leftover junk --- */ + + if (ee) { + if (!(bad & CP_ERROR)) + report(cp, CP_ERROR, 0, 0, "junk left over after reaching leaf"); + while (ee) { + e = ee->e_link; + free(ee); + ee = e; + } + } + + popall(); + return (bad); +} + +/* --- @path_setids@ --- * + * + * Arguments: @struct chkpath *cp@ = pointer to block to fill in + * + * Returns: Zero if OK, else @-1@. + * + * Use: Fills in the user ids and things in the structure. + */ + +void path_setids(struct chkpath *cp) +{ + int n, i; + gid_t g = getgid(); + + cp->cp_uid = getuid(); + n = getgroups(sizeof(cp->cp_gid) / sizeof(cp->cp_gid[0]), cp->cp_gid); + + for (i = 0; i < n; i++) { + if (cp->cp_gid[i] == g) + goto gid_ok; + } + cp->cp_gid[n++] = g; +gid_ok: + cp->cp_gids = n; +} + +/*----- That's all, folks -------------------------------------------------*/ diff --git a/path.h b/path.h new file mode 100644 index 0000000..51ff2d9 --- /dev/null +++ b/path.h @@ -0,0 +1,112 @@ +/* -*-c-*- + * + * $Id: path.h,v 1.1 1999/04/06 20:12:07 mdw Exp $ + * + * Check a path for safety + * + * (c) 1999 Mark Wooding + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of chkpath. + * + * chkpath is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * chkpath is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with chkpath; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/*----- Revision history --------------------------------------------------* + * + * $Log: path.h,v $ + * Revision 1.1 1999/04/06 20:12:07 mdw + * Initial revision + * + */ + +#ifndef PATH_H +#define PATH_H + +#ifdef __cplusplus + extern "C" { +#endif + +/*----- Header files ------------------------------------------------------*/ + +#include +#include + +/*----- Data structures ---------------------------------------------------*/ + +/* --- Search request --- * + * + * This contains parameters from the caller to control what problems are + * looked for, and what to do when they're found. + */ + +struct chkpath { + uid_t cp_uid; /* Uid that's considered OK */ + gid_t cp_gid[NGROUPS_MAX + 1]; /* Array of groups that are OK */ + int cp_gids; /* Number of groups in the array */ + int cp_verbose; /* Verbosity level to spit up */ + int cp_what; /* What things to check for */ + void (*cp_report)(int /*what*/, int /*verb*/, + const char */*dir*/, const char */*msg*/, + void */*p*/); + void *cp_arg; /* Argument for cp_report */ +}; + +/* --- Flags for `@what@' fields in the above --- */ + +#define CP_ERROR 1 /* Error report */ +#define CP_WRWORLD 2 /* Check write by world */ +#define CP_WRGRP 4 /* Check write by any group */ +#define CP_WROTHGRP 8 /* Check write by other group */ +#define CP_WROTHUSR 16 /* Check write by other user */ +#define CP_SYMLINK 32 /* Report symbolic links */ +#define CP_REPORT 64 /* Make user-readable reports */ +#define CP_STICKYOK 128 /* Don't care if sticky is set */ + +/*----- Functions provided ------------------------------------------------*/ + +/* --- @path_check@ --- * + * + * Arguments: @const char *p@ = directory name which needs checking + * @struct chkpath *cp@ = caller parameters for the check + * + * Returns: Zero if all is well, otherwise bitmask of problems. + * + * Use: Scrutinises a directory path to see what evil things other + * users could do to it. + */ + +extern int path_check(const char */*p*/, struct chkpath */*cp*/); + +/* --- @path_setids@ --- * + * + * Arguments: @struct chkpath *cp@ = pointer to block to fill in + * + * Returns: --- + * + * Use: Fills in the user ids and things in the structure. + */ + +extern void path_setids(struct chkpath */*cp*/); + +/*----- That's all, folks -------------------------------------------------*/ + +#ifdef __cplusplus + } +#endif + +#endif diff --git a/tmpdir.1 b/tmpdir.1 new file mode 100644 index 0000000..4b690ff --- /dev/null +++ b/tmpdir.1 @@ -0,0 +1,85 @@ +.TH tmpdir 1 "6 April 1999" "Local tools" +.SH NAME +tmpdir \- choose, or check a choice of, temporary directory +.SH SYNOPSIS +.B tmpdir +.RB [ \-bc ] +.RB [ \-v +.IR dir ] +.SH USAGE +The +.B tmpdir +program creates a secure place for temporary files to be stored, and +outputs an assignment to the +.B TMPDIR +variable suitable for execution by a shell. +.PP +Many programs aren't sufficiently careful about how they handle +temporary files. For example, if a program which creates files in +.B /tmp +without making careful checks beforehand, a malicious user who can +predict the name that the program will use can create a symbolic link +with that name: when run, the program will then overwrite some file +using your current privileges. Similarly, many programs create +temporary files using generous default permissions, which may well be a +mistake. +.PP +The +.B tmpdir +program finds a secure place for temporary files, creating one if +necessary. The criteria it uses to choose a place are as follows: +.IP 1. +The temporary directory must be owned by the user, and have mode 700 +(i.e., readable, writable and searchable only by the owner). +.IP 2. +The path through the filesystem to the temporary directory must be +secure against modifications by other malicious users. See the +.BR chkpath (1) +manual page for a description of how this is done: the two programs work +in the same way. +.PP +First, +.B tmpdir +checks to see whether the current value of the +.B TMPDIR +environment variable is a secure place for temporary files. If so, it +is accepted immediately. Otherwise, it tries to find or create a +directory in +.B /tmp +(on the assumption that this is a fast disk suitable for temporary +files), with the name +.BI /tmp/ user \- suffix +for some +.IR suffix . +If that fails, it tries to create a directory in your home directory, +with the name +.BI ~/tmp\- suffix\fR. +If +.I that +fails too, then +.B tmpdir +gives up: if your home directory's not secure (or full) than a secure +temporary directory is the least of your worries. +.SS OPTIONS +The following options are supported: +.TP +.B "\-b, \-\-bourne" +Output an assignment using Bourne shell syntax. The default is to +examine the user's shell and decide which syntax to use based on that. +.TP +.B "\-c, \-\-cshell" +Output an assignment using C shell syntax. +.TP +.BI "\-v, --verify=" dir +Don't try to find a temporary directory; just see whether +.I dir +is secure, and exit successfully if it is (and unsuccessfully if it +isn't). +.SH BUGS +None known. +.SH SEE ALSO +.BR chkpath (1), +.BR tmpnam (3), +.BR tmpfile (3). +.SH AUTHOR +Mark Wooding (mdw@nsict.org). diff --git a/tmpdir.c b/tmpdir.c new file mode 100644 index 0000000..453ebf3 --- /dev/null +++ b/tmpdir.c @@ -0,0 +1,392 @@ +/* -*-c-*- + * + * $Id: tmpdir.c,v 1.1 1999/04/06 20:12:07 mdw Exp $ + * + * Choose and check temporary directories + * + * (c) 1999 Mark Wooding + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of chkpath. + * + * chkpath is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * chkpath is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with chkpath; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/*----- Revision history --------------------------------------------------* + * + * $Log: tmpdir.c,v $ + * Revision 1.1 1999/04/06 20:12:07 mdw + * Initial revision + * + */ + +/*----- Header files ------------------------------------------------------*/ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "path.h" + +/*----- Static variables --------------------------------------------------*/ + +static uid_t me; +static struct chkpath cp; +static struct passwd *pw; + +/*----- Main code ---------------------------------------------------------*/ + +/* --- @ok@ --- * + * + * Arguments: @const char *p@ = pathname to check + * @int *f@ = try-to-create flag + * + * Returns: Nonzero if the directory is OK + * + * Use: Ensures that a directory is OK. If @f@ is a real pointer, + * and @*f@ is set, then try to create the directory. + */ + +static int ok(const char *p, int *f) +{ + struct stat st; + + /* --- Read the directory status --- */ + + if (lstat(p, &st)) { + + /* --- Maybe create it if it doesn't exist --- */ + + if (errno != ENOENT || !f || !*f) + return (0); + if (mkdir(p, 0700)) { + *f = 0; + return (0); + } + + /* --- Now read the new status --- * + * + * This fixes a race condition between the previous @lstat@ call and + * the @mkdir@. + */ + + if (lstat(p, &st)) + return (0); + } + + /* --- Make sure the directory is good --- * + * + * It must be a genuine directory (not a link); it must be readable + * and writable only by its owner, and that owner must be me. + */ + + if (S_ISDIR(st.st_mode) && (st.st_mode & 0777) == 0700 && st.st_uid == me) + return (1); + return (0); +} + +/* --- @trytmp@ --- * + * + * Arguments: @const char *parent@ = parent directory name + * @const char *base@ = subdirectory base name + * + * Returns: Pointer to directory name, or null. (String needs freeing.) + * + * Use: Tries to find or create an appropriate temporary directory. + */ + +static char *trytmp(const char *parent, const char *base) +{ + static char c[] = { "0123456789" + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }; + char *p, *q; + char *qq; + dstr d; + int createflag = 1; + + /* --- Make sure the parent directory is sane --- * + * + * Must make sure that all the lead-up to the temporary directory is safe. + * Otherwise other people can play with symlinks or rename directories + * after I've done all this careful work to make the endpoint directory + * safe. + */ + + if (path_check(parent, &cp)) + return (0); + + /* --- See whether the trivial version will work --- */ + + dstr_create(&d); + dstr_putf(&d, "%s/%s", parent, base); + if (ok(d.buf, &createflag)) + goto good; + + /* --- Now try with various suffixes --- */ + + DENSURE(&d, 4); + qq = d.buf + d.len; + *qq++ = '-'; + + for (p = c; *p; p++) { + qq[0] = *p; + qq[1] = 0; + if (ok(d.buf, &createflag)) + goto good; + for (q = c; *q; q++) { + qq[1] = *q; + qq[2] = 0; + if (ok(d.buf, &createflag)) + goto good; + } + } + + /* --- No joy --- */ + + dstr_destroy(&d); + return (0); + +good: + p = xstrdup(d.buf); + dstr_destroy(&d); + return (p); +} + +/* --- @fullcheck@ --- * + * + * Arguments: @const char *p@ = pointer to path to check + * + * Returns: Zero if it's a bad directory. + * + * Use: Runs a thorough check on a directory. + */ + +static int fullcheck(const char *p) +{ + return (path_check(p, &cp) == 0 && ok(p, 0)); +} + +/* --- @goodtmp@ --- * + * + * Arguments: --- + * + * Returns: Pointer to a known-good secure temporary directory, or + * null. + * + * Use: Finds a good place to store temporary files. + */ + +static char *goodtmp(void) +{ + char *p, *q; + + /* --- First of all, try the user's current choice --- */ + + if ((p = getenv("TMPDIR")) != 0 && fullcheck(p)) + return (p); + + /* --- Try making a directory in `/tmp' --- */ + + if (!(q = getenv("USER")) && !(q = getenv("LOGNAME"))) + q = pw->pw_name; + if ((q = trytmp("/tmp", q)) != 0) + return (q); + + /* --- That failed: try a directory in the user's home --- */ + + if (!(q = getenv("HOME"))) + q = pw->pw_dir; + if ((q = trytmp(q, "tmp")) != 0) + return (q); + + /* --- Still no joy: give up --- * + * + * To be fair, if the user can't find a safe place in his own home + * directory then he's pretty stuffed. + */ + + return (0); +} + +/* --- @usage@ --- */ + +static void usage(FILE *fp) +{ + fprintf(fp, "Usage: %s [-bc] [-v PATH]\n", QUIS); +} + +/* --- @version@ --- */ + +static void version(FILE *fp) +{ + fprintf(fp, "%s version %s\n", QUIS, VERSION); +} + +/* --- @help@ --- */ + +static void help(FILE *fp) +{ + version(fp); + putc('\n', fp); + usage(fp); + fputs("\n\ +Sets a suitable and secure temporary directory, or checks that a given\n\ +directory is suitable for use with temporary files. A directory is\n\ +considered good for a particular user if it's readable and writable only\n\ +by that user, and if all its parents are modifiable only by the user or\n\ +root.\n\ +\n\ +Options supported:\n\ +\n\ +-h, --help Display this help text.\n\ +-V, --version Display the program's version number.\n\ +-u, --usage Display a terse usage summary.\n\ +\n\ +-b, --bourne Output a `TMPDIR' setting for Bourne shell users.\n\ +-c, --cshell Output a `TMPDIR' setting for C shell users.\n\ +-v, --verify PATH Check whether PATH is good, setting exit status.\n\ +\n\ +The default action is to examine the caller's shell and output a suitable\n\ +setting for that shell type.\n\ +", + fp); +} + +/* --- @main@ --- * + * + * Arguments: @int argc@ = number of command line arguments + * @char *argv[]@ = the actual argument strings + * + * Returns: Zero if all went well, else nonzero. + * + * Use: Performs helpful operations on temporary directories. + */ + +int main(int argc, char *argv[]) +{ + int shell = 0; + int duff = 0; + + enum { + sh_unknown, + sh_bourne, + sh_csh + }; + + /* --- Initialize variables --- */ + + ego(argv[0]); + me = getuid(); + cp.cp_what = CP_WRWORLD | CP_WRGRP | CP_WROTHUSR | CP_STICKYOK; + cp.cp_verbose = 0; + cp.cp_report = 0; + path_setids(&cp); + pw = getpwuid(me); + if (!pw) + die(1, "you don't exist"); + + /* --- Parse arguments --- */ + + for (;;) { + static struct option opts[] = { + { "help", 0, 0, 'h' }, + { "version", 0, 0, 'V' }, + { "usage", 0, 0, 'u' }, + { "bourne", 0, 0, 'b' }, + { "cshell", 0, 0, 'c' }, + { "verify", gFlag_argReq, 0, 'v' }, + { 0, 0, 0, 0 } + }; + int i = mdwopt(argc, argv, "hVu bcv:", opts, 0, 0, 0); + + if (i < 0) + break; + switch (i) { + case 'h': + help(stdout); + exit(0); + case 'V': + version(stdout); + exit(0); + case 'u': + usage(stdout); + exit(0); + case 'b': + shell = sh_bourne; + break; + case 'c': + shell = sh_csh; + break; + case 'v': + return (!fullcheck(optarg)); + break; + default: + duff = 1; + break; + } + } + + if (duff || optind != argc) { + usage(stderr); + exit(1); + } + + /* --- Choose a shell --- */ + + if (!shell) { + char *p; + if (!(p = getenv("SHELL"))) + p = pw->pw_shell; + if (strstr(p, "csh")) + shell = sh_csh; + else + shell = sh_bourne; + } + + /* --- Start the checking --- */ + + { + char *p = goodtmp(); + if (!p) + die(1, "no good tmp directory"); + switch (shell) { + case sh_bourne: + printf("TMPDIR=\"%s\"; export TMPDIR\n", p); + break; + case sh_csh: + printf("setenv TMPDIR \"%s\"\n", p); + break; + } + } + + return (0); +} + +/*----- That's all, folks -------------------------------------------------*/ -- 2.11.0