From 427d432a56df94d69a11cc438b08adb070615005 Mon Sep 17 00:00:00 2001
From: Alexander Chernyakhovsky <achernya@mit.edu>
Date: Fri, 3 May 2013 21:38:58 -0400
Subject: [PATCH] Add scripts-specific support to suexec

This patch make suexec aware of static-cat, Scripts' tool to serve
static content out of AFS.  Specifically, this introduces a whitelist
of extensions for which suexec is supposed to invoke static-cat as a
content-handler.

Additionally, this patch also sets JAVA_TOOL_OPTIONS, to allow the JVM
to start up in Scripts' limited memory environment.

Furthermore, this patch deals with some of suexec's paranoia being
incorrect in an AFS world, by ignoring some of the irrelevant stat
results.

Finally, add support for invoking php-cgi for php files, in a safe
manner that will strip arguments passed by Apache to php-cgi.
---
 configure.in     |   4 ++
 support/suexec.c | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 173 insertions(+), 3 deletions(-)

diff --git a/configure.in b/configure.in
index 811aace..a95349f 100644
--- a/configure.in
+++ b/configure.in
@@ -721,6 +721,10 @@ AC_ARG_WITH(suexec-userdir,
 APACHE_HELP_STRING(--with-suexec-userdir,User subdirectory),[
   AC_DEFINE_UNQUOTED(AP_USERDIR_SUFFIX, "$withval", [User subdirectory] ) ] )
 
+AC_ARG_WITH(suexec-trusteddir,
+APACHE_HELP_STRING(--with-suexec-trusteddir,Trusted SuExec directory),[
+  AC_DEFINE_UNQUOTED(AP_TRUSTED_DIRECTORY, "$withval", [Trusted SuExec directory] ) ] )
+
 AC_ARG_WITH(suexec-docroot,
 APACHE_HELP_STRING(--with-suexec-docroot,SuExec root directory),[
   AC_DEFINE_UNQUOTED(AP_DOC_ROOT, "$withval", [SuExec root directory] ) ] )
diff --git a/support/suexec.c b/support/suexec.c
index 32e7320..3a4d802 100644
--- a/support/suexec.c
+++ b/support/suexec.c
@@ -30,6 +30,9 @@
  *
  */
 
+#define STATIC_CAT_PATH "/usr/bin/static-cat"
+#define PHP_PATH "/usr/bin/php-cgi"
+
 #include "apr.h"
 #include "ap_config.h"
 #include "suexec.h"
@@ -92,6 +95,7 @@ static const char *const safe_env_lst[] =
 {
     /* variable name starts with */
     "HTTP_",
+    "HTTPS_",
     "SSL_",
 
     /* variable name is */
@@ -268,9 +272,108 @@ static void clean_env(void)
     environ = cleanenv;
 }
 
