. * * PHP version 5.1.6+ * * @category Security * @package PHPIDS * @author Mario Heiderich * @author Christian Matthies * @author Lars Strojny * @license http://www.gnu.org/licenses/lgpl.html LGPL * @link http://php-ids.org/ */ /** * Monitoring engine * * This class represents the core of the frameworks attack detection mechanism * and provides functions to scan incoming data for malicious appearing script * fragments. * * @category Security * @package PHPIDS * @author Christian Matthies * @author Mario Heiderich * @author Lars Strojny * @copyright 2007-2009 The PHPIDS Group * @license http://www.gnu.org/licenses/lgpl.html LGPL * @version Release: $Id:Monitor.php 949 2008-06-28 01:26:03Z christ1an $ * @link http://php-ids.org/ */ class IDS_Monitor { /** * Tags to define what to search for * * Accepted values are xss, csrf, sqli, dt, id, lfi, rfe, spam, dos * * @var array */ private $tags = null; /** * Request array * * Array containing raw data to search in * * @var array */ private $request = null; /** * Container for filter rules * * Holds an instance of IDS_Filter_Storage * * @var object */ private $storage = null; /** * Results * * Holds an instance of IDS_Report which itself provides an API to * access the detected results * * @var object */ private $report = null; /** * Scan keys switch * * Enabling this property will cause the monitor to scan both the key and * the value of variables * * @var boolean */ public $scanKeys = false; /** * Exception container * * Using this array it is possible to define variables that must not be * scanned. Per default, utmz google analytics parameters are permitted. * * @var array */ private $exceptions = array(); /** * Html container * * Using this array it is possible to define variables that legally * contain html and have to be prepared before hitting the rules to * avoid too many false alerts * * @var array */ private $html = array(); /** * JSON container * * Using this array it is possible to define variables that contain * JSON data - and should be treated as such * * @var array */ private $json = array(); /** * Holds HTMLPurifier object * * @var object */ private $htmlpurifier = NULL; /** * Path to HTMLPurifier source * * This path might be changed in case one wishes to make use of a * different HTMLPurifier source file e.g. if already used in the * application PHPIDS is protecting * * @var string */ private $pathToHTMLPurifier = ''; /** * HTMLPurifier cache directory * * @var string */ private $HTMLPurifierCache = ''; /** * This property holds the tmp JSON string from the * _jsonDecodeValues() callback * * @var string */ private $tmpJsonString = ''; /** * Constructor * * @param array $request array to scan * @param object $init instance of IDS_Init * @param array $tags list of tags to which filters should be applied * * @return void */ public function __construct(array $request, IDS_Init $init, array $tags = null) { $version = isset($init->config['General']['min_php_version']) ? $init->config['General']['min_php_version'] : '5.1.6'; if (version_compare(PHP_VERSION, $version, '<')) { throw new Exception( 'PHP version has to be equal or higher than ' . $version . ' or PHP version couldn\'t be determined' ); } if (!empty($request)) { $this->storage = new IDS_Filter_Storage($init); $this->request = $request; $this->tags = $tags; $this->scanKeys = $init->config['General']['scan_keys']; $this->exceptions = isset($init->config['General']['exceptions']) ? $init->config['General']['exceptions'] : false; $this->html = isset($init->config['General']['html']) ? $init->config['General']['html'] : false; $this->json = isset($init->config['General']['json']) ? $init->config['General']['json'] : false; if(isset($init->config['General']['HTML_Purifier_Path']) && isset($init->config['General']['HTML_Purifier_Cache'])) { $this->pathToHTMLPurifier = $init->config['General']['HTML_Purifier_Path']; $this->HTMLPurifierCache = $init->getBasePath() . $init->config['General']['HTML_Purifier_Cache']; } } if (!is_writeable($init->getBasePath() . $init->config['General']['tmp_path'])) { throw new Exception( 'Please make sure the ' . htmlspecialchars($init->getBasePath() . $init->config['General']['tmp_path'], ENT_QUOTES, 'UTF-8') . ' folder is writable' ); } include_once 'IDS/Report.php'; $this->report = new IDS_Report; } /** * Starts the scan mechanism * * @return object IDS_Report */ public function run() { if (!empty($this->request)) { foreach ($this->request as $key => $value) { $this->_iterate($key, $value); } } return $this->getReport(); } /** * Iterates through given data and delegates it to IDS_Monitor::_detect() in * order to check for malicious appearing fragments * * @param mixed $key the former array key * @param mixed $value the former array value * * @return void */ private function _iterate($key, $value) { if (!is_array($value)) { if (is_string($value)) { if ($filter = $this->_detect($key, $value)) { include_once 'IDS/Event.php'; $this->report->addEvent( new IDS_Event( $key, $value, $filter ) ); } } } else { foreach ($value as $subKey => $subValue) { $this->_iterate($key . '.' . $subKey, $subValue); } } } /** * Checks whether given value matches any of the supplied filter patterns * * @param mixed $key the key of the value to scan * @param mixed $value the value to scan * * @return bool|array false or array of filter(s) that matched the value */ private function _detect($key, $value) { // define the pre-filter $prefilter = '/[^\w\s\/@!?\.]+|(?:\.\/)|(?:@@\w+)' . '|(?:\+ADw)|(?:union\s+select)/i'; // to increase performance, only start detection if value // isn't alphanumeric if (!$this->scanKeys && (!$value || !preg_match($prefilter, $value))) { return false; } elseif($this->scanKeys) { if((!$key || !preg_match($prefilter, $key)) && (!$value || !preg_match($prefilter, $value))) { return false; } } // check if this field is part of the exceptions if (is_array($this->exceptions)) { foreach($this->exceptions as $exception) { $matches = array(); if(preg_match('/(\/.*\/[^eE]*)$/', $exception, $matches)) { if(isset($matches[1]) && preg_match($matches[1], $key)) { return false; } } else { if($exception === $key) { return false; } } } } // check for magic quotes and remove them if necessary if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) { $value = stripslashes($value); } if(function_exists('get_magic_quotes_gpc') && !get_magic_quotes_gpc() && version_compare(PHP_VERSION, '5.3.0', '>=')) { $value = preg_replace('/\\\(["\'\/])/im', '$1', $value); } // if html monitoring is enabled for this field - then do it! if (is_array($this->html) && in_array($key, $this->html, true)) { list($key, $value) = $this->_purifyValues($key, $value); } // check if json monitoring is enabled for this field if (is_array($this->json) && in_array($key, $this->json, true)) { list($key, $value) = $this->_jsonDecodeValues($key, $value); } // use the converter include_once 'IDS/Converter.php'; $value = IDS_Converter::runAll($value); $value = IDS_Converter::runCentrifuge($value, $this); // scan keys if activated via config $key = $this->scanKeys ? IDS_Converter::runAll($key) : $key; $key = $this->scanKeys ? IDS_Converter::runCentrifuge($key, $this) : $key; $filters = array(); $filterSet = $this->storage->getFilterSet(); foreach ($filterSet as $filter) { /* * in case we have a tag array specified the IDS will only * use those filters that are meant to detect any of the * defined tags */ if (is_array($this->tags)) { if (array_intersect($this->tags, $filter->getTags())) { if ($this->_match($key, $value, $filter)) { $filters[] = $filter; } } } else { if ($this->_match($key, $value, $filter)) { $filters[] = $filter; } } } return empty($filters) ? false : $filters; } /** * Purifies given key and value variables using HTMLPurifier * * This function is needed whenever there is variables for which HTML * might be allowed like e.g. WYSIWYG post bodies. It will dectect malicious * code fragments and leaves harmless parts untouched. * * @param mixed $key * @param mixed $value * @since 0.5 * @throws Exception * * @return array */ private function _purifyValues($key, $value) { /* * Perform a pre-check if string is valid for purification */ if(!$this->_purifierPreCheck($key, $value)) { return array($key, $value); } include_once $this->pathToHTMLPurifier; if (!is_writeable($this->HTMLPurifierCache)) { throw new Exception( $this->HTMLPurifierCache . ' must be writeable'); } if (class_exists('HTMLPurifier')) { $config = HTMLPurifier_Config::createDefault(); $config->set('Attr.EnableID', true); $config->set('Cache.SerializerPath', $this->HTMLPurifierCache); $config->set('Output.Newline', "\n"); $this->htmlpurifier = new HTMLPurifier($config); } else { throw new Exception( 'HTMLPurifier class could not be found - ' . 'make sure the purifier files are valid and' . ' the path is correct' ); } $value = preg_replace('/[\x0b-\x0c]/', ' ', $value); $key = preg_replace('/[\x0b-\x0c]/', ' ', $key); $purified_value = $this->htmlpurifier->purify($value); $purified_key = $this->htmlpurifier->purify($key); $redux_value = strip_tags($value); $redux_key = strip_tags($key); if ($value != $purified_value || $redux_value) { $value = $this->_diff($value, $purified_value, $redux_value); } else { $value = NULL; } if ($key != $purified_key) { $key = $this->_diff($key, $purified_key, $redux_key); } else { $key = NULL; } return array($key, $value); } /** * This method makes sure no dangerous markup can be smuggled in * attributes when HTML mode is switched on. * * If the precheck considers the string too dangerous for * purification false is being returned. * * @param mixed $key * @param mixed $value * @since 0.6 * * @return boolean */ private function _purifierPreCheck($key = '', $value = '') { /* * Remove control chars before pre-check */ $tmp_value = preg_replace('/\p{C}/', null, $value); $tmp_key = preg_replace('/\p{C}/', null, $key); $precheck = '/<(script|iframe|applet|object)\W/i'; if(preg_match($precheck, $tmp_key) || preg_match($precheck, $tmp_value)) { return false; } return true; } /** * This method calculates the difference between the original * and the purified markup strings. * * @param string $original the original markup * @param string $purified the purified markup * @param string $redux the string without html * @since 0.5 * * @return string the difference between the strings */ private function _diff($original, $purified, $redux) { /* * deal with over-sensitive alt-attribute addition of the purifier * and other common html formatting problems */ $purified = preg_replace('/\s+alt="[^"]*"/m', null, $purified); $purified = preg_replace('/=?\s*"\s*"/m', null, $purified); $original = preg_replace('/\s+alt="[^"]*"/m', null, $original); $original = preg_replace('/=?\s*"\s*"/m', null, $original); $original = preg_replace('/style\s*=\s*([^"])/m', 'style = "$1', $original); # deal with oversensitive CSS normalization $original = preg_replace('/(?:([\w\-]+:)+\s*([^;]+;\s*))/m', '$1$2', $original); # strip whitespace between tags $original = trim(preg_replace('/>\s*<', $original)); $purified = trim(preg_replace('/>\s*<', $purified)); $original = preg_replace( '/(=\s*(["\'`])[^>"\'`]*>[^>"\'`]*["\'`])/m', 'alt$1', $original ); // no purified html is left if (!$purified) { return $original; } // calculate the diff length $length = mb_strlen($original) - mb_strlen($purified); /* * Calculate the difference between the original html input * and the purified string. */ $array_1 = preg_split('/(? $value) { if (!isset($array_2[$key]) || $value !== $array_2[$key]) { $differences[] = $value; } } // return the diff - ready to hit the converter and the rules if(intval($length) <= 10) { $diff = trim(join('', $differences)); } else { $diff = mb_substr(trim(join('', $differences)), 0, strlen($original)); } // clean up spaces between tag delimiters $diff = preg_replace('/>\s*<', $diff); // correct over-sensitively stripped bad html elements $diff = preg_replace('/[^<](iframe|script|embed|object' . '|applet|base|img|style)/m', '<$1', $diff); if (mb_strlen($diff) < 4) { return null; } return $diff . $redux; } /** * This method prepares incoming JSON data for the PHPIDS detection * process. It utilizes _jsonConcatContents() as callback and returns a * string version of the JSON data structures. * * @param mixed $key * @param mixed $value * @since 0.5.3 * * @return array */ private function _jsonDecodeValues($key, $value) { $tmp_key = json_decode($key); $tmp_value = json_decode($value); if($tmp_value && is_array($tmp_value) || is_object($tmp_value)) { array_walk_recursive($tmp_value, array($this, '_jsonConcatContents')); $value = $this->tmpJsonString; } else { $this->tmpJsonString .= " " . $tmp_value . "\n"; } if($tmp_key && is_array($tmp_key) || is_object($tmp_key)) { array_walk_recursive($tmp_key, array($this, '_jsonConcatContents')); $key = $this->tmpJsonString; } else { $this->tmpJsonString .= " " . $tmp_key . "\n"; } return array($key, $value); } /** * This is the callback used in _jsonDecodeValues(). The method * concatenates key and value and stores them in $this->tmpJsonString. * * @param mixed $key * @param mixed $value * @since 0.5.3 * * @return void */ private function _jsonConcatContents($key, $value) { if(is_string($key) && is_string($value)) { $this->tmpJsonString .= $key . " " . $value . "\n"; } else { $this->_jsonDecodeValues( json_encode($key), json_encode($value) ); } } /** * Matches given value and/or key against given filter * * @param mixed $key the key to optionally scan * @param mixed $value the value to scan * @param object $filter the filter object * * @return boolean */ private function _match($key, $value, $filter) { if ($this->scanKeys) { if ($filter->match($key)) { return true; } } if ($filter->match($value)) { return true; } return false; } /** * Sets exception array * * @param mixed $exceptions the thrown exceptions * * @return void */ public function setExceptions($exceptions) { if (!is_array($exceptions)) { $exceptions = array($exceptions); } $this->exceptions = $exceptions; } /** * Returns exception array * * @return array */ public function getExceptions() { return $this->exceptions; } /** * Sets html array * * @param mixed $html the fields containing html * @since 0.5 * * @return void */ public function setHtml($html) { if (!is_array($html)) { $html = array($html); } $this->html = $html; } /** * Adds a value to the html array * * @since 0.5 * * @return void */ public function addHtml($value) { $this->html[] = $value; } /** * Returns html array * * @since 0.5 * * @return array the fields that contain allowed html */ public function getHtml() { return $this->html; } /** * Sets json array * * @param mixed $json the fields containing json * @since 0.5.3 * * @return void */ public function setJson($json) { if (!is_array($json)) { $json = array($json); } $this->json = $json; } /** * Adds a value to the json array * * @param string the value containing JSON data * @since 0.5.3 * * @return void */ public function addJson($value) { $this->json[] = $value; } /** * Returns json array * * @since 0.5.3 * * @return array the fields that contain json */ public function getJson() { return $this->json; } /** * Returns storage container * * @return array */ public function getStorage() { return $this->storage; } /** * Returns report object providing various functions to work with * detected results. Also the centrifuge data is being set as property * of the report object. * * @return object IDS_Report */ public function getReport() { if (isset($this->centrifuge) && $this->centrifuge) { $this->report->setCentrifuge($this->centrifuge); } return $this->report; } } /** * Local variables: * tab-width: 4 * c-basic-offset: 4 * End: * vim600: sw=4 ts=4 expandtab */