/* 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 /* struct passwd */ #include /* 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 ("suicide error notice"); puts ("

suicide attempt aborted!

"); puts ("Reason:"); puts (explanation); if (errno) { puts (" - "); puts (sys_errlist[errno]); } puts (""); 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 */