// $Id: cgi-wrap.c,v 1.3 2003/07/02 22:20:57 ensc Exp $ --*- c -*-- // Copyright (C) 2003 Enrico Scholz // // This program 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; version 2 of the License. // // This program 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 this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // // \file cgi-wrap.c // \brief Wrapper around the various big-sister perl-scripts // // This program is a wrapper to execute the big-sister scripts in a chroot as // a certain user. In particularly it: // # will be executed as setuid root // # drops privilegies and becomes a special user/group // # executes the command pointed by argv[0] in a special directory inside a // chroot environment // CGI_USERFILE is a file containing the following information in the given // order: // # // # + (space-separated) // # // # // # #ifdef HAVE_CONFIG_H # include #endif #ifndef _GNU_SOURCE # define _GNU_SOURCE #endif #include #include #include #include #include #include #include #include #include #include #ifndef NGROUPS # define NGROUPS 32 #endif #ifndef CGI_USERFILE # error CGI_USERFILE not defined #endif #ifdef __GNUC__ # define __unused__ __attribute__((__unused__)) #else # define __unused__ #endif static char const __unused__ VERSION[] = "version 0.2"; static char const __unused__ REVISION[] = "$Id: cgi-wrap.c,v 1.3 2003/07/02 22:20:57 ensc Exp $"; // This list is stolen from httpd's suexec.c static char const * const SAFE_ENV[] = { /* variable name starts with */ "HTTP_", "SSL_", /* variable name is */ "AUTH_TYPE=", "CONTENT_LENGTH=", "CONTENT_TYPE=", "DATE_GMT=", "DATE_LOCAL=", "DOCUMENT_NAME=", "DOCUMENT_PATH_INFO=", "DOCUMENT_ROOT=", "DOCUMENT_URI=", "FILEPATH_INFO=", "GATEWAY_INTERFACE=", "HTTPS=", "LAST_MODIFIED=", "PATH_INFO=", "PATH_TRANSLATED=", "QUERY_STRING=", "QUERY_STRING_UNESCAPED=", "REMOTE_ADDR=", "REMOTE_HOST=", "REMOTE_IDENT=", "REMOTE_PORT=", "REMOTE_USER=", "REDIRECT_QUERY_STRING=", "REDIRECT_STATUS=", "REDIRECT_URL=", "REQUEST_METHOD=", "REQUEST_URI=", "SCRIPT_FILENAME=", "SCRIPT_NAME=", "SCRIPT_URI=", "SCRIPT_URL=", "SERVER_ADMIN=", "SERVER_NAME=", "SERVER_ADDR=", "SERVER_PORT=", "SERVER_PROTOCOL=", "SERVER_SOFTWARE=", "UNIQUE_ID=", "USER_NAME=", "TZ=", 0 }; static struct { uid_t uid; gid_t gids[NGROUPS]; size_t gid_count; char const * user_name; char const * chroot_dir; char const * bin_dir; } configuration; #define WRITEMSG(X) (void)write(2,X,sizeof(X)-1); static void writeStr(int fd, char const *str) { (void)write(fd, str, strlen(str)); } static int checkLinkStat(char const *file, struct stat *buf, char const *file_type) { if (lstat(file, buf)!=0) { perror("lstat()"); return EXIT_FAILURE; } if ( (buf->st_mode & (S_ISUID|S_ISGID)) || (buf->st_uid!=0) ) { WRITEMSG("Invalid permissions and/or ownership of "); writeStr(2, file_type); WRITEMSG("\n"); return EXIT_FAILURE; } return 0; } static int checkFileStat(char const *file, struct stat *buf, char const *file_type) { if (stat(file, buf)!=0) { perror("stat()"); return EXIT_FAILURE; } if ( (buf->st_mode & (S_IWGRP|S_IWOTH|S_ISUID|S_ISGID)) || (buf->st_uid!=0) ) { WRITEMSG("Invalid permissions and/or ownership of "); writeStr(2, file_type); WRITEMSG("\n"); return EXIT_FAILURE; } return 0; } static char * readUID(uid_t *res, char *str) { char *endptr; assert(res!=0 && str!=0); *res = strtol(str, &endptr, 0); if (*str=='\0' || *endptr!='\0') { *res = -1; WRITEMSG("Failed to parse uid\n"); return 0; } return endptr; } static char * readGIDs(gid_t *gids, size_t *gid_len, char *str) { char *endptr = str; size_t idx = 0; assert(gids!=0 && gid_len!=0 && str!=0); for (;idx<*gid_len;++idx) { gids[idx] = strtol(str, &endptr, 0); if (*str=='\0' || (*endptr!='\0' && *endptr!=' ')) { gids[idx] = -1; *gid_len = 0; WRITEMSG("Failed to parse gid\n"); return 0; } if (*endptr=='\0') break; str = endptr+1; } if (*endptr!='\0') { WRITEMSG("Too much groups given\n"); *gid_len = 0; return 0; } *gid_len = idx+1; return endptr; } static int readConfiguration() { struct stat stat_name, stat_link, stat_file; int res; int fd; char buf[8192]; ssize_t len; char *ptr; char *ptr_tok; int state = 0; if ( (res=checkLinkStat(CGI_USERFILE, &stat_link, "configfile")) || (res=checkFileStat(CGI_USERFILE, &stat_name, "configfile")) ) return res; if ( (fd=open(CGI_USERFILE, O_RDONLY))==-1) { perror("open()"); return EXIT_FAILURE; } if (fstat(fd, &stat_file)==-1) { perror("fstat()"); return EXIT_FAILURE; } stat_file.st_atime = 0; stat_name.st_atime = 0; if ( stat_file.st_dev != stat_name.st_dev || stat_file.st_ino != stat_name.st_ino || stat_file.st_mode != stat_name.st_mode || stat_file.st_size != stat_name.st_size || stat_file.st_ctime != stat_name.st_ctime || stat_file.st_mtime != stat_name.st_mtime ) { WRITEMSG("RACE detected while checking configfile\n"); return EXIT_FAILURE; } again: len = read(fd, buf, sizeof(buf)-1); if (len==-1 && (errno==EAGAIN || errno==EINTR)) goto again; if (len==-1) { perror("read()"); return EXIT_FAILURE; } if (len+1==sizeof buf) { WRITEMSG("configfile contains too much information\n"); return EXIT_FAILURE; } buf[len] = '\0'; ptr = strtok_r(buf, "\n", &ptr_tok); while (ptr!=0 && *ptr!='\0' && state<=6) { switch (state) { case 0 : ptr = readUID(&configuration.uid, ptr); if (ptr==0) return EXIT_FAILURE; break; case 1 : configuration.gid_count = NGROUPS; ptr = readGIDs(configuration.gids, &configuration.gid_count, ptr); if (ptr==0) return EXIT_FAILURE; break; case 2 : configuration.user_name = strdup(ptr); break; case 3 : configuration.chroot_dir = strdup(ptr); break; case 4 : configuration.bin_dir = strdup(ptr); break; default : break; } ptr = strtok_r(0, "\n", &ptr_tok); ++state; } if (state!=5) { WRITEMSG("Failed to parse configfile\n"); return EXIT_FAILURE; } if (configuration.gid_count==0) { WRITEMSG("Unexpected internal error: gid_count is still 0\n"); assert(0); return EXIT_FAILURE; } return 0; } static int getBasename(char *dst, char const *src) { char const * start = src; char * ptr = dst+2; { char const * i; for (i=src; *i!='\0'; ++i) { if (*i=='/') start=i+1; } } dst[0] = '.'; dst[1] = '/'; strcpy(ptr, start); switch (*ptr) { case '\0' : WRITEMSG("basename must not be empty\n"); return EXIT_FAILURE; case '.' : WRITEMSG("basename must not begin with a period\n"); return EXIT_FAILURE; case '/' : // Can not happen WRITEMSG("internal error\n"); return EXIT_FAILURE; } for (; *ptr!='\0'; ++ptr) { if ( (*ptr>='A' && *ptr<='Z') || (*ptr>='a' && *ptr<='z') || (*ptr>='0' && *ptr<='9') ) { /* all ok */ } else { switch (*ptr) { case '_' : case '-' : case '.' : break; // period '.' was checked already default : WRITEMSG("basename contains invalid characters\n"); return EXIT_FAILURE; } } } return 0; } static int doChroot() { if (chdir(configuration.chroot_dir)!=0) { perror("chdir()"); return EXIT_FAILURE; } if (chroot(configuration.chroot_dir)!=0) { perror("chroot()"); return EXIT_FAILURE; } return 0; } static int changeIDs() { uid_t const uid = configuration.uid; gid_t const gid = configuration.gids[0]; if (uid==0 || gid==0) { WRITEMSG("UID/GID must not be zero\n"); return EXIT_FAILURE; } if (setgroups(configuration.gid_count,configuration.gids)!=0) { perror("setgroups()"); return EXIT_FAILURE; } if (setregid(gid,gid)!=0 || setreuid(uid,uid)!=0) { perror("setregid()/setreuid()"); return EXIT_FAILURE; } if (getgid()!=gid || getegid()!=gid || getuid()!=uid || geteuid()!=uid) { WRITEMSG("unexpected GID/UID\n"); return EXIT_FAILURE; } return 0; } static int isValidEnv(char const *ptr) { char const * const * i; for (i=SAFE_ENV; *i!=0; ++i) { if (strncmp(ptr, *i, strlen(*i))==0) return 1; } return 0; } static int setEnv(char *buf) { char **out_ptr = environ; char **in_ptr = environ; if (in_ptr!=0) { for (; *in_ptr!=0; ++in_ptr) { if (isValidEnv(*in_ptr)) { *out_ptr = *in_ptr; ++out_ptr; } } *out_ptr = 0; } if ( putenv("IFS= \t\n")==-1 || putenv("PATH=/bin:/usr/bin")==-1 || putenv("TERM=dump")==-1 ) { perror("putenv()"); return EXIT_FAILURE; } strcpy(buf, "USER="); strcat(buf, configuration.user_name); if ( (putenv(buf)==-1) ) { perror("setenv()"); return EXIT_FAILURE; } return 0; } static int checkPerm(char const *file) { struct stat buf; int res; if (stat(".", &buf)!=0) { perror("stat()"); return EXIT_FAILURE; } if ( (buf.st_mode & (S_IWGRP|S_IWOTH)) || (buf.st_uid!=0) || !S_ISDIR(buf.st_mode) ) { WRITEMSG("Invalid permissions and/or ownership of bindir\n"); return EXIT_FAILURE; } if ( (res=checkLinkStat(file, &buf, "executable")) || (res=checkFileStat(file, &buf, "executable")) ) return res; return 0; } int main(int __unused__ argc, char *argv[]) { int res; char * basename; char * user_env; if ( (res = readConfiguration()) || (res = doChroot()) || (res = changeIDs())) return res; user_env = alloca(strlen(configuration.user_name) + sizeof("USER=") + 1); if ( (res = setEnv(user_env)) ) return res; if (chdir(configuration.bin_dir)!=0) { perror("chdir()"); return EXIT_FAILURE; } basename = alloca(strlen(argv[0])+3); if ( (res = getBasename(basename, argv[0])) ) return res; if ( (res = checkPerm(basename)) ) return res; (void)execv(basename, argv+0); perror("execv()"); return EXIT_FAILURE; } // Local Variables: // compile-command: "make cgi-wrap CFLAGS='-O0 -g3 -std=c99 -Wall -W -pedantic' CONFFILE=/tmp/cgi-wrap.conf STRIP=:" // fill-column: 80 // End: