diff --git a/CHANGELOG.md b/CHANGELOG.md index da12f46ff..ef5ec8a3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # v1.7.11 ## mm/dd/2021 +1. [](#new) + * Added configuration options to allow PHP methods to be used in Twig functions (`system.twig.safe_functions`) and filters (`system.twig.safe_filters`) + * Deprecated using PHP methods in Twig without them being in the safe lists + * Prevent dangerous PHP methods from being used as Twig functions and filters + * Restrict filesystem Twig functions to accept only local filesystem and grav streams 1. [](#improved) * Better GPM detection of unauthorized installations 1. [](#bugfix) diff --git a/system/config/system.yaml b/system/config/system.yaml index c3146141b..08b17fb16 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -113,6 +113,8 @@ twig: autoescape: true # Autoescape Twig vars (DEPRECATED, always enabled in strict mode) undefined_functions: true # Allow undefined functions undefined_filters: true # Allow undefined filters + safe_functions: [] # List of PHP functions which are allowed to be used as Twig functions + safe_filters: [] # List of PHP functions which are allowed to be used as Twig filters umask_fix: false # By default Twig creates cached files as 755, fix switches this to 775 assets: # Configuration for Assets Manager (JS, CSS) diff --git a/system/src/Grav/Common/Twig/Extension/FilesystemExtension.php b/system/src/Grav/Common/Twig/Extension/FilesystemExtension.php new file mode 100644 index 000000000..bdebae217 --- /dev/null +++ b/system/src/Grav/Common/Twig/Extension/FilesystemExtension.php @@ -0,0 +1,351 @@ +locator = Grav::instance()['locator']; + } + + /** + * @return TwigFunction[] + */ + public function getFilters() + { + return $this->getFunctions(); + } + + /** + * Return a list of all functions. + * + * @return TwigFunction[] + */ + public function getFunctions() + { + return [ + new TwigFunction('file_exists', [$this, 'file_exists']), + new TwigFunction('fileatime', [$this, 'fileatime']), + new TwigFunction('filectime', [$this, 'filectime']), + new TwigFunction('filemtime', [$this, 'filemtime']), + new TwigFunction('filesize', [$this, 'filesize']), + new TwigFunction('filetype', [$this, 'filetype']), + new TwigFunction('is_dir', [$this, 'is_dir']), + new TwigFunction('is_file', [$this, 'is_file']), + new TwigFunction('is_link', [$this, 'is_link']), + new TwigFunction('is_readable', [$this, 'is_readable']), + new TwigFunction('is_writable', [$this, 'is_writable']), + new TwigFunction('is_writeable', [$this, 'is_writable']), + new TwigFunction('lstat', [$this, 'lstat']), + new TwigFunction('getimagesize', [$this, 'getimagesize']), + new TwigFunction('exif_read_data', [$this, 'exif_read_data']), + new TwigFunction('read_exif_data', [$this, 'exif_read_data']), + new TwigFunction('exif_imagetype', [$this, 'exif_imagetype']), + new TwigFunction('hash_file', [$this, 'hash_file']), + new TwigFunction('hash_hmac_file', [$this, 'hash_hmac_file']), + new TwigFunction('md5_file', [$this, 'md5_file']), + new TwigFunction('sha1_file', [$this, 'sha1_file']), + new TwigFunction('get_meta_tags', [$this, 'get_meta_tags']), + ]; + } + + /** + * @param string $filename + * @return bool + */ + public function file_exists($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return file_exists($filename); + } + + /** + * @param string $filename + * @return int|false + */ + public function fileatime($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return fileatime($filename); + } + + /** + * @param string $filename + * @return int|false + */ + public function filectime($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return filectime($filename); + } + + /** + * @param string $filename + * @return int|false + */ + public function filemtime($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return filemtime($filename); + } + + /** + * @param string $filename + * @return int|false + */ + public function filesize($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return filesize($filename); + } + + /** + * @param string $filename + * @return string|false + */ + public function filetype($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return filetype($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_dir($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_dir($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_file($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_file($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_link($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_link($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_readable($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_readable($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_writable($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_writable($filename); + } + + /** + * @param string $filename + * @return array|false + */ + public function lstat($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return lstat($filename); + } + + /** + * @param string $filename + * @return array|false + */ + public function getimagesize($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return getimagesize($filename); + } + + /** + * @param string $file + * @param string|null $required_sections + * @param bool $as_arrays + * @param bool $read_thumbnail + * @return array|false + */ + public function exif_read_data($file, ?string $required_sections, bool $as_arrays = false, bool $read_thumbnail = false) + { + if (!Utils::functionExists('exif_read_data') || !$this->checkFilename($file)) { + return false; + } + + return exif_read_data($file, $required_sections, $as_arrays, $read_thumbnail); + } + + /** + * @param string $filename + * @return string|false + */ + public function exif_imagetype($filename) + { + if (!Utils::functionExists('exif_imagetype') || !$this->checkFilename($filename)) { + return false; + } + + return @exif_imagetype(); + } + + /** + * @param string $algo + * @param string $filename + * @param bool $binary + * @return string|false + */ + public function hash_file(string $algo, string $filename, bool $binary = false) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return hash_file($algo, $filename, $binary); + } + + /** + * @param string $algo + * @param string $data + * @param string $key + * @param bool $binary + * @return string|false + */ + public function hash_hmac_file(string $algo, string $data, string $key, bool $binary = false) + { + if (!$this->checkFilename($data)) { + return false; + } + + return hash_hmac_file($algo, $data, $key, $binary); + } + + /** + * @param string $filename + * @param bool $binary + * @return string|false + */ + public function md5_file($filename, bool $binary = false) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return md5_file($filename, $binary); + } + + /** + * @param string $filename + * @param bool $binary + * @return string|false + */ + public function sha1_file($filename, bool $binary = false) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return sha1_file($filename, $binary); + } + + /** + * @param string $filename + * @return array|false + */ + public function get_meta_tags($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return get_meta_tags($filename); + } + + /** + * @param string $filename + * @return bool + */ + private function checkFilename($filename): bool + { + return is_string($filename) && (!str_contains($filename, '://') || $this->locator->isStream($filename)); + } +} diff --git a/system/src/Grav/Common/Twig/TwigExtension.php b/system/src/Grav/Common/Twig/Extension/GravExtension.php similarity index 99% rename from system/src/Grav/Common/Twig/TwigExtension.php rename to system/src/Grav/Common/Twig/Extension/GravExtension.php index 4c6fa9e00..febdeded7 100644 --- a/system/src/Grav/Common/Twig/TwigExtension.php +++ b/system/src/Grav/Common/Twig/Extension/GravExtension.php @@ -7,7 +7,7 @@ * @license MIT License; see LICENSE file for details. */ -namespace Grav\Common\Twig; +namespace Grav\Common\Twig\Extension; use Cron\CronExpression; use Grav\Common\Config\Config; @@ -60,14 +60,13 @@ use function is_numeric; use function is_object; use function is_scalar; use function is_string; -use function ord; use function strlen; /** * Class TwigExtension * @package Grav\Common\Twig */ -class TwigExtension extends AbstractExtension implements GlobalsInterface +class GravExtension extends AbstractExtension implements GlobalsInterface { /** @var Grav */ protected $grav; @@ -214,7 +213,6 @@ class TwigExtension extends AbstractExtension implements GlobalsInterface new TwigFunction('svg_image', [$this, 'svgImageFunction']), new TwigFunction('xss', [$this, 'xssFunc']), - // Translations new TwigFunction('t', [$this, 'translate'], ['needs_environment' => true]), new TwigFunction('tl', [$this, 'translateLanguage']), diff --git a/system/src/Grav/Common/Twig/Twig.php b/system/src/Grav/Common/Twig/Twig.php index 5e0dcf751..2796c0d45 100644 --- a/system/src/Grav/Common/Twig/Twig.php +++ b/system/src/Grav/Common/Twig/Twig.php @@ -16,6 +16,9 @@ use Grav\Common\Language\Language; use Grav\Common\Language\LanguageCodes; use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Pages; +use Grav\Common\Twig\Extension\FilesystemExtension; +use Grav\Common\Twig\Extension\GravExtension; +use Grav\Common\Utils; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RocketTheme\Toolbox\Event\Event; use Phive\Twig\Extensions\Deferred\DeferredExtension; @@ -34,6 +37,8 @@ use Twig\Profiler\Profile; use Twig\TwigFilter; use Twig\TwigFunction; use function function_exists; +use function in_array; +use function is_array; /** * Class Twig @@ -154,27 +159,53 @@ class Twig $this->twig = new TwigEnvironment($loader_chain, $params); - if ($config->get('system.twig.undefined_functions')) { - $this->twig->registerUndefinedFunctionCallback(function ($name) { + $this->twig->registerUndefinedFunctionCallback(function ($name) use ($config) { + $allowed = $config->get('system.twig.safe_functions'); + if (is_array($allowed) && in_array($name, $allowed, true) && function_exists($name)) { + return new TwigFunction($name, $name); + } + if ($config->get('system.twig.undefined_functions')) { if (function_exists($name)) { - return new TwigFunction($name, $name); + if (!Utils::isDangerousFunction($name)) { + user_error("PHP function {$name}() was used as Twig function. This is deprecated in Grav 1.7. Please add it to system configuration: `system.twig.safe_functions`", E_USER_DEPRECATED); + + return new TwigFunction($name, $name); + } + + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->addException(new RuntimeException("Blocked potentially dangerous PHP function {$name}() being used as Twig function. If you really want to use it, please add it to system configuration: `system.twig.safe_functions`")); } - return new TwigFunction($name, static function () { - }); - }); - } + return new TwigFunction($name, static function () {}); + } - if ($config->get('system.twig.undefined_filters')) { - $this->twig->registerUndefinedFilterCallback(function ($name) { + return false; + }); + + $this->twig->registerUndefinedFilterCallback(function ($name) use ($config) { + $allowed = $config->get('system.twig.safe_filters'); + if (is_array($allowed) && in_array($name, $allowed, true) && function_exists($name)) { + return new TwigFilter($name, $name); + } + if ($config->get('system.twig.undefined_filters')) { if (function_exists($name)) { - return new TwigFilter($name, $name); + if (!Utils::isDangerousFunction($name)) { + user_error("PHP function {$name}() used as Twig filter. This is deprecated in Grav 1.7. Please add it to system configuration: `system.twig.safe_filters`", E_USER_DEPRECATED); + + return new TwigFilter($name, $name); + } + + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->addException(new RuntimeException("Blocked potentially dangerous PHP function {$name}() being used as Twig filter. If you really want to use it, please add it to system configuration: `system.twig.safe_filters`")); } - return new TwigFilter($name, static function () { - }); - }); - } + return new TwigFilter($name, static function () {}); + } + + return false; + }); $this->grav->fireEvent('onTwigInitialized'); @@ -188,7 +219,8 @@ class Twig if ($config->get('system.twig.debug')) { $this->twig->addExtension(new DebugExtension()); } - $this->twig->addExtension(new TwigExtension()); + $this->twig->addExtension(new GravExtension()); + $this->twig->addExtension(new FilesystemExtension()); $this->twig->addExtension(new DeferredExtension()); $this->twig->addExtension(new StringLoaderExtension()); diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index 51eaf42ef..edc093e4f 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -44,7 +44,7 @@ use function strlen; */ abstract class Utils { - /** @var array */ + /** @var array */ protected static $nonces = []; protected const ROOTURL_REGEX = '{^((?:http[s]?:\/\/[^\/]+)|(?:\/\/[^\/]+))(.*)}'; @@ -178,8 +178,8 @@ abstract class Utils /** * Check if the $haystack string starts with the substring $needle * - * @param string $haystack - * @param string|string[] $needle + * @param string $haystack + * @param string|string[] $needle * @param bool $case_sensitive * @return bool */ @@ -202,8 +202,8 @@ abstract class Utils /** * Check if the $haystack string ends with the substring $needle * - * @param string $haystack - * @param string|string[] $needle + * @param string $haystack + * @param string|string[] $needle * @param bool $case_sensitive * @return bool */ @@ -227,9 +227,9 @@ abstract class Utils /** * Check if the $haystack string contains the substring $needle * - * @param string $haystack - * @param string|string[] $needle - * @param bool $case_sensitive + * @param string $haystack + * @param string|string[] $needle + * @param bool $case_sensitive * @return bool */ public static function contains($haystack, $needle, $case_sensitive = true) @@ -266,19 +266,19 @@ abstract class Utils { $regex = str_replace( array("\*", "\?"), // wildcard chars - array('.*','.'), // regexp chars + array('.*', '.'), // regexp chars preg_quote($wildcard_pattern, '/') ); - return preg_match('/^'.$regex.'$/is', $haystack); + return preg_match('/^' . $regex . '$/is', $haystack); } /** * Render simple template filling up the variables in it. If value is not defined, leave it as it was. * - * @param string $template Template string - * @param array $variables Variables with values - * @param array $brackets Optional array of opening and closing brackets or symbols + * @param string $template Template string + * @param array $variables Variables with values + * @param array $brackets Optional array of opening and closing brackets or symbols * @return string Final string filled with values */ public static function simpleTemplate(string $template, array $variables, array $brackets = ['{', '}']): string @@ -376,8 +376,8 @@ abstract class Utils /** * Merge two objects into one. * - * @param object $obj1 - * @param object $obj2 + * @param object $obj1 + * @param object $obj2 * * @return object */ @@ -415,7 +415,7 @@ abstract class Utils */ public static function arrayRemoveValue(array $search, $value) { - foreach ((array) $value as $val) { + foreach ((array)$value as $val) { $key = array_search($val, $search); if ($key !== false) { unset($search[$key]); @@ -481,8 +481,8 @@ abstract class Utils /** * Array combine but supports different array lengths * - * @param array $arr1 - * @param array $arr2 + * @param array $arr1 + * @param array $arr2 * @return array|false */ public static function arrayCombine($arr1, $arr2) @@ -495,7 +495,7 @@ abstract class Utils /** * Array is associative or not * - * @param array $arr + * @param array $arr * @return bool */ public static function arrayIsAssociative($arr) @@ -517,15 +517,15 @@ abstract class Utils $now = new DateTime(); $date_formats = [ - 'd-m-Y H:i' => 'd-m-Y H:i (e.g. '.$now->format('d-m-Y H:i').')', - 'Y-m-d H:i' => 'Y-m-d H:i (e.g. '.$now->format('Y-m-d H:i').')', - 'm/d/Y h:i a' => 'm/d/Y h:i a (e.g. '.$now->format('m/d/Y h:i a').')', - 'H:i d-m-Y' => 'H:i d-m-Y (e.g. '.$now->format('H:i d-m-Y').')', - 'h:i a m/d/Y' => 'h:i a m/d/Y (e.g. '.$now->format('h:i a m/d/Y').')', - ]; + 'd-m-Y H:i' => 'd-m-Y H:i (e.g. ' . $now->format('d-m-Y H:i') . ')', + 'Y-m-d H:i' => 'Y-m-d H:i (e.g. ' . $now->format('Y-m-d H:i') . ')', + 'm/d/Y h:i a' => 'm/d/Y h:i a (e.g. ' . $now->format('m/d/Y h:i a') . ')', + 'H:i d-m-Y' => 'H:i d-m-Y (e.g. ' . $now->format('H:i d-m-Y') . ')', + 'h:i a m/d/Y' => 'h:i a m/d/Y (e.g. ' . $now->format('h:i a m/d/Y') . ')', + ]; $default_format = Grav::instance()['config']->get('system.pages.dateformat.default'); if ($default_format) { - $date_formats = array_merge([$default_format => $default_format.' (e.g. '.$now->format($default_format).')'], $date_formats); + $date_formats = array_merge([$default_format => $default_format . ' (e.g. ' . $now->format($default_format) . ')'], $date_formats); } return $date_formats; @@ -552,11 +552,11 @@ abstract class Utils /** * Truncate text by number of characters but can cut off words. * - * @param string $string - * @param int $limit Max number of characters. - * @param bool $up_to_break truncate up to breakpoint after char count - * @param string $break Break point. - * @param string $pad Appended padding to the end of the string. + * @param string $string + * @param int $limit Max number of characters. + * @param bool $up_to_break truncate up to breakpoint after char count + * @param string $break Break point. + * @param string $pad Appended padding to the end of the string. * @return string */ public static function truncate($string, $limit = 150, $up_to_break = false, $break = ' ', $pad = '…') @@ -582,7 +582,7 @@ abstract class Utils * Truncate text by number of characters in a "word-safe" manor. * * @param string $string - * @param int $limit + * @param int $limit * @return string */ public static function safeTruncate($string, $limit = 150) @@ -594,9 +594,9 @@ abstract class Utils /** * Truncate HTML by number of characters. not "word-safe"! * - * @param string $text - * @param int $length in characters - * @param string $ellipsis + * @param string $text + * @param int $length in characters + * @param string $ellipsis * @return string */ public static function truncateHtml($text, $length = 100, $ellipsis = '...') @@ -607,9 +607,9 @@ abstract class Utils /** * Truncate HTML by number of characters in a "word-safe" manor. * - * @param string $text - * @param int $length in words - * @param string $ellipsis + * @param string $text + * @param int $length in words + * @param string $ellipsis * @return string */ public static function safeTruncateHtml($text, $length = 25, $ellipsis = '...') @@ -633,8 +633,8 @@ abstract class Utils * * @param string $file the full path to the file to be downloaded * @param bool $force_download as opposed to letting browser choose if to download or render - * @param int $sec Throttling, try 0.1 for some speed throttling of downloads - * @param int $bytes Size of chunks to send in bytes. Default is 1024 + * @param int $sec Throttling, try 0.1 for some speed throttling of downloads + * @param int $bytes Size of chunks to send in bytes. Default is 1024 * @throws Exception */ public static function download($file, $force_download = true, $sec = 0, $bytes = 1024) @@ -645,7 +645,7 @@ abstract class Utils $file_parts = pathinfo($file); $mimetype = static::getMimeByExtension($file_parts['extension']); - $size = filesize($file); // File size + $size = filesize($file); // File size // clean all buffers while (ob_get_level()) { @@ -742,7 +742,7 @@ abstract class Utils // Set from uri extension $uri_extension = $uri->extension(); if (is_string($uri_extension) && $uri->isValidExtension($uri_extension)) { - return($uri_extension); + return ($uri_extension); } // Use content negotiation via the `accept:` header @@ -1060,7 +1060,7 @@ abstract class Utils $pretty_offset = "UTC${offset_prefix}${offset_formatted}"; - $timezone_list[$timezone] = "(${pretty_offset}) ".str_replace('_', ' ', $timezone); + $timezone_list[$timezone] = "(${pretty_offset}) " . str_replace('_', ' ', $timezone); } return $timezone_list; @@ -1069,11 +1069,11 @@ abstract class Utils /** * Recursively filter an array, filtering values by processing them through the $fn function argument * - * @param array $source the Array to filter - * @param callable $fn the function to pass through each array item + * @param array $source the Array to filter + * @param callable $fn the function to pass through each array item * @return array */ - public static function arrayFilterRecursive(Array $source, $fn) + public static function arrayFilterRecursive(array $source, $fn) { $result = []; foreach ($source as $key => $value) { @@ -1093,15 +1093,15 @@ abstract class Utils /** * Flatten a multi-dimensional associative array into query params. * - * @param array $array - * @param string $prepend + * @param array $array + * @param string $prepend * @return array */ public static function arrayToQueryParams($array, $prepend = '') { $results = []; foreach ($array as $key => $value) { - $name = $prepend ? $prepend . '[' . $key . ']' : $key; + $name = $prepend ? $prepend . '[' . $key . ']' : $key; if (is_array($value)) { $results = array_merge($results, static::arrayToQueryParams($value, $name)); @@ -1138,8 +1138,8 @@ abstract class Utils /** * Flatten a multi-dimensional associative array into dot notation * - * @param array $array - * @param string $prepend + * @param array $array + * @param string $prepend * @return array */ public static function arrayFlattenDotNotation($array, $prepend = '') @@ -1147,9 +1147,9 @@ abstract class Utils $results = array(); foreach ($array as $key => $value) { if (is_array($value)) { - $results = array_merge($results, static::arrayFlattenDotNotation($value, $prepend.$key.'.')); + $results = array_merge($results, static::arrayFlattenDotNotation($value, $prepend . $key . '.')); } else { - $results[$prepend.$key] = $value; + $results[$prepend . $key] = $value; } } @@ -1297,7 +1297,7 @@ abstract class Utils * with reverse proxy setups. * * @param string $action - * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours) + * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours) * @return string the nonce string */ private static function generateNonceString($action, $previousTick = false) @@ -1334,8 +1334,8 @@ abstract class Utils * Creates a hashed nonce tied to the passed action. Tied to the current user and time. The nonce for a given * action is the same for 12 hours. * - * @param string $action the action the nonce is tied to (e.g. save-user-admin or move-page-homepage) - * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours) + * @param string $action the action the nonce is tied to (e.g. save-user-admin or move-page-homepage) + * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours) * @return string the nonce */ public static function getNonce($action, $previousTick = false) @@ -1353,7 +1353,7 @@ abstract class Utils /** * Verify the passed nonce for the give action * - * @param string|string[] $nonce the nonce to verify + * @param string|string[] $nonce the nonce to verify * @param string $action the action to verify the nonce to * @return boolean verified or not */ @@ -1434,7 +1434,7 @@ abstract class Utils while (count($keys) > 1) { $key = array_shift($keys); - if (! isset($array[$key]) || ! is_array($array[$key])) { + if (!isset($array[$key]) || !is_array($array[$key])) { $array[$key] = array(); } @@ -1725,7 +1725,7 @@ abstract class Utils $size *= 1024 ** stripos('bkmgtpezy', $unit[0]); } - return (int) abs(round($size)); + return (int)abs(round($size)); } /** @@ -1770,7 +1770,7 @@ abstract class Utils public static function processMarkdown($string, $block = true, $page = null) { $grav = Grav::instance(); - $page = $page ?? $grav['page'] ?? null; + $page = $page ?? $grav['page'] ?? null; $defaults = [ 'markdown' => $grav['config']->get('system.pages.markdown', []), 'images' => $grav['config']->get('system.images', []) @@ -1812,12 +1812,12 @@ abstract class Utils $ip = (string)inet_pton($ip); // Maximum netmask length = same as packed address - $len = 8*strlen($ip); + $len = 8 * strlen($ip); if ($prefix > $len) { $prefix = $len; } - $mask = str_repeat('f', $prefix>>2); + $mask = str_repeat('f', $prefix >> 2); switch ($prefix & 3) { case 3: @@ -1830,7 +1830,7 @@ abstract class Utils $mask .= '8'; break; } - $mask = str_pad($mask, $len>>2, '0'); + $mask = str_pad($mask, $len >> 2, '0'); // Packed representation of netmask $mask = pack('H*', $mask); @@ -1861,4 +1861,244 @@ abstract class Utils return $types; } + + /** + * @param string $name + * @return bool + */ + public static function isDangerousFunction(string $name): bool + { + static $commandExecutionFunctions = [ + 'exec', + 'passthru', + 'system', + 'shell_exec', + 'popen', + 'proc_open', + 'pcntl_exec', + ]; + + static $codeExecutionFunctions = [ + 'assert', + 'preg_replace', + 'create_function', + 'include', + 'include_once', + 'require', + 'require_once' + ]; + + static $callbackFunctions = [ + 'ob_start' => 0, + 'array_diff_uassoc' => -1, + 'array_diff_ukey' => -1, + 'array_filter' => 1, + 'array_intersect_uassoc' => -1, + 'array_intersect_ukey' => -1, + 'array_map' => 0, + 'array_reduce' => 1, + 'array_udiff_assoc' => -1, + 'array_udiff_uassoc' => [-1, -2], + 'array_udiff' => -1, + 'array_uintersect_assoc' => -1, + 'array_uintersect_uassoc' => [-1, -2], + 'array_uintersect' => -1, + 'array_walk_recursive' => 1, + 'array_walk' => 1, + 'assert_options' => 1, + 'uasort' => 1, + 'uksort' => 1, + 'usort' => 1, + 'preg_replace_callback' => 1, + 'spl_autoload_register' => 0, + 'iterator_apply' => 1, + 'call_user_func' => 0, + 'call_user_func_array' => 0, + 'register_shutdown_function' => 0, + 'register_tick_function' => 0, + 'set_error_handler' => 0, + 'set_exception_handler' => 0, + 'session_set_save_handler' => [0, 1, 2, 3, 4, 5], + 'sqlite_create_aggregate' => [2, 3], + 'sqlite_create_function' => 2, + ]; + + static $informationDiscosureFunctions = [ + 'phpinfo', + 'posix_mkfifo', + 'posix_getlogin', + 'posix_ttyname', + 'getenv', + 'get_current_user', + 'proc_get_status', + 'get_cfg_var', + 'disk_free_space', + 'disk_total_space', + 'diskfreespace', + 'getcwd', + 'getlastmo', + 'getmygid', + 'getmyinode', + 'getmypid', + 'getmyuid' + ]; + + static $otherFunctions = [ + 'extract', + 'parse_str', + 'putenv', + 'ini_set', + 'mail', + 'header', + 'proc_nice', + 'proc_terminate', + 'proc_close', + 'pfsockopen', + 'fsockopen', + 'apache_child_terminate', + 'posix_kill', + 'posix_mkfifo', + 'posix_setpgid', + 'posix_setsid', + 'posix_setuid', + ]; + + if (in_array($name, $commandExecutionFunctions)) { + return true; + } + + if (in_array($name, $codeExecutionFunctions)) { + return true; + } + + if (isset($callbackFunctions[$name])) { + return true; + } + + if (in_array($name, $informationDiscosureFunctions)) { + return true; + } + + if (in_array($name, $otherFunctions)) { + return true; + } + + return static::isFilesystemFunction($name); + } + + /** + * @param string $name + * @return bool + */ + public static function isFilesystemFunction(string $name): bool + { + static $fileWriteFunctions = [ + 'fopen', + 'tmpfile', + 'bzopen', + 'gzopen', + // write to filesystem (partially in combination with reading) + 'chgrp', + 'chmod', + 'chown', + 'copy', + 'file_put_contents', + 'lchgrp', + 'lchown', + 'link', + 'mkdir', + 'move_uploaded_file', + 'rename', + 'rmdir', + 'symlink', + 'tempnam', + 'touch', + 'unlink', + 'imagepng', + 'imagewbmp', + 'image2wbmp', + 'imagejpeg', + 'imagexbm', + 'imagegif', + 'imagegd', + 'imagegd2', + 'iptcembed', + 'ftp_get', + 'ftp_nb_get', + ]; + + static $fileContentFunctions = [ + 'file_get_contents', + 'file', + 'filegroup', + 'fileinode', + 'fileowner', + 'fileperms', + 'glob', + 'is_executable', + 'is_uploaded_file', + 'parse_ini_file', + 'readfile', + 'readlink', + 'realpath', + 'gzfile', + 'readgzfile', + 'stat', + 'imagecreatefromgif', + 'imagecreatefromjpeg', + 'imagecreatefrompng', + 'imagecreatefromwbmp', + 'imagecreatefromxbm', + 'imagecreatefromxpm', + 'ftp_put', + 'ftp_nb_put', + 'hash_update_file', + 'highlight_file', + 'show_source', + 'php_strip_whitespace', + ]; + + static $filesystemFunctions = [ + // read from filesystem + 'file_exists', + 'fileatime', + 'filectime', + 'filemtime', + 'filesize', + 'filetype', + 'is_dir', + 'is_file', + 'is_link', + 'is_readable', + 'is_writable', + 'is_writeable', + 'linkinfo', + 'lstat', + //'pathinfo', + 'getimagesize', + 'exif_read_data', + 'read_exif_data', + 'exif_thumbnail', + 'exif_imagetype', + 'hash_file', + 'hash_hmac_file', + 'md5_file', + 'sha1_file', + 'get_meta_tags', + ]; + + if (in_array($name, $fileWriteFunctions)) { + return true; + } + + if (in_array($name, $fileContentFunctions)) { + return true; + } + + if (in_array($name, $filesystemFunctions)) { + return true; + } + + return false; + } }