From: mdw Date: Tue, 6 Apr 1999 20:12:06 +0000 (+0000) Subject: Initial revision X-Git-Tag: 1.0.1~12 X-Git-Url: https://git.distorted.org.uk/~mdw/checkpath/commitdiff_plain/efa7a97bf01444f8bfdf25f488932912d3710974 Initial revision --- efa7a97bf01444f8bfdf25f488932912d3710974 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 -------------------------------------------------*/