1
0

Monitor.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778
  1. <?php
  2. /**
  3. * PHPIDS
  4. *
  5. * Requirements: PHP5, SimpleXML
  6. *
  7. * Copyright (c) 2008 PHPIDS group (https://phpids.org)
  8. *
  9. * PHPIDS is free software; you can redistribute it and/or modify
  10. * it under the terms of the GNU Lesser General Public License as published by
  11. * the Free Software Foundation, version 3 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * PHPIDS is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Lesser General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Lesser General Public License
  20. * along with PHPIDS. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. * PHP version 5.1.6+
  23. *
  24. * @category Security
  25. * @package PHPIDS
  26. * @author Mario Heiderich <mario.heiderich@gmail.com>
  27. * @author Christian Matthies <ch0012@gmail.com>
  28. * @author Lars Strojny <lars@strojny.net>
  29. * @license http://www.gnu.org/licenses/lgpl.html LGPL
  30. * @link http://php-ids.org/
  31. */
  32. /**
  33. * Monitoring engine
  34. *
  35. * This class represents the core of the frameworks attack detection mechanism
  36. * and provides functions to scan incoming data for malicious appearing script
  37. * fragments.
  38. *
  39. * @category Security
  40. * @package PHPIDS
  41. * @author Christian Matthies <ch0012@gmail.com>
  42. * @author Mario Heiderich <mario.heiderich@gmail.com>
  43. * @author Lars Strojny <lars@strojny.net>
  44. * @copyright 2007-2009 The PHPIDS Group
  45. * @license http://www.gnu.org/licenses/lgpl.html LGPL
  46. * @version Release: $Id:Monitor.php 949 2008-06-28 01:26:03Z christ1an $
  47. * @link http://php-ids.org/
  48. */
  49. class IDS_Monitor
  50. {
  51. /**
  52. * Tags to define what to search for
  53. *
  54. * Accepted values are xss, csrf, sqli, dt, id, lfi, rfe, spam, dos
  55. *
  56. * @var array
  57. */
  58. private $tags = null;
  59. /**
  60. * Request array
  61. *
  62. * Array containing raw data to search in
  63. *
  64. * @var array
  65. */
  66. private $request = null;
  67. /**
  68. * Container for filter rules
  69. *
  70. * Holds an instance of IDS_Filter_Storage
  71. *
  72. * @var object
  73. */
  74. private $storage = null;
  75. /**
  76. * Results
  77. *
  78. * Holds an instance of IDS_Report which itself provides an API to
  79. * access the detected results
  80. *
  81. * @var object
  82. */
  83. private $report = null;
  84. /**
  85. * Scan keys switch
  86. *
  87. * Enabling this property will cause the monitor to scan both the key and
  88. * the value of variables
  89. *
  90. * @var boolean
  91. */
  92. public $scanKeys = false;
  93. /**
  94. * Exception container
  95. *
  96. * Using this array it is possible to define variables that must not be
  97. * scanned. Per default, utmz google analytics parameters are permitted.
  98. *
  99. * @var array
  100. */
  101. private $exceptions = array();
  102. /**
  103. * Html container
  104. *
  105. * Using this array it is possible to define variables that legally
  106. * contain html and have to be prepared before hitting the rules to
  107. * avoid too many false alerts
  108. *
  109. * @var array
  110. */
  111. private $html = array();
  112. /**
  113. * JSON container
  114. *
  115. * Using this array it is possible to define variables that contain
  116. * JSON data - and should be treated as such
  117. *
  118. * @var array
  119. */
  120. private $json = array();
  121. /**
  122. * Holds HTMLPurifier object
  123. *
  124. * @var object
  125. */
  126. private $htmlpurifier = NULL;
  127. /**
  128. * Path to HTMLPurifier source
  129. *
  130. * This path might be changed in case one wishes to make use of a
  131. * different HTMLPurifier source file e.g. if already used in the
  132. * application PHPIDS is protecting
  133. *
  134. * @var string
  135. */
  136. private $pathToHTMLPurifier = '';
  137. /**
  138. * HTMLPurifier cache directory
  139. *
  140. * @var string
  141. */
  142. private $HTMLPurifierCache = '';
  143. /**
  144. * This property holds the tmp JSON string from the
  145. * _jsonDecodeValues() callback
  146. *
  147. * @var string
  148. */
  149. private $tmpJsonString = '';
  150. /**
  151. * Constructor
  152. *
  153. * @param array $request array to scan
  154. * @param object $init instance of IDS_Init
  155. * @param array $tags list of tags to which filters should be applied
  156. *
  157. * @return void
  158. */
  159. public function __construct(array $request, IDS_Init $init, array $tags = null)
  160. {
  161. $version = isset($init->config['General']['min_php_version'])
  162. ? $init->config['General']['min_php_version'] : '5.1.6';
  163. if (version_compare(PHP_VERSION, $version, '<')) {
  164. throw new Exception(
  165. 'PHP version has to be equal or higher than ' . $version . ' or
  166. PHP version couldn\'t be determined'
  167. );
  168. }
  169. if (!empty($request)) {
  170. $this->storage = new IDS_Filter_Storage($init);
  171. $this->request = $request;
  172. $this->tags = $tags;
  173. $this->scanKeys = $init->config['General']['scan_keys'];
  174. $this->exceptions = isset($init->config['General']['exceptions'])
  175. ? $init->config['General']['exceptions'] : false;
  176. $this->html = isset($init->config['General']['html'])
  177. ? $init->config['General']['html'] : false;
  178. $this->json = isset($init->config['General']['json'])
  179. ? $init->config['General']['json'] : false;
  180. if(isset($init->config['General']['HTML_Purifier_Path'])
  181. && isset($init->config['General']['HTML_Purifier_Cache'])) {
  182. $this->pathToHTMLPurifier =
  183. $init->config['General']['HTML_Purifier_Path'];
  184. $this->HTMLPurifierCache = $init->getBasePath()
  185. . $init->config['General']['HTML_Purifier_Cache'];
  186. }
  187. }
  188. if (!is_writeable($init->getBasePath()
  189. . $init->config['General']['tmp_path'])) {
  190. throw new Exception(
  191. 'Please make sure the ' .
  192. htmlspecialchars($init->getBasePath() .
  193. $init->config['General']['tmp_path'], ENT_QUOTES, 'UTF-8') .
  194. ' folder is writable'
  195. );
  196. }
  197. include_once 'IDS/Report.php';
  198. $this->report = new IDS_Report;
  199. }
  200. /**
  201. * Starts the scan mechanism
  202. *
  203. * @return object IDS_Report
  204. */
  205. public function run()
  206. {
  207. if (!empty($this->request)) {
  208. foreach ($this->request as $key => $value) {
  209. $this->_iterate($key, $value);
  210. }
  211. }
  212. return $this->getReport();
  213. }
  214. /**
  215. * Iterates through given data and delegates it to IDS_Monitor::_detect() in
  216. * order to check for malicious appearing fragments
  217. *
  218. * @param mixed $key the former array key
  219. * @param mixed $value the former array value
  220. *
  221. * @return void
  222. */
  223. private function _iterate($key, $value)
  224. {
  225. if (!is_array($value)) {
  226. if (is_string($value)) {
  227. if ($filter = $this->_detect($key, $value)) {
  228. include_once 'IDS/Event.php';
  229. $this->report->addEvent(
  230. new IDS_Event(
  231. $key,
  232. $value,
  233. $filter
  234. )
  235. );
  236. }
  237. }
  238. } else {
  239. foreach ($value as $subKey => $subValue) {
  240. $this->_iterate($key . '.' . $subKey, $subValue);
  241. }
  242. }
  243. }
  244. /**
  245. * Checks whether given value matches any of the supplied filter patterns
  246. *
  247. * @param mixed $key the key of the value to scan
  248. * @param mixed $value the value to scan
  249. *
  250. * @return bool|array false or array of filter(s) that matched the value
  251. */
  252. private function _detect($key, $value)
  253. {
  254. // define the pre-filter
  255. $prefilter = '/[^\w\s\/@!?\.]+|(?:\.\/)|(?:@@\w+)'
  256. . '|(?:\+ADw)|(?:union\s+select)/i';
  257. // to increase performance, only start detection if value
  258. // isn't alphanumeric
  259. if (!$this->scanKeys
  260. && (!$value || !preg_match($prefilter, $value))) {
  261. return false;
  262. } elseif($this->scanKeys) {
  263. if((!$key || !preg_match($prefilter, $key))
  264. && (!$value || !preg_match($prefilter, $value))) {
  265. return false;
  266. }
  267. }
  268. // check if this field is part of the exceptions
  269. if (is_array($this->exceptions)) {
  270. foreach($this->exceptions as $exception) {
  271. $matches = array();
  272. if(preg_match('/(\/.*\/[^eE]*)$/', $exception, $matches)) {
  273. if(isset($matches[1]) && preg_match($matches[1], $key)) {
  274. return false;
  275. }
  276. } else {
  277. if($exception === $key) {
  278. return false;
  279. }
  280. }
  281. }
  282. }
  283. // check for magic quotes and remove them if necessary
  284. if (function_exists('get_magic_quotes_gpc')
  285. && get_magic_quotes_gpc()) {
  286. $value = stripslashes($value);
  287. }
  288. if(function_exists('get_magic_quotes_gpc')
  289. && !get_magic_quotes_gpc()
  290. && version_compare(PHP_VERSION, '5.3.0', '>=')) {
  291. $value = preg_replace('/\\\(["\'\/])/im', '$1', $value);
  292. }
  293. // if html monitoring is enabled for this field - then do it!
  294. if (is_array($this->html) && in_array($key, $this->html, true)) {
  295. list($key, $value) = $this->_purifyValues($key, $value);
  296. }
  297. // check if json monitoring is enabled for this field
  298. if (is_array($this->json) && in_array($key, $this->json, true)) {
  299. list($key, $value) = $this->_jsonDecodeValues($key, $value);
  300. }
  301. // use the converter
  302. include_once 'IDS/Converter.php';
  303. $value = IDS_Converter::runAll($value);
  304. $value = IDS_Converter::runCentrifuge($value, $this);
  305. // scan keys if activated via config
  306. $key = $this->scanKeys ? IDS_Converter::runAll($key)
  307. : $key;
  308. $key = $this->scanKeys ? IDS_Converter::runCentrifuge($key, $this)
  309. : $key;
  310. $filters = array();
  311. $filterSet = $this->storage->getFilterSet();
  312. foreach ($filterSet as $filter) {
  313. /*
  314. * in case we have a tag array specified the IDS will only
  315. * use those filters that are meant to detect any of the
  316. * defined tags
  317. */
  318. if (is_array($this->tags)) {
  319. if (array_intersect($this->tags, $filter->getTags())) {
  320. if ($this->_match($key, $value, $filter)) {
  321. $filters[] = $filter;
  322. }
  323. }
  324. } else {
  325. if ($this->_match($key, $value, $filter)) {
  326. $filters[] = $filter;
  327. }
  328. }
  329. }
  330. return empty($filters) ? false : $filters;
  331. }
  332. /**
  333. * Purifies given key and value variables using HTMLPurifier
  334. *
  335. * This function is needed whenever there is variables for which HTML
  336. * might be allowed like e.g. WYSIWYG post bodies. It will dectect malicious
  337. * code fragments and leaves harmless parts untouched.
  338. *
  339. * @param mixed $key
  340. * @param mixed $value
  341. * @since 0.5
  342. * @throws Exception
  343. *
  344. * @return array
  345. */
  346. private function _purifyValues($key, $value)
  347. {
  348. /*
  349. * Perform a pre-check if string is valid for purification
  350. */
  351. if(!$this->_purifierPreCheck($key, $value)) {
  352. return array($key, $value);
  353. }
  354. include_once $this->pathToHTMLPurifier;
  355. if (!is_writeable($this->HTMLPurifierCache)) {
  356. throw new Exception(
  357. $this->HTMLPurifierCache . ' must be writeable');
  358. }
  359. if (class_exists('HTMLPurifier')) {
  360. $config = HTMLPurifier_Config::createDefault();
  361. $config->set('Attr.EnableID', true);
  362. $config->set('Cache.SerializerPath', $this->HTMLPurifierCache);
  363. $config->set('Output.Newline', "\n");
  364. $this->htmlpurifier = new HTMLPurifier($config);
  365. } else {
  366. throw new Exception(
  367. 'HTMLPurifier class could not be found - ' .
  368. 'make sure the purifier files are valid and' .
  369. ' the path is correct'
  370. );
  371. }
  372. $value = preg_replace('/[\x0b-\x0c]/', ' ', $value);
  373. $key = preg_replace('/[\x0b-\x0c]/', ' ', $key);
  374. $purified_value = $this->htmlpurifier->purify($value);
  375. $purified_key = $this->htmlpurifier->purify($key);
  376. $redux_value = strip_tags($value);
  377. $redux_key = strip_tags($key);
  378. if ($value != $purified_value || $redux_value) {
  379. $value = $this->_diff($value, $purified_value, $redux_value);
  380. } else {
  381. $value = NULL;
  382. }
  383. if ($key != $purified_key) {
  384. $key = $this->_diff($key, $purified_key, $redux_key);
  385. } else {
  386. $key = NULL;
  387. }
  388. return array($key, $value);
  389. }
  390. /**
  391. * This method makes sure no dangerous markup can be smuggled in
  392. * attributes when HTML mode is switched on.
  393. *
  394. * If the precheck considers the string too dangerous for
  395. * purification false is being returned.
  396. *
  397. * @param mixed $key
  398. * @param mixed $value
  399. * @since 0.6
  400. *
  401. * @return boolean
  402. */
  403. private function _purifierPreCheck($key = '', $value = '')
  404. {
  405. /*
  406. * Remove control chars before pre-check
  407. */
  408. $tmp_value = preg_replace('/\p{C}/', null, $value);
  409. $tmp_key = preg_replace('/\p{C}/', null, $key);
  410. $precheck = '/<(script|iframe|applet|object)\W/i';
  411. if(preg_match($precheck, $tmp_key)
  412. || preg_match($precheck, $tmp_value)) {
  413. return false;
  414. }
  415. return true;
  416. }
  417. /**
  418. * This method calculates the difference between the original
  419. * and the purified markup strings.
  420. *
  421. * @param string $original the original markup
  422. * @param string $purified the purified markup
  423. * @param string $redux the string without html
  424. * @since 0.5
  425. *
  426. * @return string the difference between the strings
  427. */
  428. private function _diff($original, $purified, $redux)
  429. {
  430. /*
  431. * deal with over-sensitive alt-attribute addition of the purifier
  432. * and other common html formatting problems
  433. */
  434. $purified = preg_replace('/\s+alt="[^"]*"/m', null, $purified);
  435. $purified = preg_replace('/=?\s*"\s*"/m', null, $purified);
  436. $original = preg_replace('/\s+alt="[^"]*"/m', null, $original);
  437. $original = preg_replace('/=?\s*"\s*"/m', null, $original);
  438. $original = preg_replace('/style\s*=\s*([^"])/m', 'style = "$1', $original);
  439. # deal with oversensitive CSS normalization
  440. $original = preg_replace('/(?:([\w\-]+:)+\s*([^;]+;\s*))/m', '$1$2', $original);
  441. # strip whitespace between tags
  442. $original = trim(preg_replace('/>\s*</m', '><', $original));
  443. $purified = trim(preg_replace('/>\s*</m', '><', $purified));
  444. $original = preg_replace(
  445. '/(=\s*(["\'`])[^>"\'`]*>[^>"\'`]*["\'`])/m', 'alt$1', $original
  446. );
  447. // no purified html is left
  448. if (!$purified) {
  449. return $original;
  450. }
  451. // calculate the diff length
  452. $length = mb_strlen($original) - mb_strlen($purified);
  453. /*
  454. * Calculate the difference between the original html input
  455. * and the purified string.
  456. */
  457. $array_1 = preg_split('/(?<!^)(?!$)/u', html_entity_decode(urldecode($original)));
  458. $array_2 = preg_split('/(?<!^)(?!$)/u', $purified);
  459. // create an array containing the single character differences
  460. $differences = array();
  461. foreach ($array_1 as $key => $value) {
  462. if (!isset($array_2[$key]) || $value !== $array_2[$key]) {
  463. $differences[] = $value;
  464. }
  465. }
  466. // return the diff - ready to hit the converter and the rules
  467. if(intval($length) <= 10) {
  468. $diff = trim(join('', $differences));
  469. } else {
  470. $diff = mb_substr(trim(join('', $differences)), 0, strlen($original));
  471. }
  472. // clean up spaces between tag delimiters
  473. $diff = preg_replace('/>\s*</m', '><', $diff);
  474. // correct over-sensitively stripped bad html elements
  475. $diff = preg_replace('/[^<](iframe|script|embed|object' .
  476. '|applet|base|img|style)/m', '<$1', $diff);
  477. if (mb_strlen($diff) < 4) {
  478. return null;
  479. }
  480. return $diff . $redux;
  481. }
  482. /**
  483. * This method prepares incoming JSON data for the PHPIDS detection
  484. * process. It utilizes _jsonConcatContents() as callback and returns a
  485. * string version of the JSON data structures.
  486. *
  487. * @param mixed $key
  488. * @param mixed $value
  489. * @since 0.5.3
  490. *
  491. * @return array
  492. */
  493. private function _jsonDecodeValues($key, $value) {
  494. $tmp_key = json_decode($key);
  495. $tmp_value = json_decode($value);
  496. if($tmp_value && is_array($tmp_value) || is_object($tmp_value)) {
  497. array_walk_recursive($tmp_value, array($this, '_jsonConcatContents'));
  498. $value = $this->tmpJsonString;
  499. } else {
  500. $this->tmpJsonString .= " " . $tmp_value . "\n";
  501. }
  502. if($tmp_key && is_array($tmp_key) || is_object($tmp_key)) {
  503. array_walk_recursive($tmp_key, array($this, '_jsonConcatContents'));
  504. $key = $this->tmpJsonString;
  505. } else {
  506. $this->tmpJsonString .= " " . $tmp_key . "\n";
  507. }
  508. return array($key, $value);
  509. }
  510. /**
  511. * This is the callback used in _jsonDecodeValues(). The method
  512. * concatenates key and value and stores them in $this->tmpJsonString.
  513. *
  514. * @param mixed $key
  515. * @param mixed $value
  516. * @since 0.5.3
  517. *
  518. * @return void
  519. */
  520. private function _jsonConcatContents($key, $value) {
  521. if(is_string($key) && is_string($value)) {
  522. $this->tmpJsonString .= $key . " " . $value . "\n";
  523. } else {
  524. $this->_jsonDecodeValues(
  525. json_encode($key), json_encode($value)
  526. );
  527. }
  528. }
  529. /**
  530. * Matches given value and/or key against given filter
  531. *
  532. * @param mixed $key the key to optionally scan
  533. * @param mixed $value the value to scan
  534. * @param object $filter the filter object
  535. *
  536. * @return boolean
  537. */
  538. private function _match($key, $value, $filter)
  539. {
  540. if ($this->scanKeys) {
  541. if ($filter->match($key)) {
  542. return true;
  543. }
  544. }
  545. if ($filter->match($value)) {
  546. return true;
  547. }
  548. return false;
  549. }
  550. /**
  551. * Sets exception array
  552. *
  553. * @param mixed $exceptions the thrown exceptions
  554. *
  555. * @return void
  556. */
  557. public function setExceptions($exceptions)
  558. {
  559. if (!is_array($exceptions)) {
  560. $exceptions = array($exceptions);
  561. }
  562. $this->exceptions = $exceptions;
  563. }
  564. /**
  565. * Returns exception array
  566. *
  567. * @return array
  568. */
  569. public function getExceptions()
  570. {
  571. return $this->exceptions;
  572. }
  573. /**
  574. * Sets html array
  575. *
  576. * @param mixed $html the fields containing html
  577. * @since 0.5
  578. *
  579. * @return void
  580. */
  581. public function setHtml($html)
  582. {
  583. if (!is_array($html)) {
  584. $html = array($html);
  585. }
  586. $this->html = $html;
  587. }
  588. /**
  589. * Adds a value to the html array
  590. *
  591. * @since 0.5
  592. *
  593. * @return void
  594. */
  595. public function addHtml($value)
  596. {
  597. $this->html[] = $value;
  598. }
  599. /**
  600. * Returns html array
  601. *
  602. * @since 0.5
  603. *
  604. * @return array the fields that contain allowed html
  605. */
  606. public function getHtml()
  607. {
  608. return $this->html;
  609. }
  610. /**
  611. * Sets json array
  612. *
  613. * @param mixed $json the fields containing json
  614. * @since 0.5.3
  615. *
  616. * @return void
  617. */
  618. public function setJson($json)
  619. {
  620. if (!is_array($json)) {
  621. $json = array($json);
  622. }
  623. $this->json = $json;
  624. }
  625. /**
  626. * Adds a value to the json array
  627. *
  628. * @param string the value containing JSON data
  629. * @since 0.5.3
  630. *
  631. * @return void
  632. */
  633. public function addJson($value)
  634. {
  635. $this->json[] = $value;
  636. }
  637. /**
  638. * Returns json array
  639. *
  640. * @since 0.5.3
  641. *
  642. * @return array the fields that contain json
  643. */
  644. public function getJson()
  645. {
  646. return $this->json;
  647. }
  648. /**
  649. * Returns storage container
  650. *
  651. * @return array
  652. */
  653. public function getStorage()
  654. {
  655. return $this->storage;
  656. }
  657. /**
  658. * Returns report object providing various functions to work with
  659. * detected results. Also the centrifuge data is being set as property
  660. * of the report object.
  661. *
  662. * @return object IDS_Report
  663. */
  664. public function getReport()
  665. {
  666. if (isset($this->centrifuge) && $this->centrifuge) {
  667. $this->report->setCentrifuge($this->centrifuge);
  668. }
  669. return $this->report;
  670. }
  671. }
  672. /**
  673. * Local variables:
  674. * tab-width: 4
  675. * c-basic-offset: 4
  676. * End:
  677. * vim600: sw=4 ts=4 expandtab
  678. */