浏览代码

Opensource HazardableBehavior

euromark 13 年之前
父节点
当前提交
cf5e0779b9
共有 3 个文件被更改,包括 241 次插入15 次删除
  1. 17 15
      Lib/HazardLib.php
  2. 157 0
      Model/Behavior/HazardableBehavior.php
  3. 67 0
      Test/Case/Model/Behavior/HazardableBehaviorTest.php

+ 17 - 15
Lib/HazardLib.php

@@ -3,23 +3,25 @@
 App::uses('Xml', 'Utility');
 
 /**
- * get dangerous strings for various security checks
+ * Get dangerous strings for various security checks
  *
  * used in configurations controller + debug helper
+ *
+ * @author Mark Scherer
+ * @license MIT
  * 2010-07-30 ms
  */
 class HazardLib {
 
 	const URL = 'http://ha.ckers.org/xssAttacks.xml';
 
-
 	/**
 	 * get dangerous sql strings to test with
 	 * @return array
 	 * @static
 	 * 2010-07-31 ms
 	 */
-	public function sqlStrings($veryDangerousToo = false) {
+	public static function sqlStrings($veryDangerousToo = false) {
 		/*
 		$res = array(
 			"SELECT * FROM users WHERE email = 'x'; INSERT INTO users ('username', 'password') VALUES ('x', 'y');--"
@@ -41,14 +43,12 @@ class HazardLib {
 		return $strings;
 	}
 
-
 	/**
 	 * get dangerous php strings to test with
 	 * @return array
-	 * @static
 	 * 2010-07-31 ms
 	 */
-	public function phpStrings() {
+	public static function phpStrings() {
 		$res = array(
 			'a:100000000:{}', # serialized objects run the magic _ _wakeup() function when they're unserialized
 			':3:"PDO":0:{}' # If the PDO extension is enabled -- and it is by default in PHP 5 -- you can cause a fatal error
@@ -56,21 +56,22 @@ class HazardLib {
 		return $res;
 	}
 
-
 	/**
 	 * get dangerous html strings to test with
 	 * @return array
-	 * @static
 	 * 2010-07-31 ms
 	 */
-	public function xssStrings($cache = true) {
+	public static function xssStrings($cache = true) {
 		if ($cache) {
-			$texts = Cache::read('security_lib_texts');
+			$texts = Cache::read('hazard_lib_texts');
 		}
 		if (empty($texts)) {
 			$texts = array();
-			$contents = $this->_parseXml(self::URL);
+			$contents = self::_parseXml(self::URL);
 			foreach ($contents as $content) {
+				if ($content['code'] === 'See Below') {
+					continue;
+				}
 				$texts[] = $content['code'];
 			}
 			if (empty($texts)) {
@@ -78,19 +79,20 @@ class HazardLib {
 				return array();
 			}
 			if ($cache) {
-				Cache::write('security_lib_texts', $texts);
+				Cache::write('hazard_lib_texts', $texts);
 			}
 
 		}
 		return $texts;
 	}
 
-
 	/**
-	 * parse xml
+	 * Parse xml
+	 *
+	 * @return array
 	 * 2010-02-07 ms
 	 */
-	public function _parseXml($file) {
+	protected static function _parseXml($file) {
 		$xml = Xml::build($file);
 		$res = Xml::toArray($xml);
 

+ 157 - 0
Model/Behavior/HazardableBehavior.php

@@ -0,0 +1,157 @@
+<?php
+App::uses('ModelBehavior', 'Model');
+App::uses('HazardLib', 'Tools.Lib');
+
+/**
+ * Uses the HazardLib to test well known injection snippets of all kinds (including XSS, SQL)
+ * and tests your db wrapper methods or populates your records with it.
+ * Use this ONLY for testing environments and never on your live data.
+ *
+ * Available snippet types are XSS, PHP and SQL.
+ *
+ * The main concern is not just "eval" users that might try to hijack/abuse your site.
+ * Not properly securing your view also means that strings like `some>cool<string` will most likely mess up your HTML.
+ * The view could be rendered as a complete mess without the user or the developer knowing it. It might have even been
+ * the admin which inserted those layout-breaking strings, after all.
+ * That's why it is so important to follow the rule "do NOT sanitize, validate input, escape output" (there are exceptions, of course).
+ * Also make sure, you already cover those basics in your baking template. This will save a lot of time in the long run.
+ *
+ * If you inserted records go and browse your backend and especially your frontend.
+ * Everywhere where you get some alert or strange behavior, you might have forgotten to use h() or other
+ * measures to secure your output properly.
+ *
+ * You can also apply this behavior globally to overwrite all strings in your application temporarily.
+ * This way you don't need to modify the database. On output it will just inject the hazardous strings and
+ * you can browse your website just as if they were actually stored in your db.
+ * Either add it to some models or even the AppModel (temporarily!) as `$actsAs = array('Tools.Hazardable'))`
+ * A known limitation of Cake behaviors is, though, that this would only apply for first-level records (not related data).
+ * So it is usually better to insert some hazardous strings into all your tables and make your tests then as closely
+ * to the reality as possible.
+ *
+ * @author Mark Scherer
+ * @license MIT
+ * 2013-02-25 ms
+ */
+class HazardableBehavior extends ModelBehavior {
+
+	protected $_defaults = array(
+		'replaceFind' => false, // fake data after a find call (defaults to false)
+		'fields' => array(), // additional fields or custom mapping to a specific snippet type (defaults to XSS)
+		'skipFields' => array('id', 'slug') // fields of the schema that should be skipped
+	);
+
+	public $snippets = array();
+
+	public function setup(Model $Model, $config = array()) {
+		$this->settings[$Model->alias] = array_merge($this->_defaults, $config);
+	}
+
+	/**
+	 * beforeSave() to inject the hazardous strings into the model data for save().
+	 *
+	 * Note: Remember to disable validation as you want to insert those strings just for
+	 * testing purposes.
+	 */
+	public function beforeSave(Model $Model) {
+		$fields = $this->_fields($Model);
+		foreach ($fields as $field) {
+			$length = 0;
+			$schema = $Model->schema($field);
+			if (!empty($schema['length'])) {
+				$length = $schema['length'];
+			}
+			$Model->data[$Model->alias][$field] = $this->_snippet($length);
+		}
+
+		return true;
+	}
+
+	/**
+	 * afterFind() to inject the hazardous strings into the retrieved model data.
+	 * Only activate this if you have not persistently stored any hazardous strings yet.
+	 */
+	public function afterFind(Model $Model, $results, $primary) {
+		if (empty($this->settings[$Model->alias]['replaceFind'])) {
+			return $results;
+		}
+
+		foreach ($results as $key => $result) {
+			foreach ($result as $model => $row) {
+				$fields = $this->_fields($Model);
+				foreach ($fields as $field) {
+					$length = 0;
+					$schema = $Model->schema($field);
+					if (!empty($schema['length'])) {
+						$length = $schema['length'];
+					}
+					$results[$key][$model][$field] = $this->_snippet($length);
+				}
+			}
+		}
+		return $results;
+	}
+
+	/**
+	 * @param int $maxLength The lenght of the field if applicable to return a suitable snippet
+	 * @return string Hazardous string
+	 */
+	protected function _snippet($maxLength = 0) {
+		$snippets = $this->_snippets();
+		$max = count($snippets) - 1;
+
+		if ($maxLength) {
+			foreach ($snippets as $key => $snippet) {
+				if (mb_strlen($snippet) > $maxLength) {
+					break;
+				}
+				$max = $key;
+			}
+		}
+
+		$keyByChance = mt_rand(0, $max);
+		return $snippets[$keyByChance];
+	}
+
+	/**
+	 * @return array
+	 */
+	protected function _snippets() {
+		if ($this->snippets) {
+			return $this->snippets;
+		}
+		$snippetArray = HazardLib::xssStrings();
+		$snippetArray[] = '<SCRIPT>alert(\'X\')</SCRIPT>';
+		$snippetArray[] = '<';
+
+		usort($snippetArray, array($this, '_sort'));
+
+		$this->snippets = $snippetArray;
+		return $snippetArray;
+	}
+
+	/**
+	 * Sort all snippets by length (ASC)
+	 */
+	protected function _sort($a, $b) {
+		return strlen($a) - strlen($b);
+	}
+
+	/**
+	 * @return array
+	 */
+	protected function _fields(Model $Model) {
+		$fields = array();
+		$schema = $Model->schema();
+		foreach ($schema as $key => $field) {
+			if (!in_array($field['type'], array('text', 'string'))) {
+				continue;
+			}
+			if ($this->settings[$Model->alias]['skipFields'] && in_array($key, $this->settings[$Model->alias]['skipFields'])) {
+				continue;
+			}
+			$fields[] = $key;
+		}
+		return $fields;
+	}
+
+}

+ 67 - 0
Test/Case/Model/Behavior/HazardableBehaviorTest.php

@@ -0,0 +1,67 @@
+<?php
+App::uses('HazardableBehavior', 'Tools.Model/Behavior');
+App::uses('MyCakeTestCase', 'Tools.TestSuite');
+
+class HazardableBehaviorTest extends MyCakeTestCase {
+
+	public $fixtures = array('core.comment');
+
+	public $Model;
+
+	public function setUp() {
+		$this->Model = ClassRegistry::init('Comment');
+
+		$this->Model->Behaviors->load('Tools.Hazardable', array());
+	}
+
+	public function tearDown() {
+		unset($this->Model);
+	}
+
+	public function testObject() {
+		$this->assertTrue(is_a($this->Model->Behaviors->Hazardable, 'HazardableBehavior'));
+	}
+
+	public function testSaveAndFind() {
+		$data = array(
+			'comment' => 'foo',
+		);
+		$this->Model->create();
+		$res = $this->Model->save($data);
+		$this->assertTrue((bool)$res);
+
+		$res = $this->Model->find('first', array('conditions' => array('id' => $this->Model->id)));
+		$this->assertTrue((bool)$res);
+
+		$this->assertEquals('<', $res['Comment']['published']);
+		$this->assertTrue(!empty($res['Comment']['comment']));
+
+		//echo $res['Comment']['comment'];ob_flush();
+	}
+
+	public function testFind() {
+		$this->Model->Behaviors->unload('Hazardable');
+		$data = array(
+			'comment' => 'foo',
+		);
+		$this->Model->create();
+		$res = $this->Model->save($data);
+		$this->assertTrue((bool)$res);
+
+		$res = $this->Model->find('first', array('conditions' => array('id' => $this->Model->id)));
+		$this->assertTrue((bool)$res);
+
+		$this->assertEquals('foo', $res['Comment']['comment']);
+
+		$this->Model->Behaviors->load('Tools.Hazardable', array('replaceFind' => true));
+		$res = $this->Model->find('first', array('conditions' => array('id' => $this->Model->id)));
+		$this->assertTrue((bool)$res);
+
+		$this->assertEquals('<', $res['Comment']['published']);
+		$this->assertTrue(!empty($res['Comment']['comment']));
+		$this->assertNotEquals('foo', $res['Comment']['comment']);
+
+		//echo $res['Comment']['comment'];ob_flush();
+	}
+
+}