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