From: warren@itexjct.jct.ac.il (Warren Burstein)
Newsgroups: comp.unix.admin
Subject: Is this program secure?
Date: 27 Jul 92 18:36:51 GMT
Reply-To: warren@itexjct.jct.ac.il
Organization: Mail to News Gateway at Wang Labs

The problem: people anonymously ftp files into our upload directory -
these files are owned by ftp.  Also, sometimes people who have
accounts ftp stuff into our uploads dir using their own login - these
files are owned by the user.

The people who maintain the ftp archives log in as 'archives' rather
than ftp (to minimize the damage that they can do) and need the files
to be owned by archives so that they can do things like chmod them.

So I wrote this program which makes sure the user has an euid of
archives or root and that the file is underneath ~ftp, and if so
chowns it to archives.  The program will be suid root, so I'd like to 
be sure there are no holes (like allowing someone to make a symbolic
link to /etc, chown passwd and ...).  I think the tactic of starting
from the file and constantly moving up one directory until I hit
either root or ~ftp will work, but if I've done something stupid, I'd
rather be told about it now before a hacker finds out.

And if I've re-invented the wheel, please let me know.

/*
 * If the user is USER or root, and the file is underneath ~ftp,
 * chown it to USER.
 */

#include <stdio.h>
#include <varargs.h>
#include <pwd.h>
#include <sys/types.h>
#include <sys/stat.h>

#ifdef BSD
#define strrchr rindex
#endif

char *strrchr(), *strcpy(), *strncpy(), *strcat(), *malloc();
#define TRUE 1
#define FALSE 0

#define USER "archives"
#define FTP  "ftp"

char *argv0;

main(argc, argv)
char **argv;
{
    struct passwd *pw;
    struct stat home_st, root_st;
    int arg, uid = getuid(), user_uid, user_gid;
    char *home_dir;
    
    argv0 = argv[0];
    if (argc <= 1) {
	fprintf(stderr, "usage: %s files\n", argv[0]);
	exit(1);
    }

    /*
     * Check that program is suid root.  Won't do anything if it isn't
     * so might as well give up.
     */
    if (geteuid() != 0)
	fatal("program is not suid root");

    /*
     * Get USER's pw to find uid and gid that files will get changed to
     */
    if ( !(pw = getpwnam(USER)))
	fatal("user %s unknown", USER);
    user_uid = pw->pw_uid;
    user_gid = pw->pw_gid;

    /*
     * Get FTP's pw to find it's home dir.  Get its stat.
     */
    if ( !(pw = getpwnam(FTP)))
	fatal("user %s unknown", FTP);
    if (stat(home_dir = pw->pw_dir, &home_st))
	fatal("cannot stat dir %s", home_dir);

    /*
     * Also get root's stat.
     */
    if (stat("/", &root_st))
	fatal("cannot stat /");

    /*
     * Check that user is USER or root.
     */
    if (uid != 0 && uid != user_uid)
	fatal("you are not logged in as %s or root", USER);

    /*
     * Change the files if underneath ~ftp.
     */
    for (arg = 1; arg < argc; arg++)
	if ( !underneath(&home_st, &root_st, argv[arg]))
	    fprintf(stderr, "%s: file %s is not underneath %s\n",
		    argv0, argv[arg], home_dir);
	else if (chown(argv[arg], user_uid, user_gid))
	    perror(argv[arg]);
}

/*
 * Is file underneath home_dir?
 *
 * Get the dir that the file is in
 *   if it's identical to home_dir it's underneath
 *   if it's identical to / it's not.
 *   if it's not identical to either, append /.. and try again
 */
int underneath(home_st, root_st, file)
char *file;
struct stat *home_st, *root_st;
{
    char *dir;
    struct stat st;

    /*
     * If file contains a slash, its dir is the file truncated at the last
     * slash.  Otherwise its dir is ..
     */
    if (dir = strrchr(file, '/')) {
	int len = dir - file;

	if ( !(dir = malloc(len + 1)))
	    fatal("cannot alloc for %s", file);
	strncpy(dir, file, len);
	dir[len] = '\0';
    } else {
	if ( !(dir = malloc(3)))
	    fatal("cannot alloc for ..", file);
	strcpy(dir, "..");
    }

    /*
     * Keep moving to .. until we hit either the root dir or the home dir.
     */
#define SAME_INODE(p, q) ((p)->st_ino==(q)->st_ino && (p)->st_dev==(q)->st_dev)
    for (;;) {
	char *new_dir;

	if (stat(dir, &st))
	    fatal("cannot stat dir %s", dir);

	if (SAME_INODE(&st, root_st)) {
	    free(dir);
	    return FALSE;
	}

	if (SAME_INODE(&st, home_st)) {
	    free(dir);
	    return TRUE;
	}

	/*
	 * Append /..
	 */
	if ( !(new_dir = malloc(strlen(dir) + 4)))
	    fatal("cannot alloc for %s/..", file);
	strcpy(new_dir, dir);
	strcat(new_dir, "/..");
	free(dir);
	dir = new_dir;
    }
}

fatal(va_alist)
va_dcl
{
    va_list args;
    char *fmt;
    extern char *argv0;

    va_start(args);
    fmt = va_arg(args, char *);
    (void) fprintf(stderr, "%s: ", argv0);
    (void) vfprintf(stderr, fmt, args);
    (void) fprintf(stderr, "\n");
    va_end(args);

    exit(1);
}
-- 
/|/-\/-\       The entire kitchen		Jerusalem
 |__/__/_/     is a very Czarish apple.
 |warren@      But the Kibo
/ nysernet.org is not all that paranoid.
Newsgroups: comp.unix.admin,comp.unix.shell