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