This patch is based on http://kyberdigi.cz/projects/execdir/ but instead trying to sanitize the exec command string inside PHP this job is handled by bash and a special restricted PHP-mode which strips off all path informations. This patch depends on: - bash restricted php mode patch https://manuel.mausz.at/coding/patches/php/bash-4.2-restricted-php.patch No warranty! diff -Naur a/ext/standard/exec.c b/ext/standard/exec.c --- a/ext/standard/exec.c 2024-09-24 17:59:52.000000000 +0200 +++ b/ext/standard/exec.c 2024-09-27 14:57:40.554136412 +0200 @@ -114,21 +114,32 @@ FILE *fp; char *buf; int pclose_return; - char *b, *d=NULL; + char *cmd_p, *b, *d=NULL; php_stream *stream; size_t buflen, bufl = 0; #if PHP_SIGCHILD void (*sig_handler)() = NULL; #endif + if (PG(exec_dir) && strlen(PG(exec_dir))) { + zend_string *cmd_str = zend_string_init(cmd, strlen(cmd), 0); + zend_string *cmd_esc = php_escape_shell_arg(cmd_str); + spprintf(&d, 0, "PATH=%s /bin/bash --php -rc %s", PG(exec_dir), ZSTR_VAL(cmd_esc)); + zend_string_release(cmd_esc); + zend_string_release(cmd_str); + cmd_p = d; + } else { + cmd_p = cmd; + } + #if PHP_SIGCHILD sig_handler = signal (SIGCHLD, SIG_DFL); #endif #ifdef PHP_WIN32 - fp = VCWD_POPEN(cmd, "rb"); + fp = VCWD_POPEN(cmd_p, "rb"); #else - fp = VCWD_POPEN(cmd, "r"); + fp = VCWD_POPEN(cmd_p, "r"); #endif if (!fp) { php_error_docref(NULL, E_WARNING, "Unable to fork [%s]", cmd); @@ -508,7 +519,7 @@ PHP_FUNCTION(shell_exec) { FILE *in; - char *command; + char *command, *command_p; size_t command_len; zend_string *ret; php_stream *stream; @@ -522,14 +533,26 @@ RETURN_THROWS(); } + if (PG(exec_dir) && strlen(PG(exec_dir))) { + zend_string *cmd_str = zend_string_init(command, command_len, 0); + zend_string *cmd_esc = php_escape_shell_arg(cmd_str); + spprintf(&command_p, 0, "PATH=%s /bin/bash --php -rc %s", PG(exec_dir), ZSTR_VAL(cmd_esc)); + zend_string_release(cmd_esc); + zend_string_release(cmd_str); + } else { + command_p = estrdup(command); + } + #ifdef PHP_WIN32 - if ((in=VCWD_POPEN(command, "rt"))==NULL) { + if ((in=VCWD_POPEN(command_p, "rt"))==NULL) { #else - if ((in=VCWD_POPEN(command, "r"))==NULL) { + if ((in=VCWD_POPEN(command_p, "r"))==NULL) { #endif php_error_docref(NULL, E_WARNING, "Unable to execute '%s'", command); + efree(command_p); RETURN_FALSE; } + efree(command_p); stream = php_stream_fopen_from_pipe(in, "rb"); ret = php_stream_copy_to_mem(stream, PHP_STREAM_COPY_ALL, 0); diff -Naur a/ext/standard/file.c b/ext/standard/file.c --- a/ext/standard/file.c 2024-09-24 17:59:52.000000000 +0200 +++ b/ext/standard/file.c 2024-09-27 14:57:40.555136404 +0200 @@ -28,6 +28,7 @@ #include "ext/standard/basic_functions.h" #include "php_ini.h" #include "zend_smart_str.h" +#include "ext/standard/exec.h" #include #include @@ -813,7 +814,18 @@ RETURN_THROWS(); } - fp = VCWD_POPEN(command, posix_mode); + if (PG(exec_dir) && strlen(PG(exec_dir))) { + zend_string *cmd_str = zend_string_init(command, command_len, 0); + zend_string *cmd_esc = php_escape_shell_arg(cmd_str); + char *tmp; + spprintf(&tmp, 0, "PATH=%s /bin/bash --php -rc %s", PG(exec_dir), ZSTR_VAL(cmd_esc)); + fp = VCWD_POPEN(tmp, posix_mode); + efree(tmp); + zend_string_release(cmd_esc); + zend_string_release(cmd_str); + } else { + fp = VCWD_POPEN(command, posix_mode); + } if (!fp) { php_error_docref2(NULL, command, posix_mode, E_WARNING, "%s", strerror(errno)); efree(posix_mode); diff -Naur a/ext/standard/filestat.c b/ext/standard/filestat.c --- a/ext/standard/filestat.c 2024-09-24 17:59:52.000000000 +0200 +++ b/ext/standard/filestat.c 2024-09-27 14:57:40.556136395 +0200 @@ -734,6 +734,7 @@ zend_stat_t *stat_sb = &ssb.sb; int flags = 0, rmask=S_IROTH, wmask=S_IWOTH, xmask=S_IXOTH; /* access rights defaults to other */ const char *local = NULL; + char local_safe[MAXPATHLEN]; php_stream_wrapper *wrapper = NULL; if (IS_ACCESS_CHECK(type)) { @@ -745,7 +746,7 @@ } if ((wrapper = php_stream_locate_url_wrapper(ZSTR_VAL(filename), &local, 0)) == &php_plain_files_wrapper - && php_check_open_basedir(local)) { + && type != FS_IS_X && php_check_open_basedir(local)) { RETURN_FALSE; } @@ -775,9 +776,25 @@ break; #endif #ifdef X_OK - case FS_IS_X: - RETURN_BOOL(VCWD_ACCESS(file_path_to_check, X_OK) == 0); + case FS_IS_X: { + bool xok = (VCWD_ACCESS(file_path_to_check, X_OK) == 0); + if (!xok && PG(exec_dir) && strlen(PG(exec_dir))) { + if (strstr(file_path_to_check, "..")) { + RETURN_FALSE; + } else { + char *b = strrchr(file_path_to_check, PHP_DIR_SEPARATOR); + if (b) { + snprintf(local_safe, MAXPATHLEN, "%s%s", PG(exec_dir), b); + } else { + snprintf(local_safe, MAXPATHLEN, "%s%c%s", PG(exec_dir), PHP_DIR_SEPARATOR, file_path_to_check); + } + file_path_to_check = (char *)&local_safe; + } + xok = (VCWD_ACCESS(file_path_to_check, X_OK) == 0); + } + RETURN_BOOL(xok); break; + } #endif } } diff -Naur a/ext/standard/proc_open.c b/ext/standard/proc_open.c --- a/ext/standard/proc_open.c 2024-09-24 17:59:52.000000000 +0200 +++ b/ext/standard/proc_open.c 2024-09-27 14:58:20.151810732 +0200 @@ -728,7 +728,17 @@ zend_string *command = NULL; int i = 0; - *argv = safe_emalloc(sizeof(char *), num_elems + 1, 0); + *argv = safe_emalloc(sizeof(char *), num_elems + 1 + 5, 0); + + if (PG(exec_dir) && strlen(PG(exec_dir))) { + command = zend_string_init("/bin/bash", sizeof("/bin/bash") - 1, 0); + (*argv)[i++] = estrdup("/bin/bash"); + (*argv)[i++] = estrdup("--php"); + (*argv)[i++] = estrdup("-rc"); + (*argv)[i++] = estrdup("exec \"$@\""); + (*argv)[i++] = estrdup("bash"); + (*argv)[i] = NULL; + } ZEND_HASH_FOREACH_VAL(array, arg_zv) { zend_string *arg_str = get_valid_arg_string(arg_zv, i + 1); @@ -1222,6 +1232,15 @@ } #endif + zval envpath; + if (PG(exec_dir) && strlen(PG(exec_dir))) { + if (!environment || zend_hash_num_elements(Z_ARRVAL_P(environment)) == 0) { + array_init_size(&envpath, 1); + environment = &envpath; + } + add_assoc_string(environment, "PATH", PG(exec_dir)); + } + if (environment) { env = _php_array_to_envp(environment); } @@ -1347,9 +1366,15 @@ if (argv) { r = posix_spawnp(&child, ZSTR_VAL(command_str), &factions, NULL, argv, (env.envarray ? env.envarray : environ)); } else { - r = posix_spawn(&child, "/bin/sh" , &factions, NULL, - (char * const[]) {"sh", "-c", ZSTR_VAL(command_str), NULL}, - env.envarray ? env.envarray : environ); + if (PG(exec_dir) && strlen(PG(exec_dir))) { + r = posix_spawn(&child, "/bin/bash" , &factions, NULL, + (char * const[]) {"bash", "--php", "-rc", ZSTR_VAL(command_str), NULL}, + env.envarray ? env.envarray : environ); + } else { + r = posix_spawn(&child, "/bin/sh" , &factions, NULL, + (char * const[]) {"sh", "-c", ZSTR_VAL(command_str), NULL}, + env.envarray ? env.envarray : environ); + } } posix_spawn_file_actions_destroy(&factions); if (r != 0) { @@ -1382,10 +1407,14 @@ } execvp(ZSTR_VAL(command_str), argv); } else { - if (env.envarray) { - execle("/bin/sh", "sh", "-c", ZSTR_VAL(command_str), NULL, env.envarray); + if (PG(exec_dir) && strlen(PG(exec_dir))) { + execle("/bin/bash", "bash", "--php", "-rc", ZSTR_VAL(command_str), NULL, env.envarray); } else { - execl("/bin/sh", "sh", "-c", ZSTR_VAL(command_str), NULL); + if (env.envarray) { + execle("/bin/sh", "sh", "-c", ZSTR_VAL(command_str), NULL, env.envarray); + } else { + execl("/bin/sh", "sh", "-c", ZSTR_VAL(command_str), NULL); + } } } diff -Naur a/main/main.c b/main/main.c --- a/main/main.c 2024-09-27 14:37:57.841234655 +0200 +++ b/main/main.c 2024-09-27 14:57:40.557136387 +0200 @@ -824,6 +824,7 @@ STD_PHP_INI_ENTRY("syslog.facility", "LOG_USER", PHP_INI_SYSTEM, OnSetFacility, syslog_facility, php_core_globals, core_globals) STD_PHP_INI_ENTRY("syslog.ident", "php", PHP_INI_SYSTEM, OnUpdateString, syslog_ident, php_core_globals, core_globals) STD_PHP_INI_ENTRY("syslog.filter", "no-ctrl", PHP_INI_ALL, OnSetLogFilter, syslog_filter, php_core_globals, core_globals) + STD_PHP_INI_ENTRY("exec_dir", NULL, PHP_INI_SYSTEM, OnUpdateString, exec_dir, php_core_globals, core_globals) PHP_INI_END() /* }}} */ diff -Naur a/main/php_globals.h b/main/php_globals.h --- a/main/php_globals.h 2024-09-24 17:59:52.000000000 +0200 +++ b/main/php_globals.h 2024-09-27 14:57:40.557136387 +0200 @@ -172,6 +172,8 @@ char *syslog_ident; zend_long syslog_filter; zend_long error_log_mode; + + char *exec_dir; }; diff -Naur a/php.ini-development b/php.ini-development --- a/php.ini-development 2024-09-24 17:59:52.000000000 +0200 +++ b/php.ini-development 2024-09-27 14:57:40.558136379 +0200 @@ -317,6 +317,10 @@ ; https://php.net/open-basedir ;open_basedir = +; Only executables located in the exec_dir will be allowed to be executed +; via the exec family of functions. +exec_dir = + ; This directive allows you to disable certain functions. ; It receives a comma-delimited list of function names. ; https://php.net/disable-functions diff -Naur a/php.ini-production b/php.ini-production --- a/php.ini-production 2024-09-24 17:59:52.000000000 +0200 +++ b/php.ini-production 2024-09-27 14:57:40.559136371 +0200 @@ -317,6 +317,10 @@ ; https://php.net/open-basedir ;open_basedir = +; Only executables located in the exec_dir will be allowed to be executed +; via the exec family of functions. +exec_dir = + ; This directive allows you to disable certain functions. ; It receives a comma-delimited list of function names. ; https://php.net/disable-functions