From faa938d64f03034c33991bc21f73bf32d7924b8d Mon Sep 17 00:00:00 2001 From: Simon Arlott Date: Tue, 19 Apr 2016 18:42:03 +0100 Subject: [PATCH] sapi: fpm: Add security.exec_basedir per-pool configuration option When a single webserver is used to access multiple FPM pools (one per user), it can become possible for one vhost to execute scripts using the pool of another vhost (e.g. Apache SetHandler in .htaccess). To prevent this, it is necessary to restrict which files a pool can execute for a request (it doesn't matter what that file then does as long as it is under control of the correct user). While chroot does achieve this it then becomes inconvenient to allow access to any external resources (e.g. database sockets, libraries, applications) without inadvertently allowing third party users to make files available within the chroot. This adds a per-pool configuration option "security.exec_basedir": Limits the directory which can be used to execute the request script. This can be used to ensure that pools defined for specific users can only be used to execute scripts under the control of those users. This value must be defined as an absolute path and end with a '/'. --- sapi/fpm/fpm/fpm_conf.c | 17 +++++++++++++++++ sapi/fpm/fpm/fpm_conf.h | 1 + sapi/fpm/fpm/fpm_main.c | 6 ++++++ sapi/fpm/fpm/fpm_php.c | 20 ++++++++++++++++++++ sapi/fpm/fpm/fpm_php.h | 1 + sapi/fpm/www.conf.in | 8 ++++++++ 6 files changed, 53 insertions(+) diff --git a/sapi/fpm/fpm/fpm_conf.c b/sapi/fpm/fpm/fpm_conf.c index 9a619ce8871cb..2960c96780a21 100644 --- a/sapi/fpm/fpm/fpm_conf.c +++ b/sapi/fpm/fpm/fpm_conf.c @@ -153,6 +153,7 @@ static struct ini_value_parser_s ini_fpm_pool_options[] = { { "chdir", &fpm_conf_set_string, WPO(chdir) }, { "catch_workers_output", &fpm_conf_set_boolean, WPO(catch_workers_output) }, { "clear_env", &fpm_conf_set_boolean, WPO(clear_env) }, + { "security.exec_basedir", &fpm_conf_set_string, WPO(security_exec_basedir) }, { "security.limit_extensions", &fpm_conf_set_string, WPO(security_limit_extensions) }, #ifdef HAVE_APPARMOR { "apparmor_hat", &fpm_conf_set_string, WPO(apparmor_hat) }, @@ -654,6 +655,7 @@ int fpm_worker_pool_config_free(struct fpm_worker_pool_config_s *wpc) /* {{{ */ free(wpc->slowlog); free(wpc->chroot); free(wpc->chdir); + free(wpc->security_exec_basedir); free(wpc->security_limit_extensions); #ifdef HAVE_APPARMOR free(wpc->apparmor_hat); @@ -1016,6 +1018,20 @@ static int fpm_conf_process_all_pools() /* {{{ */ } } + /* security.exec_basedir */ + if (wp->config->security_exec_basedir) { + size_t path_len; + + fpm_evaluate_full_path(&wp->config->security_exec_basedir, wp, NULL, 0); + + path_len = strlen(wp->config->security_exec_basedir); + + if (!path_len || wp->config->security_exec_basedir[path_len - 1] != '/') { + zlog(ZLOG_ERROR, "[pool %s] the security.exec_basedir path '%s' must end with a '/'", wp->config->name, wp->config->security_exec_basedir); + return -1; + } + } + /* security.limit_extensions */ if (!wp->config->security_limit_extensions) { wp->config->security_limit_extensions = strdup(".php .phar"); @@ -1642,6 +1658,7 @@ static void fpm_conf_dump() /* {{{ */ zlog(ZLOG_NOTICE, "\tchdir = %s", STR2STR(wp->config->chdir)); zlog(ZLOG_NOTICE, "\tcatch_workers_output = %s", BOOL2STR(wp->config->catch_workers_output)); zlog(ZLOG_NOTICE, "\tclear_env = %s", BOOL2STR(wp->config->clear_env)); + zlog(ZLOG_NOTICE, "\tsecurity.exec_basedir = %s", wp->config->security_exec_basedir); zlog(ZLOG_NOTICE, "\tsecurity.limit_extensions = %s", wp->config->security_limit_extensions); for (kv = wp->config->env; kv; kv = kv->next) { diff --git a/sapi/fpm/fpm/fpm_conf.h b/sapi/fpm/fpm/fpm_conf.h index 540b22795df34..4b5abb0535db6 100644 --- a/sapi/fpm/fpm/fpm_conf.h +++ b/sapi/fpm/fpm/fpm_conf.h @@ -85,6 +85,7 @@ struct fpm_worker_pool_config_s { char *chdir; int catch_workers_output; int clear_env; + char *security_exec_basedir; char *security_limit_extensions; struct key_value_s *env; struct key_value_s *php_admin_values; diff --git a/sapi/fpm/fpm/fpm_main.c b/sapi/fpm/fpm/fpm_main.c index 940d6c788d925..d77201d98d63a 100644 --- a/sapi/fpm/fpm/fpm_main.c +++ b/sapi/fpm/fpm/fpm_main.c @@ -1931,6 +1931,12 @@ consult the installation file that came with this distribution, or visit \n\ goto fastcgi_request_done; } + if (UNEXPECTED(fpm_php_check_exec_basedir(SG(request_info).path_translated))) { + SG(sapi_headers).http_response_code = 403; + PUTS("Access denied.\n"); + goto fastcgi_request_done; + } + if (UNEXPECTED(fpm_php_limit_extensions(SG(request_info).path_translated))) { SG(sapi_headers).http_response_code = 403; PUTS("Access denied.\n"); diff --git a/sapi/fpm/fpm/fpm_php.c b/sapi/fpm/fpm/fpm_php.c index e20276974d15a..1e10499245790 100644 --- a/sapi/fpm/fpm/fpm_php.c +++ b/sapi/fpm/fpm/fpm_php.c @@ -21,6 +21,7 @@ #include "fpm_worker_pool.h" #include "zlog.h" +static char *exec_basedir = NULL; static char **limit_extensions = NULL; static int fpm_php_zend_ini_alter_master(char *name, int name_length, char *new_value, int new_value_length, int mode, int stage) /* {{{ */ @@ -221,6 +222,10 @@ int fpm_php_init_child(struct fpm_worker_pool_s *wp) /* {{{ */ return -1; } + if (wp->config->security_exec_basedir) { + exec_basedir = strdup(wp->config->security_exec_basedir); + } + if (wp->limit_extensions) { limit_extensions = wp->limit_extensions; } @@ -228,6 +233,21 @@ int fpm_php_init_child(struct fpm_worker_pool_s *wp) /* {{{ */ } /* }}} */ +int fpm_php_check_exec_basedir(char *path) /* {{{ */ +{ + if (!path || !exec_basedir) { + return 0; /* allowed by default */ + } + + if (strncmp(path, exec_basedir, strlen(exec_basedir)) == 0) { + return 0; /* allow as the exec base dir matches the path */ + } + + zlog(ZLOG_NOTICE, "Access to the script '%s' has been denied (see security.exec_basedir)", path); + return 1; +} +/* }}} */ + int fpm_php_limit_extensions(char *path) /* {{{ */ { char **p; diff --git a/sapi/fpm/fpm/fpm_php.h b/sapi/fpm/fpm/fpm_php.h index a2f2138d23f26..affbfe3477ed9 100644 --- a/sapi/fpm/fpm/fpm_php.h +++ b/sapi/fpm/fpm/fpm_php.h @@ -43,6 +43,7 @@ size_t fpm_php_content_length(void); void fpm_php_soft_quit(); int fpm_php_init_main(); int fpm_php_apply_defines_ex(struct key_value_s *kv, int mode); +int fpm_php_check_exec_basedir(char *path); int fpm_php_limit_extensions(char *path); char* fpm_php_get_string_from_table(zend_string *table, char *key); diff --git a/sapi/fpm/www.conf.in b/sapi/fpm/www.conf.in index 394e27819d935..16c82ad9ff526 100644 --- a/sapi/fpm/www.conf.in +++ b/sapi/fpm/www.conf.in @@ -12,6 +12,7 @@ ; - 'chdir' ; - 'php_values' ; - 'php_admin_values' +; - 'security.exec_basedir' ; When not set, the global prefix (or @php_fpm_prefix@) applies instead. ; Note: This directive can also be relative to the global prefix. ; Default Value: none @@ -370,6 +371,13 @@ pm.max_spare_servers = 3 ; Default Value: yes ;clear_env = no +; Limits the directory which can be used to execute the request script. +; This can be used to ensure that pools defined for specific users can +; only be used to execute scripts under the control of those users. +; This value must be defined as an absolute path and end with a '/'. +; Default Value: not set +;security.exec_basedir = + ; Limits the extensions of the main script FPM will allow to parse. This can ; prevent configuration mistakes on the web server side. You should only limit ; FPM to .php extensions to prevent malicious users to use other extensions to