* @link https://www.tassos.gr * @copyright Copyright © 2024 Tassos All Rights Reserved * @license GNU GPLv3 or later */ namespace NRFramework; defined('_JEXEC') or die; use Joomla\Registry\Registry; use Joomla\Filesystem\File; use Joomla\CMS\Factory; /** * Cleverly evaluate php code using a temporary file and without using the evil eval() PHP method */ class Executer { /** * The php code is going to be executed * * @var string */ private $php_code; /** * The data object passed as argument to function * * @var mixed */ private $payload; /** * Executer configuration * * @var object */ private $options; /** * Class constructor * * @param string $php_code The php code is going to be executed */ public function __construct($php_code = null, &$payload = null, $options = array()) { $this->setPhpCode($php_code); $this->setPayload($payload); // Default options $defaults = [ 'forbidden_php_functions' => [ 'fopen', 'popen', 'unlink', 'rmdir', 'dl', 'escapeshellarg', 'escapeshellcmd', 'exec', 'passthru', 'proc_close', 'proc_open', 'shell_exec', 'symlink', 'system', 'pcntl_exec', 'eval', 'create_function' ] ]; $options = array_merge($defaults, $options); $this->options = new Registry($options); } /** * Payload contains the variables passed as argumentse into the PHP code * * @param mixed $data * * @return void */ public function setPayload(&$data) { $this->payload = &$data; return $this; } /** * Set forbidden PHP functions. If any found, the whole PHP block won't run. * * @param array $functions * * @return void */ public function setForbiddenPHPFunctions($functions) { if (empty($functions)) { return $this; } if (is_string($functions)) { $functions = explode(',', $functions); } $this->options->set('forbidden_php_functions', $functions); return $this; } /** * Helper method to set the php code is about to be executed * * @param string $php_code * * @return void */ public function setPhpCode($php_code) { $this->php_code = $php_code; return $this; } /** * Checks if given PHP code is valid and it's allowed to run. * * @return bool */ private function allowedToRun() { // Get the forbidden PHP functions, but we want to make sure we whitelist 'curl_exec' $forbidden_functions = array_diff($this->options->get('forbidden_php_functions'), ['curl_exec']); // Build the regex, making sure 'exec' does not match within 'curl_exec' $re = '/\b(' . implode('|', $forbidden_functions) . ')\b(\s*\(|\s+[\'"])/mi'; preg_match_all($re, $this->php_code ?? '', $matches); if (!empty($matches[0])) { return false; } // Check for backticks `` if ($has_back_ticks = preg_match('/`(.*?)`/s', $this->php_code ?? '')) { return false; } return true; } /** * Run function * * @return function */ public function run() { if (!$this->allowedToRun()) { return; } $function_name = $this->getFunctionName(); // Function doesn't exist. Let's create it. if (!function_exists($function_name)) { if (!$this->createFunction()) { return; } } return $function_name($this->payload); } /** * Creates a temporary function in memory * * @return void */ private function createFunction() { $function_name = $this->getFunctionName(); $function_content = $this->getFunctionContent(); $temp_file = $this->getTempPath() . '/' . $function_name; // Write function's content to a temporary file File::write($temp_file, $function_content); // Include file include_once $temp_file; // Delete file if (!defined('JDEBUG') || !JDEBUG) { @chmod($temp_file, 0777); @unlink($temp_file); } return function_exists($function_name); } /** * Get temporary file content * * @return string */ private function getFunctionContent() { $function_name = $this->getFunctionName(); $variables = $this->getFunctionVariables(); $contents = [ 'php_code, ';return true;}' ]; $contents = implode("\n", $contents); // Remove Zero Width spaces / (non-)joiners $contents = str_replace( [ "\xE2\x80\x8B", "\xE2\x80\x8C", "\xE2\x80\x8D", ], '', $contents ); return $contents; } /** * Make user's life easier by initializing some Joomla helpful variables * * @return array */ protected function getFunctionVariables() { return [ '$app = $mainframe = \Joomla\CMS\Factory::getApplication();', '$document = $doc = \Joomla\CMS\Factory::getDocument();', '$database = $db = \Joomla\CMS\Factory::getDbo();', '$user = \Joomla\CMS\Factory::getUser();', '$Itemid = $app->input->getInt(\'Itemid\');' ]; } /** * Construct a temporary function name * * @return string */ private function getFunctionName() { return 'tassos_php_' . md5($this->php_code ?? ''); } /** * Return Joomla temporary path * * @return void */ private function getTempPath() { return Factory::getConfig()->get('tmp_path', JPATH_ROOT . '/tmp'); } }