/* When installed setuid root, this wrapper will execute CGI programs
   in their author's name.  It has a more transparent interface than
   http://www.umr.edu/~cgiwrap/ and is more secure than
   ftp://ftp.rz.uni-karlsruhe.de/pub/net/www/tools/cgi-src/user.tar.gz
   
   Consult your pharmacist for more information on the suicidal side
   effects of setuid programs.  For instance, make sure that ld(1)
   ignores LD_LIBRARY_PATH for them.

   The suicide binary must be installed in a directory like
   /usr/local/etc where the S_ISUID semantics are in effect.

   Configuring ScriptAlias /sui/ /usr/local/etc/suicide/ for NCSA or
   Exec /sui/* /usr/local/etc/suicide/* for CERN httpd will turn it
   on.  User CGI programs can then be refrenced by URLs like

	http://www.cs.tu-berlin.de/sui/czyborra/hello
   
   The suicides had better heed the following rules:

   1. no logging or exporting of private data
   2. no resource hogging
   3. inspectable directories

*/

#include <pwd.h>		/* struct passwd */
#include <sys/stat.h>		/* struct stat */

/* This wrapper is not intended for command line use.  As it will be
   invoked through a HTTP server we will pipe the error messages to
   stdout so that the caller gets to see them. */

error (explanation)
     char *explanation;
{
    extern errno; extern char *sys_errlist[];

    puts ("Status: 404 Not Found\nContent-Type: text/html\n");
    puts ("<HTML><HEAD><TITLE>suicide error notice</TITLE></HEAD><BODY>");
    puts ("<H1><A HREF=/www/suicide.html>suicide</A> attempt aborted!</H1>");
    puts ("Reason:");
    puts (explanation);
    if (errno) {
	puts (" - ");
	puts (sys_errlist[errno]);
    }
    puts ("</BODY></HTML>");
    exit (1);
}

/* String manipulation in C is a lot harder than in PERL, but our
   local perl binary isn't fit for root yet.  And this little thing
   could be coded in C after all, only two little string functions
   needed: startswith() returns true iff string starts with prefix,
   prepend() copies the prefix onto the string */

startswith (prefix, string)
     char *prefix, *string;
{
    while (*prefix)
	if (*prefix++ != *string++)
	    return 0;
    return 1;
}

prepend (prefix, string)
     char *prefix, *string;
{
    while (*prefix) *string++ = *prefix++;
}

/* The main function performs the following steps:

	1. sift environment
	2. parse parameters
	3. adjust environment
	4. find executable
	5. check security
	6. set uid
	7. run program
*/

