Security.php 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. <?php
  2. /**
  3. * @package Grav\Common
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common;
  9. use Grav\Common\Page\Pages;
  10. class Security
  11. {
  12. public static function detectXssFromPages(Pages $pages, $route = true, callable $status = null)
  13. {
  14. $routes = $pages->routes();
  15. // Remove duplicate for homepage
  16. unset($routes['/']);
  17. $list = [];
  18. // This needs Symfony 4.1 to work
  19. $status && $status([
  20. 'type' => 'count',
  21. 'steps' => count($routes),
  22. ]);
  23. foreach ($routes as $path) {
  24. $status && $status([
  25. 'type' => 'progress',
  26. ]);
  27. try {
  28. $page = $pages->get($path);
  29. // call the content to load/cache it
  30. $header = (array) $page->header();
  31. $content = $page->value('content');
  32. $data = ['header' => $header, 'content' => $content];
  33. $results = Security::detectXssFromArray($data);
  34. if (!empty($results)) {
  35. if ($route) {
  36. $list[$page->route()] = $results;
  37. } else {
  38. $list[$page->filePathClean()] = $results;
  39. }
  40. }
  41. } catch (\Exception $e) {
  42. continue;
  43. }
  44. }
  45. return $list;
  46. }
  47. /**
  48. * @param array $array Array such as $_POST or $_GET
  49. * @param string $prefix Prefix for returned values.
  50. * @return array Returns flatten list of potentially dangerous input values, such as 'data.content'.
  51. */
  52. public static function detectXssFromArray(array $array, $prefix = '')
  53. {
  54. $list = [];
  55. foreach ($array as $key => $value) {
  56. if (\is_array($value)) {
  57. $list[] = static::detectXssFromArray($value, $prefix . $key . '.');
  58. }
  59. if ($result = static::detectXss($value)) {
  60. $list[] = [$prefix . $key => $result];
  61. }
  62. }
  63. if (!empty($list)) {
  64. return array_merge(...$list);
  65. }
  66. return $list;
  67. }
  68. /**
  69. * Determine if string potentially has a XSS attack. This simple function does not catch all XSS and it is likely to
  70. * return false positives because of it tags all potentially dangerous HTML tags and attributes without looking into
  71. * their content.
  72. *
  73. * @param string $string The string to run XSS detection logic on
  74. * @return bool|string Type of XSS vector if the given `$string` may contain XSS, false otherwise.
  75. *
  76. * Copies the code from: https://github.com/symphonycms/xssfilter/blob/master/extension.driver.php#L138
  77. */
  78. public static function detectXss($string)
  79. {
  80. // Skip any null or non string values
  81. if (null === $string || !\is_string($string) || empty($string)) {
  82. return false;
  83. }
  84. // Keep a copy of the original string before cleaning up
  85. $orig = $string;
  86. // URL decode
  87. $string = urldecode($string);
  88. // Convert Hexadecimals
  89. $string = (string)preg_replace_callback('!(&#|\\\)[xX]([0-9a-fA-F]+);?!u', function($m) {
  90. return \chr(hexdec($m[2]));
  91. }, $string);
  92. // Clean up entities
  93. $string = preg_replace('!(&#0+[0-9]+)!u','$1;', $string);
  94. // Decode entities
  95. $string = html_entity_decode($string, ENT_NOQUOTES, 'UTF-8');
  96. // Strip whitespace characters
  97. $string = preg_replace('!\s!u','', $string);
  98. $config = Grav::instance()['config'];
  99. $dangerous_tags = array_map('preg_quote', array_map("trim", $config->get('security.xss_dangerous_tags')));
  100. $invalid_protocols = array_map('preg_quote', array_map("trim", $config->get('security.xss_invalid_protocols')));
  101. $enabled_rules = $config->get('security.xss_enabled');
  102. // Set the patterns we'll test against
  103. $patterns = [
  104. // Match any attribute starting with "on" or xmlns
  105. 'on_events' => '#(<[^>]+[[a-z\x00-\x20\"\'\/])([\s\/]on|\sxmlns)[a-z].*=>?#iUu',
  106. // Match javascript:, livescript:, vbscript:, mocha:, feed: and data: protocols
  107. 'invalid_protocols' => '#(' . implode('|', $invalid_protocols) . '):.*?#iUu',
  108. // Match -moz-bindings
  109. 'moz_binding' => '#-moz-binding[a-z\x00-\x20]*:#u',
  110. // Match style attributes
  111. 'html_inline_styles' => '#(<[^>]+[a-z\x00-\x20\"\'\/])(style=[^>]*(url\:|x\:expression).*)>?#iUu',
  112. // Match potentially dangerous tags
  113. 'dangerous_tags' => '#</*(' . implode('|', $dangerous_tags) . ')[^>]*>?#ui'
  114. ];
  115. // Iterate over rules and return label if fail
  116. foreach ((array) $patterns as $name => $regex) {
  117. if ($enabled_rules[$name] === true) {
  118. if (preg_match($regex, $string) || preg_match($regex, $orig)) {
  119. return $name;
  120. }
  121. }
  122. }
  123. return false;
  124. }
  125. }