Security.php 4.8 KB

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