main (argc, argv, envp)
     char **argv, **envp;	/* execl(3)  */
{
    /* Simply because this is easier to code, we impose a length limit
       of forty characters on the name of the CGI script.  Since
       PATH_INFO and QUERY_STRING can grow as long as they want,
       this shouldn't be a problem. */

    static char user[8+1], program[40+1], selfreference[80+1];
    char **read, **write, **PATH_INFO = 0, **SCRIPT_NAME = 0;
    struct passwd * nis;
    struct stat file;

    umask (022); /* just to be sure */

    /* First of all, the environment is sifted.  All potentially evil
       values are deleted, only those listed in the CGI specification
       http://hoohoo.ncsa.uiuc.edu/cgi/interface.html pass. */

    for (read = write = envp; * read; ++ read)
    {
	/* The most important parameter is PATH_INFO saying which
           program to call.  PATH_INFO and SCRIPT_INFO can be
           rewritten so that the user interface simulates a direct CGI
           call.  PATH_TRANSLATED depends on the server configuration,
           so we'd rather forget about it. */

 	if (startswith ("PATH_INFO=", *read))
	{
	    PATH_INFO=write; *write++ = *read; 
	}

	else if (startswith ("SCRIPT_NAME=", *read))
	{
	    SCRIPT_NAME=write; *write++ = *read; 
	}

	/* All variables starting with HTTP_ must be passed
	   transparently.  The other variables are recognized by
	   prefix as well.  This is a little simpler and allows more
	   variables with these prefixes to be defined. */

	else if (startswith ("AUTH_",    *read) || 
		 startswith ("CONTENT_", *read) ||
		 startswith ("DOCUMENT_",*read) ||
		 startswith ("GATEWAY_", *read) ||
		 startswith ("HTTP_",    *read) ||
		 startswith ("QUERY_",   *read) ||
		 startswith ("REFERER_", *read) ||
		 startswith ("REMOTE_",  *read) ||
		 startswith ("REQUEST_", *read) ||
		 startswith ("SERVER_",  *read))
	{
	    *write++ = *read;
	}

	/* A sparse PATH like /bin would teach script authors to set
	   their PATH, but it would sabotage their first attempts.
	   Therefore we prefer to define a more sensible PATH.
	   /usr/local/bin appears first because we want to use our
	   locally modified commands, /usr/ucb precedes /bin so we get
	   the BSD-style echo -n to work with the SunOS 5 shell and
	   /export/www/bin is needed for tools like cgiparse. */

	else if (startswith ("PATH=", *read))
	{
	    *write++= "PATH=/usr/local/bin:/usr/ucb:/bin:/export/www/bin";
	}

    }

    *write= 0;			/* terminates list */

    /* The first component of PATH_INFO must be the user's login and
       the second the name of her program. */

    PATH_INFO 
	|| error ("missing /PATH_INFO");

    /* Before the HTTP daemons stuff URL-encoded characters into
       PATH_INFO they decode them.  So we can simply use sscanf(3).
       String lengths must be limited to prevent break-ins through
       overwritten array bounds. */

    sscanf (*PATH_INFO, "PATH_INFO=/%8[a-z0-9]", user) > 0
	|| error ("missing /username/");

    (nis= getpwnam (user))
	|| error ("no such user");

    sscanf (*PATH_INFO, "PATH_INFO=/%*[a-z0-9]/%40[^/]", program) > 0
	|| error ("missing /program");

    /* The /user/program part is wiped out of PATH_INFO and appended
       to SCRIPT_NAME so that the users get the real CGI flavor */

    if (SCRIPT_NAME) {
	sprintf (selfreference, "%.30s/%s/%s", *SCRIPT_NAME, user, program);
	*SCRIPT_NAME = selfreference;
    }

    *PATH_INFO += strlen (user) + strlen (program) + 2;
    prepend ("PATH_INFO=", *PATH_INFO);

    /* Where should the users place their CGI programs?  A .public_cgi
       subdirectory at $HOME would create an NFS dependency that we
       don't want.  Instead we prefer a overseeable suicide directory
       on the server where each user can install her CGI home.  Even
       though this may lead to quota abuses.  But the server
       administrators can threaten with aliasing away the user's URLs
       to get them to comply with the rules. */

    /* /export/www exists on the server machine only.  Others see this
       subtree as /home/www.  If this program is called on a machine
       other than the WWW server it will refuse to run. */

    !chdir ("/export/www")
	|| error ("cannot chdir to /export/www");

    /* The following security checks suffice for our particular setup.
       No guarantee for anybody else. */

    !lstat ("sui", &file) 
	&& S_ISDIR (file.st_mode) && !chdir ("sui") && !file.st_uid
	|| error ("/home/www/sui must be a public directory owned by root");

    !lstat (user, &file) && S_ISDIR (file.st_mode) && !chdir (user)
	|| error ("this user has no personal CGI directory");

    file.st_uid == nis->pw_uid
	|| error ("directory belongs to a stranger");

    !(file.st_mode & (S_IWGRP|S_IWOTH))
	|| error ("insecure directory writable by group/others");

    !lstat (program, &file)
	|| error ("no such program");

    !S_ISLNK(file.st_mode)
	|| error ("symbolic links are considered insecure");

    S_ISREG(file.st_mode)
	|| error ("the executable is not a regular file");

    !(file.st_mode & (S_IWGRP|S_IWOTH))
	|| error ("insecure file writable by group/others");

    file.st_uid == nis->pw_uid
	|| error ("executable file belongs to a stranger");

    /* Everything smells alright?  Change suit and run program. */

    !setgid (nis->pw_gid)           || error ("setgid failed");
    !initgroups (user, nis->pw_gid) || error ("initgroups failed");
    !setuid (nis->pw_uid)           || error ("setuid failed");

    if (*argv) argv[0]= program;
    execve (program, argv, envp);
    error ("execve failed");
}

/* Roman Czyborra@cs.tu-berlin.de, February 1995 */

