From b1ca8af3f7d9f11e3887f8a0aaf7b28ed0dd37b5 Mon Sep 17 00:00:00 2001 From: mikelodder7 Date: Tue, 23 Feb 2016 16:00:06 -0700 Subject: [PATCH 1/7] Added suexec like behavior when -u flag is used --- fcgiwrap.c | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 144 insertions(+), 5 deletions(-) diff --git a/fcgiwrap.c b/fcgiwrap.c index b44d8aa..b187364 100644 --- a/fcgiwrap.c +++ b/fcgiwrap.c @@ -41,6 +41,8 @@ #include #include #include +#include +#include #include #include @@ -83,9 +85,11 @@ static const char * blacklisted_env_vars[] = { }; static int stderr_to_fastcgi = 0; +static int use_suexec = 0; #define FCGI_BUF_SIZE 4096 +#define MIN_ID_ALLOWED 1000 static int write_all(int fd, char *buf, size_t size) { @@ -380,6 +384,119 @@ static int check_file_perms(const char *path) } } +static int check_suexec(const char* const cgi_filename, struct stat *ls) +{ + struct stat pls; + struct passwd *user; + struct group *group; + char* p; + char* parent = NULL; + size_t len_docroot, len_filename, len_parent, len_userdir; + + /* Can stat the target cgi program */ + if (lstat(cgi_filename, ls) < 0) { + return -EACCES; + } else if (!S_ISREG(ls->st_mode) && S_ISLNK(ls->st_mode)) { + if (stat(cgi_filename, ls) < 0) { + return -EACCES; + } + } + + /* Is the target cgi program not writable by anyone else */ + /* Is the target cgi program NOT setuid or setgid */ + if ((ls->st_mode & S_IWGRP) || + (ls->st_mode & S_IWOTH) || + (ls->st_mode & S_ISUID) || + (ls->st_mode & S_ISGID)) { + return -EACCES; + } + + /* Is the target userid or groupid a superuser */ + if (ls->st_uid == 0 || ls->st_gid == 0) { + return -EACCES; + } + + /* Is the target user name valid */ + user = getpwuid(ls->st_uid); + if (user == NULL) { + return -EACCES; + } + + /* Is the target group name valid */ + group = getgrgid(ls->st_gid); + if (group == NULL) { + return -EACCES; + } + + /* Is the target user/group the same as the owner */ + if (group->gr_gid != user->pw_gid) { + return -EACCES; + } + + /* Is the target groupid or userid above the minimum ID number */ + if (group->gr_gid < MIN_ID_ALLOWED || user->pw_uid < MIN_ID_ALLOWED) { + return -EACCES; + } + + /* Is the directory within document root */ + if ((p = getenv("DOCUMENT_ROOT"))) { + len_docroot = strlen(p); + len_filename = strlen(cgi_filename); + len_userdir = strlen(user->pw_dir); + + if (len_docroot > len_filename) { + return -EACCES; + } else if (strncmp(p, cgi_filename, len_docroot) != 0) { + return -EACCES; + } else if (len_userdir > len_filename) { /* Is the target directory within the user's directory */ + return -EACCES; + } else if (strncmp(user->pw_dir, cgi_filename, len_userdir) != 0) { + return -EACCES; + } + } else { + /* DOCUMENT_ROOT must be set to use suexec */ + return -EACCES; + } + + p = strrchr(cgi_filename, '/'); + + if (!p) { + return -EACCES; + } + + len_parent = p - cgi_filename; + + parent = malloc(len_parent); + if (!parent) { + return -EACCES; + } + + strncpy(parent, cgi_filename, len_parent); + + if (lstat(parent, &pls) < 0) { + goto err_parent; + } else if (!S_ISDIR(pls.st_mode) && S_ISLNK(pls.st_mode)) { + if (stat(parent, &pls) < 0) { + goto err_parent; + } else if (!S_ISDIR(pls.st_mode)) { + goto err_parent; + } + } + free(parent); + /* Make sure directory is not writable by anyone else */ + if ((pls.st_mode & S_IWGRP) || (pls.st_mode & S_IWOTH)) { + return -EACCES; + } + if (pls.st_gid != ls->st_gid || pls.st_uid != ls->st_uid) { + return -EACCES; + } + + return 0; +err_parent: + free(parent); + return -EACCES; +} + static char *get_cgi_filename(void) /* and fixup environment */ { int buflen = 1, docrootlen; @@ -523,6 +640,7 @@ static void handle_fcgi_request(void) char *last_slash; char *p; pid_t pid; + struct stat ls; struct fcgi_context fc; @@ -577,6 +695,18 @@ static void handle_fcgi_request(void) } } + if (use_suexec) { + if (check_suexec(filename, &ls) < 0) { + cgi_error("403 Forbidden", "Cannot suexec script because the permissions are incorrect", filename); + } + if (setgid(ls.st_gid) < 0) { + cgi_error("403 Forbidden", "Cannot change to script group owner", filename); + } + if (setuid(ls.st_uid) < 0) { + cgi_error("403 Forbidden", "Cannot change to script user owner", filename); + } + } + execl(filename, filename, (void *)NULL); cgi_error("502 Bad Gateway", "Cannot execute script", filename); @@ -731,7 +861,7 @@ static int setup_socket(char *url) { if (strlen(p) >= UNIX_PATH_MAX) { fprintf(stderr, "Socket path too long, exceeds %d characters\n", - UNIX_PATH_MAX); + UNIX_PATH_MAX); return -1; } @@ -776,9 +906,9 @@ static int setup_socket(char *url) { } else { invalid_url: fprintf(stderr, "Valid socket URLs are:\n" - "unix:/path/to/socket for Unix sockets\n" - "tcp:dot.ted.qu.ad:port for IPv4 sockets\n" - "tcp6:[ipv6_addr]:port for IPv6 sockets\n"); + "unix:/path/to/socket for Unix sockets\n" + "tcp:dot.ted.qu.ad:port for IPv4 sockets\n" + "tcp6:[ipv6_addr]:port for IPv6 sockets\n"); return -1; } @@ -803,10 +933,11 @@ int main(int argc, char **argv) { int nchildren = 1; char *socket_url = NULL; + int i_am_root = (getuid() == 0); int fd = 0; int c; - while ((c = getopt(argc, argv, "c:hfs:p:")) != -1) { + while ((c = getopt(argc, argv, "c:hufs:p:")) != -1) { switch (c) { case 'f': stderr_to_fastcgi++; @@ -817,6 +948,7 @@ int main(int argc, char **argv) "Options are:\n" " -f\t\t\tSend CGI's stderr over FastCGI\n" " -c \t\tNumber of processes to prefork\n" + " -u \tUse suexec like behavior. Change to owner uid and gid before executing. (See https://httpd.apache.org/docs/2.4/suexec.html)\n" " -s \tSocket to bind to (say -s help for help)\n" " -h\t\t\tShow this help message and exit\n" " -p \t\tRestrict execution to this script. (repeated options will be merged)\n" @@ -825,6 +957,13 @@ int main(int argc, char **argv) argv[0] ); return 0; + case 'u': + if (!i_am_root) { + fprintf(stderr, "Option -%c requires program to be run as superuser.\n", optopt); + return 1; + } + use_suexec = 1; + break; case 'c': nchildren = atoi(optarg); break; From 1292b48ca47dca519510d7659912d19710ac85ed Mon Sep 17 00:00:00 2001 From: mikelodder7 Date: Wed, 24 Feb 2016 13:50:49 -0700 Subject: [PATCH 2/7] Handle unsafe hierarchical reference --- fcgiwrap.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fcgiwrap.c b/fcgiwrap.c index b187364..65c0d51 100644 --- a/fcgiwrap.c +++ b/fcgiwrap.c @@ -452,6 +452,8 @@ static int check_suexec(const char* const cgi_filename, struct stat *ls) return -EACCES; } else if (strncmp(user->pw_dir, cgi_filename, len_userdir) != 0) { return -EACCES; + } else if (strstr(cgi_filename, "..") != NULL) { /* Unsafe hierarchical reference */ + return -EACCES; } } else { /* DOCUMENT_ROOT must be set to use suexec */ From 0a4eb72ae281b3cc59c01fa7c705a7ac8b1a32a7 Mon Sep 17 00:00:00 2001 From: mikelodder7 Date: Fri, 26 Feb 2016 09:14:44 -0700 Subject: [PATCH 3/7] Updated Readme --- README.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 5d7a30e..fa70899 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ ======== -fcgiwrap +fcgiwrap-suexec ======== -:Info: Simple FastCGI wrapper for CGI scripts +:Info: Simple FastCGI wrapper for CGI scripts that can use suexec like behavior :Homepage: http://nginx.localdomain.pl/wiki/FcgiWrap :Author: Grzegorz Nosek :Contributors: W-Mark Kubacki @@ -55,3 +55,5 @@ Most probably you will want ``fcgiwrap`` be launched by `www-servers/spawn-fcgi There are two modes of ``fcgiwrap`` operation: - when *SCRIPT_FILENAME* is set, its value is treated as the script name and executed directly. - otherwise, *DOCUMENT_ROOT* and *SCRIPT_NAME* are concatenated and split back again into the script name and *PATH_INFO*. For example, given a *DOCUMENT_ROOT* of ``/www/cgi`` and *SCRIPT_NAME* of ``/subdir/example.cgi/foobar``, ``fcgiwrap`` will execute ``/www/cgi/subdir/example.cgi`` with *PATH_INFO* of ``/foobar`` (assuming ``example.cgi`` exists and is executable). + + Use the ``-u`` flag to enable suexec behavior similar to http://httpd.apache.org/docs/current/suexec.html From 711735c34fe2e304dde39af7263108573f329311 Mon Sep 17 00:00:00 2001 From: mikelodder7 Date: Fri, 26 Feb 2016 09:15:47 -0700 Subject: [PATCH 4/7] Update README.rst --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index fa70899..1af986f 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,8 @@ fcgiwrap-suexec ======== :Info: Simple FastCGI wrapper for CGI scripts that can use suexec like behavior :Homepage: http://nginx.localdomain.pl/wiki/FcgiWrap -:Author: Grzegorz Nosek +:Original Author: Grzegorz Nosek +:Author: Mike Lodder :Contributors: W-Mark Kubacki Jordi Mallach From a58f1ffc0bff97268a1f60b4429c38f5ea1ab890 Mon Sep 17 00:00:00 2001 From: mikelodder7 Date: Fri, 26 Feb 2016 09:25:04 -0700 Subject: [PATCH 5/7] Update README.rst --- README.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1af986f..b94929e 100644 --- a/README.rst +++ b/README.rst @@ -57,4 +57,12 @@ There are two modes of ``fcgiwrap`` operation: - when *SCRIPT_FILENAME* is set, its value is treated as the script name and executed directly. - otherwise, *DOCUMENT_ROOT* and *SCRIPT_NAME* are concatenated and split back again into the script name and *PATH_INFO*. For example, given a *DOCUMENT_ROOT* of ``/www/cgi`` and *SCRIPT_NAME* of ``/subdir/example.cgi/foobar``, ``fcgiwrap`` will execute ``/www/cgi/subdir/example.cgi`` with *PATH_INFO* of ``/foobar`` (assuming ``example.cgi`` exists and is executable). - Use the ``-u`` flag to enable suexec behavior similar to http://httpd.apache.org/docs/current/suexec.html +Use the ``-u`` flag to enable suexec behavior similar to http://httpd.apache.org/docs/current/suexec.html + +When using the ``-u`` flag, spawn-fcgi will probably not allow you to run fcgiwrap as root which is a requirement for suexec. +In this case you can run it as root with the following command +``/usr/local/sbin/fcgiwrap -f -u -c -s unix:/var/run/fcgiwrap.socket`` + +/var/run/fcgiwrap.socket will be owned by root and only writable by root. Webserver like nginx or yaws need write permissions. +Using ``chmod 777 /var/run/fcgiwrap.socket`` will allow them to use the socket while still running as another system user than root. +If fcgiwrap is restarted make sure to readjust the permissions on the socket. I have found ports to not suffer from this issue. From 310da07f35d82dbaa5ae30ffc3db949fca1457ae Mon Sep 17 00:00:00 2001 From: mikelodder7 Date: Mon, 12 Sep 2016 13:26:25 -0600 Subject: [PATCH 6/7] Added suexec log --- fcgiwrap.c | 128 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 106 insertions(+), 22 deletions(-) diff --git a/fcgiwrap.c b/fcgiwrap.c index 65c0d51..b85c12d 100644 --- a/fcgiwrap.c +++ b/fcgiwrap.c @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -86,6 +87,7 @@ static const char * blacklisted_env_vars[] = { static int stderr_to_fastcgi = 0; static int use_suexec = 0; +static FILE* suexec_log; #define FCGI_BUF_SIZE 4096 @@ -384,57 +386,89 @@ static int check_file_perms(const char *path) } } +static void print_time_suexec_log(void) +{ + time_t rawtime; + struct tm* info; + char* months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + time(&rawtime); + info = localtime(&rawtime); + fprintf(suexec_log, "[%d/%s/%d %d:%d:%d]: ", (info->tm_year+1900), months[info->tm_mon], info->tm_mday, info->tm_hour, info->tm_min, info->tm_sec); +} + static int check_suexec(const char* const cgi_filename, struct stat *ls) { struct stat pls; struct passwd *user; struct group *group; char* p; - char* parent = NULL; - size_t len_docroot, len_filename, len_parent, len_userdir; + size_t len_docroot, len_filename, len_userdir; /* Can stat the target cgi program */ if (lstat(cgi_filename, ls) < 0) { + print_time_suexec_log(); + fprintf(suexec_log, "Can't stat %s\n", cgi_filename); + fflush(suexec_log); return -EACCES; } else if (!S_ISREG(ls->st_mode) && S_ISLNK(ls->st_mode)) { if (stat(cgi_filename, ls) < 0) { + print_time_suexec_log(); + fprintf(suexec_log, "Can't stat %s\n", cgi_filename); + fflush(suexec_log); return -EACCES; } } /* Is the target cgi program not writable by anyone else */ /* Is the target cgi program NOT setuid or setgid */ - if ((ls->st_mode & S_IWGRP) || - (ls->st_mode & S_IWOTH) || + if ((ls->st_mode & S_IWOTH) || (ls->st_mode & S_ISUID) || (ls->st_mode & S_ISGID)) { + print_time_suexec_log(); + fprintf(suexec_log, "%s is writable by anyone or has setuid/setgid set\n", cgi_filename); + fflush(suexec_log); return -EACCES; } /* Is the target userid or groupid a superuser */ if (ls->st_uid == 0 || ls->st_gid == 0) { + print_time_suexec_log(); + fprintf(suexec_log, "%s is owned by root\n", cgi_filename); + fflush(suexec_log); return -EACCES; } /* Is the target user name valid */ user = getpwuid(ls->st_uid); if (user == NULL) { + print_time_suexec_log(); + fprintf(suexec_log, "%s is owned by user id %d. %d is not a user on this system\n", cgi_filename, ls->st_uid, ls->st_uid); + fflush(suexec_log); return -EACCES; } /* Is the target group name valid */ group = getgrgid(ls->st_gid); if (group == NULL) { + print_time_suexec_log(); + fprintf(suexec_log, "%s is owned by group id %d. %d is not a group on this system\n", cgi_filename, ls->st_gid, ls->st_gid); + fflush(suexec_log); return -EACCES; } /* Is the target user/group the same as the owner */ if (group->gr_gid != user->pw_gid) { + print_time_suexec_log(); + fprintf(suexec_log, "%s is owned by group id %d but expected %d\n", cgi_filename, group->gr_gid, user->pw_gid); + fflush(suexec_log); return -EACCES; } /* Is the target groupid or userid above the minimum ID number */ if (group->gr_gid < MIN_ID_ALLOWED || user->pw_uid < MIN_ID_ALLOWED) { + print_time_suexec_log(); + fprintf(suexec_log, "%s is owned by user id %d. The minimal group/user id allowed is %d\n", cgi_filename, ls->st_uid, MIN_ID_ALLOWED); + fflush(suexec_log); return -EACCES; } @@ -445,57 +479,83 @@ static int check_suexec(const char* const cgi_filename, struct stat *ls) len_userdir = strlen(user->pw_dir); if (len_docroot > len_filename) { + print_time_suexec_log(); + fprintf(suexec_log, "%s is not located in %s\n", cgi_filename, p); + fflush(suexec_log); return -EACCES; } else if (strncmp(p, cgi_filename, len_docroot) != 0) { + print_time_suexec_log(); + fprintf(suexec_log, "%s is not located in %s\n", cgi_filename, p); + fflush(suexec_log); return -EACCES; } else if (len_userdir > len_filename) { /* Is the target directory within the user's directory */ + print_time_suexec_log(); + fprintf(suexec_log, "%s is not located in the user's directory\n", cgi_filename); + fflush(suexec_log); return -EACCES; } else if (strncmp(user->pw_dir, cgi_filename, len_userdir) != 0) { - return -EACCES; - } else if (strstr(cgi_filename, "..") != NULL) { /* Unsafe hierarchical reference */ + print_time_suexec_log(); + fprintf(suexec_log, "%s is not located in the user's directory\n", cgi_filename); + fflush(suexec_log); return -EACCES; } } else { /* DOCUMENT_ROOT must be set to use suexec */ + print_time_suexec_log(); + fprintf(suexec_log, "DOCUMENT_ROOT is not set. DOCUMENT_ROOT must be set to use suexec for %s\n", cgi_filename); + fflush(suexec_log); return -EACCES; } p = strrchr(cgi_filename, '/'); if (!p) { + print_time_suexec_log(); + fprintf(suexec_log, "Unable to / in %s\n", cgi_filename); + fflush(suexec_log); return -EACCES; } - len_parent = p - cgi_filename; - - parent = malloc(len_parent); - if (!parent) { - return -EACCES; - } - - strncpy(parent, cgi_filename, len_parent); - - if (lstat(parent, &pls) < 0) { + *p = 0; + if (lstat(cgi_filename, &pls) < 0) { + print_time_suexec_log(); + fprintf(suexec_log, "Unable to stat %s\n", cgi_filename); + fflush(suexec_log); goto err_parent; } else if (!S_ISDIR(pls.st_mode) && S_ISLNK(pls.st_mode)) { - if (stat(parent, &pls) < 0) { + if (stat(cgi_filename, &pls) < 0) { + print_time_suexec_log(); + fprintf(suexec_log, "Unable to stat %s\n", cgi_filename); + fflush(suexec_log); goto err_parent; } else if (!S_ISDIR(pls.st_mode)) { + print_time_suexec_log(); + fprintf(suexec_log, "%s is not a directory\n", cgi_filename); + fflush(suexec_log); goto err_parent; } } - free(parent); /* Make sure directory is not writable by anyone else */ - if ((pls.st_mode & S_IWGRP) || (pls.st_mode & S_IWOTH)) { - return -EACCES; + if (pls.st_mode & S_IWOTH) { + print_time_suexec_log(); + fprintf(suexec_log, "Parent directory %s is writable by anyone.", cgi_filename); + *p = '/'; + fprintf(suexec_log, " Don't execute %s\n", cgi_filename); + *p = 0; + fflush(suexec_log); + goto err_parent; } if (pls.st_gid != ls->st_gid || pls.st_uid != ls->st_uid) { - return -EACCES; + print_time_suexec_log(); + fprintf(suexec_log, "%s is owned by user %d group %d. Expected user owner %d group owner %d\n", cgi_filename, pls.st_uid, pls.st_gid, ls->st_uid, ls->st_gid); + fflush(suexec_log); + goto err_parent; } + *p = '/'; return 0; err_parent: - free(parent); + *p = '/'; return -EACCES; } @@ -643,6 +703,8 @@ static void handle_fcgi_request(void) char *p; pid_t pid; struct stat ls; + struct group *group; + struct passwd *user; struct fcgi_context fc; @@ -702,13 +764,24 @@ static void handle_fcgi_request(void) cgi_error("403 Forbidden", "Cannot suexec script because the permissions are incorrect", filename); } if (setgid(ls.st_gid) < 0) { + print_time_suexec_log(); + fprintf(suexec_log, "Cannot change process to script group owner %d to execute %s\n", ls.st_gid, filename); + fflush(suexec_log); cgi_error("403 Forbidden", "Cannot change to script group owner", filename); } if (setuid(ls.st_uid) < 0) { + print_time_suexec_log(); + fprintf(suexec_log, "Cannot change process to script user owner %d to execute %s\n", ls.st_uid, filename); + fflush(suexec_log); cgi_error("403 Forbidden", "Cannot change to script user owner", filename); } } + print_time_suexec_log(); + group = getgrgid(ls.st_gid); + user = getpwuid(ls.st_uid); + fprintf(suexec_log, "uid: (%d/%s) gid: (%d/%s) cmd: %s\n", ls.st_uid, user->pw_name, ls.st_gid, group->gr_name, filename); + fflush(suexec_log); execl(filename, filename, (void *)NULL); cgi_error("502 Bad Gateway", "Cannot execute script", filename); @@ -1008,6 +1081,14 @@ int main(int argc, char **argv) } } + if (use_suexec) { + suexec_log = fopen("/var/log/fcgiwrap_suexec.log", "ab"); + if (suexec_log == NULL) { + fprintf(stderr, "An error occurred while opening /var/log/fcgiwrap_suexec.log '%s'", strerror(errno)); + return 1; + } + } + prefork(nchildren); fcgiwrap_main(); @@ -1022,5 +1103,8 @@ int main(int argc, char **argv) } } } + if (suexec_log) { + fclose(suexec_log); + } return 0; } From 8a0f7214632dde91d44d393f8da10325b4d4187c Mon Sep 17 00:00:00 2001 From: Dan Gaidula Date: Fri, 14 Oct 2016 13:01:11 -0400 Subject: [PATCH 7/7] fix for non-suexec error --- fcgiwrap.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fcgiwrap.c b/fcgiwrap.c index b85c12d..ba6c572 100644 --- a/fcgiwrap.c +++ b/fcgiwrap.c @@ -775,13 +775,13 @@ static void handle_fcgi_request(void) fflush(suexec_log); cgi_error("403 Forbidden", "Cannot change to script user owner", filename); } + print_time_suexec_log(); + group = getgrgid(ls.st_gid); + user = getpwuid(ls.st_uid); + fprintf(suexec_log, "uid: (%d/%s) gid: (%d/%s) cmd: %s\n", ls.st_uid, user->pw_name, ls.st_gid, group->gr_name, filename); + fflush(suexec_log); } - print_time_suexec_log(); - group = getgrgid(ls.st_gid); - user = getpwuid(ls.st_uid); - fprintf(suexec_log, "uid: (%d/%s) gid: (%d/%s) cmd: %s\n", ls.st_uid, user->pw_name, ls.st_gid, group->gr_name, filename); - fflush(suexec_log); execl(filename, filename, (void *)NULL); cgi_error("502 Bad Gateway", "Cannot execute script", filename);