HazardableBehavior.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. <?php
  2. App::uses('ModelBehavior', 'Model');
  3. App::uses('HazardLib', 'Tools.Lib');
  4. /**
  5. * Uses the HazardLib to test well known injection snippets of all kinds (including XSS, SQL)
  6. * and tests your db wrapper methods or populates your records with it.
  7. * Use this ONLY for testing environments and never on your live data.
  8. *
  9. * Available snippet types are XSS, PHP and SQL.
  10. *
  11. * The main concern is not just "eval" users that might try to hijack/abuse your site.
  12. * Not properly securing your view also means that strings like `some>cool<string` will most likely mess up your HTML.
  13. * The view could be rendered as a complete mess without the user or the developer knowing it. It might have even been
  14. * the admin which inserted those layout-breaking strings, after all.
  15. * That's why it is so important to follow the rule "do NOT sanitize, validate input, escape output" (there are exceptions, of course).
  16. * Also make sure, you already cover those basics in your baking template. This will save a lot of time in the long run.
  17. *
  18. * If you inserted records go and browse your backend and especially your frontend.
  19. * Everywhere where you get some alert or strange behavior, you might have forgotten to use h() or other
  20. * measures to secure your output properly.
  21. *
  22. * You can also apply this behavior globally to overwrite all strings in your application temporarily.
  23. * This way you don't need to modify the database. On output it will just inject the hazardous strings and
  24. * you can browse your website just as if they were actually stored in your db.
  25. *
  26. * Add it to some models or even the AppModel (temporarily!) as `$actsAs = array('Tools.Hazardable'))`.
  27. * A known limitation of Cake behaviors is, though, that this would only apply for first-level records (not related data).
  28. * So it is usually better to insert some hazardous strings into all your tables and make your tests then as closely
  29. * to the reality as possible.
  30. *
  31. * @author Mark Scherer
  32. * @license http://opensource.org/licenses/mit-license.php MIT
  33. */
  34. class HazardableBehavior extends ModelBehavior {
  35. protected $_defaultConfig = [
  36. 'replaceFind' => false, // fake data after a find call (defaults to false)
  37. 'fields' => [], // additional fields or custom mapping to a specific snippet type (defaults to XSS)
  38. 'skipFields' => ['id', 'slug'] // fields of the schema that should be skipped
  39. ];
  40. protected $_snippets = [];
  41. /**
  42. * HazardableBehavior::setup()
  43. *
  44. * @param Model $Model
  45. * @param array $config
  46. * @return void
  47. */
  48. public function setup(Model $Model, $config = []) {
  49. $this->settings[$Model->alias] = $config + $this->_defaultConfig;
  50. }
  51. /**
  52. * BeforeSave() to inject the hazardous strings into the model data for save().
  53. *
  54. * Note: Remember to disable validation as you want to insert those strings just for
  55. * testing purposes.
  56. */
  57. public function beforeSave(Model $Model, $options = []) {
  58. $fields = $this->_fields($Model);
  59. foreach ($fields as $field) {
  60. $length = 0;
  61. $schema = $Model->schema($field);
  62. if (!empty($schema['length'])) {
  63. $length = $schema['length'];
  64. }
  65. $Model->data[$Model->alias][$field] = $this->_snippet($length);
  66. }
  67. return true;
  68. }
  69. /**
  70. * AfterFind() to inject the hazardous strings into the retrieved model data.
  71. * Only activate this if you have not persistently stored any hazardous strings yet.
  72. */
  73. public function afterFind(Model $Model, $results, $primary = false) {
  74. if (empty($this->settings[$Model->alias]['replaceFind'])) {
  75. return $results;
  76. }
  77. foreach ($results as $key => $result) {
  78. foreach ($result as $model => $row) {
  79. $fields = $this->_fields($Model);
  80. foreach ($fields as $field) {
  81. $length = 0;
  82. $schema = $Model->schema($field);
  83. if (!empty($schema['length'])) {
  84. $length = $schema['length'];
  85. }
  86. $results[$key][$model][$field] = $this->_snippet($length);
  87. }
  88. }
  89. }
  90. return $results;
  91. }
  92. /**
  93. * @param int $maxLength The lenght of the field if applicable to return a suitable snippet
  94. * @return string Hazardous string
  95. */
  96. protected function _snippet($maxLength = 0) {
  97. $snippets = $this->_snippets();
  98. $max = count($snippets) - 1;
  99. if ($maxLength) {
  100. foreach ($snippets as $key => $snippet) {
  101. if (mb_strlen($snippet) > $maxLength) {
  102. break;
  103. }
  104. $max = $key;
  105. }
  106. }
  107. $keyByChance = mt_rand(0, $max);
  108. return $snippets[$keyByChance];
  109. }
  110. /**
  111. * @return array
  112. */
  113. protected function _snippets() {
  114. if ($this->_snippets) {
  115. return $this->_snippets;
  116. }
  117. $snippetArray = HazardLib::xssStrings();
  118. $snippetArray[] = '<SCRIPT>alert(\'X\')</SCRIPT>';
  119. $snippetArray[] = '<';
  120. usort($snippetArray, [$this, '_sort']);
  121. $this->_snippets = $snippetArray;
  122. return $snippetArray;
  123. }
  124. /**
  125. * Sort all snippets by length (ASC)
  126. */
  127. protected function _sort($a, $b) {
  128. return strlen($a) - strlen($b);
  129. }
  130. /**
  131. * @return array
  132. */
  133. protected function _fields(Model $Model) {
  134. $fields = [];
  135. $schema = $Model->schema();
  136. foreach ($schema as $key => $field) {
  137. if (!in_array($field['type'], ['text', 'string'])) {
  138. continue;
  139. }
  140. if ($this->settings[$Model->alias]['skipFields'] && in_array($key, $this->settings[$Model->alias]['skipFields'])) {
  141. continue;
  142. }
  143. $fields[] = $key;
  144. }
  145. return $fields;
  146. }
  147. }