UnsecureFunctionsUsageTest.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Test\Legacy;
  7. use Magento\Framework\App\Utility\Files;
  8. use Magento\Framework\Component\ComponentRegistrar;
  9. use Magento\TestFramework\Utility\FunctionDetector;
  10. /**
  11. * Tests to detect unsecure functions usage
  12. */
  13. class UnsecureFunctionsUsageTest extends \PHPUnit\Framework\TestCase
  14. {
  15. /**
  16. * Php unsecure functions
  17. *
  18. * @var array
  19. */
  20. private static $phpUnsecureFunctions = [];
  21. /**
  22. * JS unsecure functions
  23. *
  24. * @var array
  25. */
  26. private static $jsUnsecureFunctions = [];
  27. /**
  28. * File extensions pattern to search for
  29. *
  30. * @var string
  31. */
  32. private $fileExtensions = '/\.(php|phtml|js)$/';
  33. /**
  34. * Read fixtures into memory as arrays
  35. *
  36. * @return void
  37. */
  38. public static function setUpBeforeClass()
  39. {
  40. self::loadData(self::$phpUnsecureFunctions, 'unsecure_php_functions*.php');
  41. self::loadData(self::$jsUnsecureFunctions, 'unsecure_js_functions*.php');
  42. }
  43. /**
  44. * Loads and merges data from fixtures
  45. *
  46. * @param array $data
  47. * @param string $filePattern
  48. * @return void
  49. */
  50. private static function loadData(array &$data, $filePattern)
  51. {
  52. foreach (glob(__DIR__ . '/_files/security/' . $filePattern) as $file) {
  53. $data = array_merge_recursive($data, self::readList($file));
  54. }
  55. $componentRegistrar = new ComponentRegistrar();
  56. foreach ($data as $key => $value) {
  57. $excludes = $value['exclude'];
  58. $excludePaths = [];
  59. foreach ($excludes as $exclude) {
  60. if ('setup' == $exclude['type']) {
  61. $excludePaths[] = BP . '/setup/' . $exclude['path'];
  62. } else {
  63. $excludePaths[] = $componentRegistrar->getPath($exclude['type'], $exclude['name'])
  64. . '/' . $exclude['path'];
  65. }
  66. }
  67. $data[$key]['exclude'] = $excludePaths;
  68. }
  69. }
  70. /**
  71. * Isolate including a file into a method to reduce scope
  72. *
  73. * @param string $file
  74. * @return array
  75. */
  76. private static function readList($file)
  77. {
  78. return include $file;
  79. }
  80. /**
  81. * Detect unsecure functions usage for changed files in whitelist with the exception of blacklist
  82. *
  83. * @return void
  84. */
  85. public function testUnsecureFunctionsUsage()
  86. {
  87. $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
  88. $functionDetector = new FunctionDetector();
  89. $invoker(
  90. function ($fileFullPath) use ($functionDetector) {
  91. $functions = $this->getFunctions($fileFullPath);
  92. $lines = $functionDetector->detect($fileFullPath, array_keys($functions));
  93. $message = '';
  94. if (!empty($lines)) {
  95. $message = $this->composeMessage($fileFullPath, $lines, $functions);
  96. }
  97. $this->assertEmpty(
  98. $lines,
  99. $message
  100. );
  101. },
  102. $this->getFilesToVerify()
  103. );
  104. }
  105. /**
  106. * Compose message
  107. *
  108. * @param string $fileFullPath
  109. * @param array $lines
  110. * @param array $functionRules
  111. * @return string
  112. */
  113. private function composeMessage($fileFullPath, $lines, $functionRules)
  114. {
  115. $result = '';
  116. foreach ($lines as $lineNumber => $detectedFunctions) {
  117. $detectedFunctionRules = array_intersect_key($functionRules, array_flip($detectedFunctions));
  118. $replacementString = '';
  119. foreach ($detectedFunctionRules as $function => $functionRule) {
  120. $replacement = $functionRule['replacement'];
  121. if (is_array($replacement)) {
  122. $replacement = array_unique($replacement);
  123. $replacement = count($replacement) > 1 ?
  124. "[\n\t\t\t" . implode("\n\t\t\t", $replacement) . "\n\t\t]" :
  125. $replacement[0];
  126. }
  127. $replacement = empty($replacement) ? 'No suggested replacement at this time' : $replacement;
  128. $replacementString .= "\t\t'$function' => '$replacement'\n";
  129. }
  130. $result .= sprintf(
  131. "Functions '%s' are not secure in %s. \n\tSuggested replacement:\n%s",
  132. implode(', ', $detectedFunctions),
  133. $fileFullPath . ':' . $lineNumber,
  134. $replacementString
  135. );
  136. }
  137. return $result;
  138. }
  139. /**
  140. * Get files to be verified
  141. *
  142. * @return array
  143. */
  144. private function getFilesToVerify()
  145. {
  146. $fileExtensions = $this->fileExtensions;
  147. $directoriesToScan = Files::init()->readLists(__DIR__ . '/_files/security/whitelist.txt');
  148. $filesToVerify = [];
  149. foreach (glob(__DIR__ . '/../_files/changed_files*') as $listFile) {
  150. $filesToVerify = array_merge(
  151. $filesToVerify,
  152. file($listFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)
  153. );
  154. }
  155. array_walk(
  156. $filesToVerify,
  157. function (&$file) {
  158. $file = [BP . '/' . $file];
  159. }
  160. );
  161. $filesToVerify = array_filter(
  162. $filesToVerify,
  163. function ($path) use ($directoriesToScan, $fileExtensions) {
  164. if (!file_exists($path[0])) {
  165. return false;
  166. }
  167. $path = realpath($path[0]);
  168. foreach ($directoriesToScan as $directory) {
  169. $directory = realpath($directory);
  170. if (strpos($path, $directory) === 0) {
  171. if (preg_match($fileExtensions, $path)) {
  172. // skip unit tests
  173. if (preg_match('#' . preg_quote('Test/Unit', '#') . '#', $path)) {
  174. return false;
  175. }
  176. return true;
  177. }
  178. }
  179. }
  180. return false;
  181. }
  182. );
  183. return $filesToVerify;
  184. }
  185. /**
  186. * Get functions for the given file
  187. *
  188. * @param string $fileFullPath
  189. * @return array
  190. */
  191. private function getFunctions($fileFullPath)
  192. {
  193. $fileExtension = pathinfo($fileFullPath, PATHINFO_EXTENSION);
  194. $functions = [];
  195. if ($fileExtension == 'php') {
  196. $functions = self::$phpUnsecureFunctions;
  197. } elseif ($fileExtension == 'js') {
  198. $functions = self::$jsUnsecureFunctions;
  199. } elseif ($fileExtension == 'phtml') {
  200. $functions = array_merge_recursive(self::$phpUnsecureFunctions, self::$jsUnsecureFunctions);
  201. }
  202. foreach ($functions as $function => $functionRules) {
  203. if (in_array($fileFullPath, $functionRules['exclude'])) {
  204. unset($functions[$function]);
  205. }
  206. }
  207. return $functions;
  208. }
  209. }