<?PHP /** * Provides class MD_JAIL. */ declare(strict_types = 1); /** * A class that, once initialized, forces the programmer to make security instructions implicit. * If an object of the class has been created, not specifying security instructions * leads to an error. * A restriction on basic file operations is not practical in an md context because of * the way transations are loaded through MDTlLoader. */ final class MD_JAIL { const STATUS_NONE = 0; const STATUS_STARTED = 1; const STATUS_SPECIFIED = 2; // Determines that everything is fine. /** @var integer */ private int $_status = self::STATUS_NONE; /** * @var integer * Maximum execution time in seconds. */ public int $max_execution_time; /** * @var string[] * Specifies which paths may be used by this script. */ private array $_open_basedir = []; /** * @var string * Specifies the maximum RAM the script may use. */ public string $memory_limit; /** * Static function providing an advisory on how to harden the php.ini or * .user.ini. * * @param array{shell_access_whitelist: string[], sys_function_whitelist: string[], file_function_whitelist: string[], file_uploads: bool, allow_url_fopen: bool, max_input_vars: integer, max_input_nesting_level: integer, post_max_size: string, curl: bool} $requested_resources Requested resources. * * @return string */ public static function check_server_setup(array $requested_resources):string { # ini_set("open_basedir", __DIR__ . "/../../"); $disable_functions = [ "dl", # Loads a PHP extension at runtime "opcache_get_status", # Gets opcache status "phpinfo", # For obvious reasons "parse_ini_file", # For obvious reasons "show_source", "highlight_file", # Aliases; serve the content of a PHP file "php_uname", # Returns information about the operating system PHP is running on "phpcredits", # Credits page of PHP authors "php_strip_whitespace", # Return PHP source with stripped comments and whitespace "popen", "pclose", # Similar to fopen, but capable of shell access "virtual", # Perform an Apache sub-request ]; $disable_aliases = [ "ini_set" => "ini_alter", # Alias of ini_set(), ini_set is preferred "disk_free_space" => "diskfreespace", # Alias of disk_free_space() "PHP_SAPI" => "php_sapi_name", # Alias of constant PHP_SAPI "stream_set_write_buffer" => "set_file_buffer", # Alias of stream_set_write_buffer() ]; $shell_access_functions = array_diff(["exec", "passthru", "proc_close", "proc_get_status", "proc_nice", "proc_open", "proc_terminate", "shell_exec", "system"], $requested_resources['shell_access_whitelist']); $sys_functions = array_diff(["get_current_user", "getmyuid", "getmygid", "getmypid", "getmyinode", "getlastmod", "getenv", "putenv"], $requested_resources['sys_function_whitelist']); $file_functions = array_diff(["chgrp", "chgrp", "lchgrp", "lchown", "link", "linkinfo", "symlink"], $requested_resources['file_function_whitelist']); if ($requested_resources['curl'] === false) { $disable_functions[] = "curl_init"; $disable_functions[] = "curl_exec"; $disable_functions[] = "curl_multi_init"; $disable_functions[] = "curl_multi_exec"; } $disabledWOAliases = array_merge($disable_functions, $shell_access_functions, $sys_functions, $file_functions); $output = '## php.ini' . PHP_EOL . PHP_EOL; $output .= PHP_EOL . "php_value[disable_functions] = \"" . implode(", ", array_merge($disabledWOAliases, $disable_aliases)) . "\""; if ($requested_resources['file_uploads'] === false) { $output .= PHP_EOL . "php_value[file_uploads] = 0"; } if ($requested_resources['allow_url_fopen'] === false) { $output .= PHP_EOL . "php_value[allow_url_fopen] = 0"; } $output .= PHP_EOL . PHP_EOL . '## .user.ini' . PHP_EOL; if ($requested_resources['file_uploads'] === false) { $output .= PHP_EOL . "upload_max_filesize = 1"; } if ($requested_resources['max_input_vars'] != ini_get("max_input_vars")) { $output .= PHP_EOL . "max_input_vars = " . $requested_resources['max_input_vars']; } if ($requested_resources['max_input_nesting_level'] != ini_get("max_input_nesting_level")) { $output .= PHP_EOL . "max_input_nesting_level = " . $requested_resources['max_input_nesting_level']; } if ($requested_resources['post_max_size'] != ini_get("post_max_size")) { $output .= PHP_EOL . "post_max_size = " . $requested_resources['post_max_size']; } $output .= PHP_EOL . PHP_EOL . '## PHPStan Directives' . PHP_EOL . PHP_EOL . " disallowedFunctionCalls:" . PHP_EOL; foreach ($disable_aliases as $to_keep => $disabled) { $output .= ' - function: \'' . $disabled . '()\' message: \'use ' . $to_keep . ' instead\''; } foreach ($disabledWOAliases as $disabled) { $output .= ' - function: \'' . $disabled . '()\''; } return $output; } /** * Registers an additional accessible directory for open_basedir. * * @param string $dir Directory to register. * * @return void */ public function register_accessible_dir(string $dir):void { $this->_open_basedir[] = $dir; } /** * Applies the memory limit setting. * * @return void */ private function _apply_memory_limit():void { if (!isset($this->memory_limit)) { throw new MDJailSecurityOptionNotSetException("It has not been specified, which memory limit the script should hold. Set MD_JAIL->memory_limit = string."); } if (ini_set("memory_limit", $this->memory_limit) === false) { throw new Exception('Failed to change memory_limit to ' . $this->memory_limit); } } /** * Applies the maximum execution time setting. * * @return void */ private function _apply_time_limit():void { if (!isset($this->max_execution_time)) { throw new MDJailSecurityOptionNotSetException("It has not been specified, which maximum execution time the script should hold. Set MD_JAIL->max_execution_time = integer."); } if (set_time_limit($this->max_execution_time) === false) { throw new Exception('Failed to change max_execution_time to ' . $this->max_execution_time); } } /** * Applies basedir restrictions. * * @return void */ private function _apply_basedir_restriction():void { if (empty($this->_open_basedir)) { throw new MDJailSecurityOptionNotSetException("It has not been specified, which memory limit the script should hold. Set MD_JAIL->open_basedir = string."); } if (ini_set("open_basedir", implode(':', $this->_open_basedir)) === false) { throw new Exception('Failed to set open_basedir restrictions'); } } /** * Enforces security options previously set. * * @return void */ public function enforce():void { // Special instructions on CLI, so as to not disturb PHPUnit if (PHP_SAPI === 'cli') { if (!isset($this->memory_limit)) { throw new MDJailSecurityOptionNotSetException("It has not been specified, which memory limit the script should hold. Set MD_JAIL->memory_limit = string."); } if (!isset($this->max_execution_time)) { throw new MDJailSecurityOptionNotSetException("It has not been specified, which maximum execution time the script should hold. Set MD_JAIL->max_execution_time = integer."); } $this->_status = self::STATUS_SPECIFIED; $this->__destruct(); } $this->_apply_memory_limit(); $this->_apply_time_limit(); // Set accessible file paths $this->_apply_basedir_restriction(); $this->_status = self::STATUS_SPECIFIED; $this->__destruct(); } /** * Setup function. Registers a shutdown function that throws an error * if the security specifications have not been made. * * @return void */ public function __construct() { $this->_status = self::STATUS_STARTED; } public function __destruct() { if ($this->_status !== self::STATUS_SPECIFIED) { echo "Security specifications need to be set."; if (!isset($this->memory_limit)) { echo "Set memory limit"; } if (!isset($this->max_execution_time)) { echo "Set max_execution_time"; } if (empty($this->_open_basedir)) { echo "Set open_basedir"; } throw new MDJailSecurityOptionNotSetException("Security specifications need to be set."); } } }