+static const char *static_extensions[] = {
+    "html",
+    "css",
+    "gif",
+    "jpg",
+    "png",
+    "htm",
+    "jpeg",
+    "js",
+    "ico",
+    "xml",
+    "xsl",
+    "tiff",
+    "tif",
+    "tgz",
+    "tar",
+    "jar",
+    "zip",
+    "pdf",
+    "ps",
+    "doc",
+    "xls",
+    "ppt",
+    "dot",
+    "docx",
+    "dotx",
+    "docm",
+    "dotm",
+    "xlt",
+    "xla",
+    "xlsx",
+    "xltx",
+    "xlsm",
+    "xltm",
+    "xlam",
+    "xlsb",
+    "pot",
+    "pps",
+    "ppa",
+    "pptx",
+    "potx",
+    "ppsx",
+    "ppam",
+    "pptm",
+    "potm",
+    "ppsm",
+    "swf",
+    "mp3",
+    "mov",
+    "wmv",
+    "mpg",
+    "mpeg",
+    "avi",
+    "il",
+    "xhtml",
+    "svg",
+    "xaml",
+    "xap",
+    "wav",
+    "mid",
+    "midi",
+    "ttf",
+    "otf",
+    "odc",
+    "odb",
+    "odf",
+    "odg",
+    "otg",
+    "odi",
+    "odp",
+    "otp",
+    "ods",
+    "ots",
+    "odt",
+    "odm",
+    "ott",
+    "oth",
+    NULL
+};
+
+static int is_static_extension(const char *file)
+{
+    const char *extension = strrchr(file, '.');
+    const char **p;
+    if (extension == NULL) return 0;
+    for (p = static_extensions; *p; ++p) {
+        if (strcasecmp(extension + 1, *p) == 0) return 1;
+    }
+    return 0;
+}
+
+static int is_php_extension(const char *file)
+{
+    const char *extension = strrchr(file, '.');
+    if (extension == NULL) return 0;
+    return strcmp(extension + 1, "php") == 0;
+}
+
 int main(int argc, char *argv[])
 {
     int userdir = 0;        /* ~userdir flag             */
+    int trusteddir = 0;     /* TRUSTED_DIRECTORY flag    */
     uid_t uid;              /* user information          */
     gid_t gid;              /* target group placeholder  */
     char *target_uname;     /* target user name          */
@@ -290,6 +393,7 @@ int main(int argc, char *argv[])
      * Start with a "clean" environment
      */
     clean_env();
+    setenv("JAVA_TOOL_OPTIONS", "-Xmx128M", 1); /* scripts.mit.edu local hack */
 
     /*
      * Check existence/validity of the UID of the user
@@ -373,6 +477,20 @@ int main(int argc, char *argv[])
 #endif /*_OSD_POSIX*/
 
     /*
+     * First check if this is an absolute path to the directory
+     * of trusted executables. These are supposed to be security
+     * audited to check parameters and validity on their own...
+     */
+    if (strstr(cmd, AP_TRUSTED_DIRECTORY) == cmd) {
+        if (strstr(cmd, "/../") != NULL) {
+            log_err("invalid command (%s)\n", cmd);
+            exit(104);
+        }
+        trusteddir = 1;
+        goto TRUSTED_DIRECTORY;
+    }
+
+    /*
      * Check for a leading '/' (absolute path) in the command to be executed,
      * or attempts to back up out of the current directory,
      * to protect against attacks.  If any are
@@ -394,6 +512,7 @@ int main(int argc, char *argv[])
         userdir = 1;
     }
 
+TRUSTED_DIRECTORY:
     /*
      * Error out if the target username is invalid.
      */
@@ -482,7 +601,7 @@ int main(int argc, char *argv[])
      * Error out if attempt is made to execute as root or as
      * a UID less than AP_UID_MIN.  Tsk tsk.
      */
-    if ((uid == 0) || (uid < AP_UID_MIN)) {
+    if ((uid == 0) || (uid < AP_UID_MIN && uid != 102)) { /* uid 102 = signup  */
         log_err("cannot run as forbidden uid (%lu/%s)\n", (unsigned long)uid, cmd);
         exit(107);
     }
@@ -514,6 +633,7 @@ int main(int argc, char *argv[])
         log_err("failed to setuid (%lu: %s)\n", (unsigned long)uid, cmd);
         exit(110);
     }
+    setenv("HOME", target_homedir, 1);
 
     /*
      * Get the current working directory, as well as the proper
@@ -536,6 +656,21 @@ int main(int argc, char *argv[])
             log_err("cannot get docroot information (%s)\n", target_homedir);
             exit(112);
         }
+        size_t expected_len = strlen(target_homedir)+1+strlen(AP_USERDIR_SUFFIX)+1;
+        char *expected = malloc(expected_len);
+        snprintf(expected, expected_len, "%s/%s", target_homedir, AP_USERDIR_SUFFIX);
+        if (strncmp(cwd, expected, expected_len-1) != 0) {
+            log_err("error: file's directory not a subdirectory of user's home directory (%s, %s)\n", cwd, expected);
+            exit(114);
+        }
+    }
+    else if (trusteddir) {
+        if (((chdir(AP_TRUSTED_DIRECTORY)) != 0) ||
+            ((getcwd(dwd, AP_MAXPATH)) == NULL) |
+            ((chdir(cwd)) != 0)) {
+            log_err("cannot get docroot information (%s)\n", AP_TRUSTED_DIRECTORY);
+            exit(112);
+        }
     }
     else {
         if (((chdir(AP_DOC_ROOT)) != 0) ||
@@ -562,15 +697,17 @@ int main(int argc, char *argv[])
     /*
      * Error out if cwd is writable by others.
      */
+#if 0
     if ((dir_info.st_mode & S_IWOTH) || (dir_info.st_mode & S_IWGRP)) {
         log_err("directory is writable by others: (%s)\n", cwd);
         exit(116);
     }
+#endif
 
     /*
      * Error out if we cannot stat the program.
      */
-    if (((lstat(cmd, &prg_info)) != 0) || (S_ISLNK(prg_info.st_mode))) {
+    if (((lstat(cmd, &prg_info)) != 0) /*|| (S_ISLNK(prg_info.st_mode))*/) {
         log_err("cannot stat program: (%s)\n", cmd);
         exit(117);
     }
@@ -578,10 +715,12 @@ int main(int argc, char *argv[])
     /*
      * Error out if the program is writable by others.
      */
+#if 0
     if ((prg_info.st_mode & S_IWOTH) || (prg_info.st_mode & S_IWGRP)) {
         log_err("file is writable by others: (%s/%s)\n", cwd, cmd);
         exit(118);
     }
+#endif
 
     /*
      * Error out if the file is setuid or setgid.
@@ -595,6 +734,7 @@ int main(int argc, char *argv[])
      * Error out if the target name/group is different from
      * the name/group of the cwd or the program.
      */
+#if 0
     if ((uid != dir_info.st_uid) ||
         (gid != dir_info.st_gid) ||
         (uid != prg_info.st_uid) ||
@@ -606,12 +746,14 @@ int main(int argc, char *argv[])
                 (unsigned long)prg_info.st_uid, (unsigned long)prg_info.st_gid);
         exit(120);
     }
+#endif
     /*
      * Error out if the program is not executable for the user.
      * Otherwise, she won't find any error in the logs except for
      * "[error] Premature end of script headers: ..."
      */
-    if (!(prg_info.st_mode & S_IXUSR)) {
+    if (!is_static_extension(cmd) && !is_php_extension(cmd) &&
+        !(prg_info.st_mode & S_IXUSR)) {
         log_err("file has no execute permission: (%s/%s)\n", cwd, cmd);
         exit(121);
     }
@@ -660,6 +802,30 @@ int main(int argc, char *argv[])
     /*
      * Execute the command, replacing our image with its own.
      */
+    if (is_static_extension(cmd)) {
+        if (setenv("PATH_TRANSLATED", cmd, 1) != 0) {
+            log_err("setenv failed\n");
+            exit(255);
+        }
+        execl(STATIC_CAT_PATH, STATIC_CAT_PATH, (const char *)NULL);
+        log_err("(%d)%s: static-cat exec failed (%s)\n", errno, strerror(errno), STATIC_CAT_PATH);
+        exit(255);
+    }
+    if (is_php_extension(cmd)) {
+        setenv("PHPRC", ".", 1);
+        argv[1] = PHP_PATH;
+        argv[2] = "-f";
+        /*
+         * argv[3] is the command to run. argv[4] is either an argument or
+         * already null. We don't want to pass any arguments through from
+         * Apache (since they're untrusted), so we chop off the remainder
+         * of argv here.
+         */
+        argv[4] = 0;
+        execv(PHP_PATH, &argv[1]);
+        log_err("(%d)%s: php exec failed (%s)\n", errno, strerror(errno), argv[1]);
+        exit(255);
+    }
 #ifdef NEED_HASHBANG_EMUL
     /* We need the #! emulation when we want to execute scripts */
     {
-- 
1.8.1.2

