diff --git a/CHANGELOG.md b/CHANGELOG.md index 611b3ca18..7bc0d630a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ 1. [](#new) * Added `Deprecated` tab to DebugBar to catch future incompatibilities with later Grav versions * Added deprecation notices for features which will be removed in Grav 2.0 + * Added `Utils::detectXssFromArray()` and `Utils::detectXss()` methods 1. [](#bugfix) * Allow `$page->slug()` to be called before `$page->init()` without breaking the page * Fix for `Page::translatedLanguages()` to use routes always [#2163](https://github.com/getgrav/grav/issues/2163) diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index 5de4fe7b1..53c67e7ab 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -253,6 +253,90 @@ abstract class Utils return $date_formats; } + /** + * @param array $array Array such as $_POST or $_GET + * @param string $prefix Prefix for returned values. + * @return array Returns flatten list of potentially dangerous input values, such as 'data.content'. + */ + public static function detectXssFromArray(array $array, $prefix = '') + { + $list = []; + + foreach ($array as $key => $value) { + if (\is_array($value)) { + $list[] = static::detectXssFromArray($value, $prefix . $key . '.'); + } + if (static::detectXss($value)) { + $list[] = [$prefix . $key]; + } + } + + return array_merge(...$list); + } + + /** + * Determine if string potentially has a XSS attack. This simple function does not catch all XSS and it is likely to + * return false positives because of it tags all potentially dangerous HTML tags and attributes without looking into + * their content. + * + * @param string $string The string to run XSS detection logic on + * @return boolean True if the given `$string` may contain XSS, false otherwise. + * + * Copies the code from: https://github.com/symphonycms/xssfilter/blob/master/extension.driver.php#L138 + */ + public static function detectXss($string) + { + // Skip any null or non string values + if (null === $string || !\is_string($string) || empty($string)) { + return false; + } + + // Keep a copy of the original string before cleaning up + $orig = $string; + + // URL decode + $string = urldecode($string); + + // Convert Hexadecimals + $string = (string)preg_replace_callback('!(&#|\\\)[xX]([0-9a-fA-F]+);?!u', function($m) { + return \chr(hexdec($m[2])); + }, $string); + + // Clean up entities + $string = preg_replace('!(�+[0-9]+)!u','$1;', $string); + + // Decode entities + $string = html_entity_decode($string, ENT_NOQUOTES, 'UTF-8'); + + // Strip whitespace characters + $string = preg_replace('!\s!u','', $string); + + // Set the patterns we'll test against + $patterns = [ + // Match any attribute starting with "on" or xmlns + '#(<[^>]+[\x00-\x20\"\'\/])(on|xmlns)[^>]*>?#iUu', + + // Match javascript:, livescript:, vbscript:, mocha:, feed: and data: protocols + '!((java|live|vb)script|mocha|feed|data):(\w)*!iUu', + + // Match -moz-bindings + '#-moz-binding[\x00-\x20]*:#u', + + // Match style attributes + '#(<[^>]+[\x00-\x20\"\'\/])style=[^>]*>?#iUu', + + // Match potentially dangerous tags + '#]*>?#ui' + ]; + + foreach ($patterns as $pattern) { + // Test both the original string and clean string + if (preg_match($pattern, $string) || preg_match($pattern, $orig)) { + return true; + } + } + return false; + } /** * Truncate text by number of characters but can cut off words. *