blob: 63a6833d6eb17828a8242f7eb28d149c2d34ac86 [file] [log] [blame]
Derek Allarda72b60d2007-01-31 23:56:11 +00001<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
2/**
Derek Allardd2df9bc2007-04-15 17:41:17 +00003 * CodeIgniter
Derek Allarda72b60d2007-01-31 23:56:11 +00004 *
5 * An open source application development framework for PHP 4.3.2 or newer
6 *
7 * @package CodeIgniter
8 * @author Rick Ellis
Derek Allardd2df9bc2007-04-15 17:41:17 +00009 * @copyright Copyright (c) 2006, EllisLab, Inc.
Derek Allarda72b60d2007-01-31 23:56:11 +000010 * @license http://www.codeignitor.com/user_guide/license.html
11 * @link http://www.codeigniter.com
12 * @since Version 1.0
13 * @filesource
14 */
15
16// ------------------------------------------------------------------------
17
18/**
19 * Input Class
20 *
21 * Pre-processes global input data for security
22 *
23 * @package CodeIgniter
24 * @subpackage Libraries
25 * @category Input
26 * @author Rick Ellis
27 * @link http://www.codeigniter.com/user_guide/libraries/input.html
28 */
29class CI_Input {
30 var $use_xss_clean = FALSE;
31 var $ip_address = FALSE;
32 var $user_agent = FALSE;
33 var $allow_get_array = FALSE;
34
35 /**
36 * Constructor
37 *
38 * Sets whether to globally enable the XSS processing
39 * and whether to allow the $_GET array
40 *
41 * @access public
42 */
43 function CI_Input()
44 {
45 log_message('debug', "Input Class Initialized");
46
47 $CFG =& load_class('Config');
48 $this->use_xss_clean = ($CFG->item('global_xss_filtering') === TRUE) ? TRUE : FALSE;
49 $this->allow_get_array = ($CFG->item('enable_query_strings') === TRUE) ? TRUE : FALSE;
50 $this->_sanitize_globals();
51 }
52
53 // --------------------------------------------------------------------
54
55 /**
56 * Sanitize Globals
57 *
58 * This function does the following:
59 *
60 * Unsets $_GET data (if query strings are not enabled)
61 *
62 * Unsets all globals if register_globals is enabled
63 *
64 * Standardizes newline characters to \n
65 *
66 * @access private
67 * @return void
68 */
69 function _sanitize_globals()
70 {
71 // Unset globals. This is effectively the same as register_globals = off
72 foreach (array($_GET, $_POST, $_COOKIE) as $global)
73 {
74 if ( ! is_array($global))
75 {
76 global $global;
77 $$global = NULL;
78 }
79 else
80 {
81 foreach ($global as $key => $val)
82 {
83 global $$key;
84 $$key = NULL;
85 }
86 }
87 }
88
89 // Is $_GET data allowed? If not we'll set the $_GET to an empty array
90 if ($this->allow_get_array == FALSE)
91 {
92 $_GET = array();
93 }
Rick Ellis112569d2007-02-26 19:19:08 +000094 else
95 {
96 if (is_array($_GET) AND count($_GET) > 0)
97 {
98 foreach($_GET as $key => $val)
99 {
100 $_GET[$this->_clean_input_keys($key)] = $this->_clean_input_data($val);
101 }
102 }
103 }
Derek Allarda72b60d2007-01-31 23:56:11 +0000104
105 // Clean $_POST Data
106 if (is_array($_POST) AND count($_POST) > 0)
107 {
108 foreach($_POST as $key => $val)
109 {
110 $_POST[$this->_clean_input_keys($key)] = $this->_clean_input_data($val);
111 }
112 }
113
114 // Clean $_COOKIE Data
115 if (is_array($_COOKIE) AND count($_COOKIE) > 0)
116 {
117 foreach($_COOKIE as $key => $val)
118 {
119 $_COOKIE[$this->_clean_input_keys($key)] = $this->_clean_input_data($val);
120 }
121 }
122
123 log_message('debug', "Global POST and COOKIE data sanitized");
124 }
125
126 // --------------------------------------------------------------------
127
128 /**
129 * Clean Input Data
130 *
131 * This is a helper function. It escapes data and
132 * standardizes newline characters to \n
133 *
134 * @access private
135 * @param string
136 * @return string
137 */
138 function _clean_input_data($str)
139 {
140 if (is_array($str))
141 {
142 $new_array = array();
143 foreach ($str as $key => $val)
144 {
145 $new_array[$this->_clean_input_keys($key)] = $this->_clean_input_data($val);
146 }
147 return $new_array;
148 }
149
150 if ($this->use_xss_clean === TRUE)
151 {
152 $str = $this->xss_clean($str);
153 }
154
155 // Standardize newlines
156 return preg_replace("/\015\012|\015|\012/", "\n", $str);
157 }
158
159 // --------------------------------------------------------------------
160
161 /**
162 * Clean Keys
163 *
164 * This is a helper function. To prevent malicious users
165 * from trying to exploit keys we make sure that keys are
166 * only named with alpha-numeric text and a few other items.
167 *
168 * @access private
169 * @param string
170 * @return string
171 */
172 function _clean_input_keys($str)
173 {
174 if ( ! preg_match("/^[a-z0-9:_\/-]+$/i", $str))
175 {
176 exit('Disallowed Key Characters.');
177 }
178
179 if ( ! get_magic_quotes_gpc())
180 {
181 return addslashes($str);
182 }
183
184 return $str;
185 }
Rick Ellis112569d2007-02-26 19:19:08 +0000186
187 // --------------------------------------------------------------------
188
189 /**
190 * Fetch an item from the GET array
191 *
192 * @access public
193 * @param string
194 * @param bool
195 * @return string
196 */
Derek Allard87d1eeb2007-03-01 13:20:43 +0000197 function get($index = '', $xss_clean = FALSE)
Rick Ellis112569d2007-02-26 19:19:08 +0000198 {
199 if ( ! isset($_GET[$index]))
200 {
201 return FALSE;
202 }
203
204 if ($xss_clean === TRUE)
205 {
206 if (is_array($_GET[$index]))
207 {
208 foreach($_GET[$index] as $key => $val)
209 {
210 $_GET[$index][$key] = $this->xss_clean($val);
211 }
212 }
213 else
214 {
215 return $this->xss_clean($_GET[$index]);
216 }
217 }
218
219 return $_GET[$index];
220 }
Derek Allarda72b60d2007-01-31 23:56:11 +0000221
222 // --------------------------------------------------------------------
223
224 /**
225 * Fetch an item from the POST array
226 *
227 * @access public
228 * @param string
229 * @param bool
230 * @return string
231 */
232 function post($index = '', $xss_clean = FALSE)
233 {
234 if ( ! isset($_POST[$index]))
235 {
236 return FALSE;
237 }
238
239 if ($xss_clean === TRUE)
240 {
241 if (is_array($_POST[$index]))
242 {
243 foreach($_POST[$index] as $key => $val)
244 {
245 $_POST[$index][$key] = $this->xss_clean($val);
246 }
247 }
248 else
249 {
250 return $this->xss_clean($_POST[$index]);
251 }
252 }
253
254 return $_POST[$index];
255 }
256
257 // --------------------------------------------------------------------
258
259 /**
260 * Fetch an item from the COOKIE array
261 *
262 * @access public
263 * @param string
264 * @param bool
265 * @return string
266 */
267 function cookie($index = '', $xss_clean = FALSE)
268 {
269 if ( ! isset($_COOKIE[$index]))
270 {
271 return FALSE;
272 }
273
274 if ($xss_clean === TRUE)
275 {
276 if (is_array($_COOKIE[$index]))
277 {
278 $cookie = array();
279 foreach($_COOKIE[$index] as $key => $val)
280 {
281 $cookie[$key] = $this->xss_clean($val);
282 }
283
284 return $cookie;
285 }
286 else
287 {
288 return $this->xss_clean($_COOKIE[$index]);
289 }
290 }
291 else
292 {
293 return $_COOKIE[$index];
294 }
295 }
296
297 // --------------------------------------------------------------------
298
299 /**
300 * Fetch an item from the SERVER array
301 *
302 * @access public
303 * @param string
304 * @param bool
305 * @return string
306 */
307 function server($index = '', $xss_clean = FALSE)
308 {
309 if ( ! isset($_SERVER[$index]))
310 {
311 return FALSE;
312 }
313
314 if ($xss_clean === TRUE)
315 {
316 return $this->xss_clean($_SERVER[$index]);
317 }
318
319 return $_SERVER[$index];
320 }
321
322 // --------------------------------------------------------------------
323
324 /**
325 * Fetch the IP Address
326 *
327 * @access public
328 * @return string
329 */
330 function ip_address()
331 {
332 if ($this->ip_address !== FALSE)
333 {
334 return $this->ip_address;
335 }
336
337 if ($this->server('REMOTE_ADDR') AND $this->server('HTTP_CLIENT_IP'))
338 {
339 $this->ip_address = $_SERVER['HTTP_CLIENT_IP'];
340 }
341 elseif ($this->server('REMOTE_ADDR'))
342 {
343 $this->ip_address = $_SERVER['REMOTE_ADDR'];
344 }
345 elseif ($this->server('HTTP_CLIENT_IP'))
346 {
347 $this->ip_address = $_SERVER['HTTP_CLIENT_IP'];
348 }
349 elseif ($this->server('HTTP_X_FORWARDED_FOR'))
350 {
351 $this->ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'];
352 }
353
354 if ($this->ip_address === FALSE)
355 {
356 $this->ip_address = '0.0.0.0';
357 return $this->ip_address;
358 }
359
360 if (strstr($this->ip_address, ','))
361 {
362 $x = explode(',', $this->ip_address);
363 $this->ip_address = end($x);
364 }
365
366 if ( ! $this->valid_ip($this->ip_address))
367 {
368 $this->ip_address = '0.0.0.0';
369 }
370
371 return $this->ip_address;
372 }
373
374 // --------------------------------------------------------------------
375
376 /**
377 * Validate IP Address
378 *
379 * @access public
380 * @param string
381 * @return string
382 */
383 function valid_ip($ip)
384 {
Rick Ellis112569d2007-02-26 19:19:08 +0000385 if ( ! preg_match( "/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/", $ip))
386 {
387 return FALSE;
388 }
389
390 $octets = explode('.', $ip);
391
392 for ($i = 1; $i <= 4; $i++)
393 {
394 $octet = intval($octets[($i-1)]);
395 if ($i === 1)
396 {
397 if ($octet > 223 OR $octet < 1)
398 return FALSE;
399 }
400 elseif ($i === 4)
401 {
402 if ($octet < 1)
403 return FALSE;
404 }
405 else
406 {
407 if ($octet > 254)
408 return FALSE;
409 }
410 }
411
412 return TRUE;
Derek Allarda72b60d2007-01-31 23:56:11 +0000413 }
414
415 // --------------------------------------------------------------------
416
417 /**
418 * User Agent
419 *
420 * @access public
421 * @return string
422 */
423 function user_agent()
424 {
425 if ($this->user_agent !== FALSE)
426 {
427 return $this->user_agent;
428 }
429
430 $this->user_agent = ( ! isset($_SERVER['HTTP_USER_AGENT'])) ? FALSE : $_SERVER['HTTP_USER_AGENT'];
431
432 return $this->user_agent;
433 }
434
435 // --------------------------------------------------------------------
436
437 /**
438 * XSS Clean
439 *
440 * Sanitizes data so that Cross Site Scripting Hacks can be
441 * prevented.  This function does a fair amount of work but
442 * it is extremely thorough, designed to prevent even the
443 * most obscure XSS attempts.  Nothing is ever 100% foolproof,
444 * of course, but I haven't been able to get anything passed
445 * the filter.
446 *
447 * Note: This function should only be used to deal with data
448 * upon submission.  It's not something that should
449 * be used for general runtime processing.
450 *
451 * This function was based in part on some code and ideas I
452 * got from Bitflux: http://blog.bitflux.ch/wiki/XSS_Prevention
453 *
454 * To help develop this script I used this great list of
455 * vulnerabilities along with a few other hacks I've
456 * harvested from examining vulnerabilities in other programs:
457 * http://ha.ckers.org/xss.html
458 *
459 * @access public
460 * @param string
461 * @return string
462 */
463 function xss_clean($str, $charset = 'ISO-8859-1')
464 {
465 /*
466 * Remove Null Characters
467 *
468 * This prevents sandwiching null characters
469 * between ascii characters, like Java\0script.
470 *
471 */
472 $str = preg_replace('/\0+/', '', $str);
473 $str = preg_replace('/(\\\\0)+/', '', $str);
474
475 /*
476 * Validate standard character entities
477 *
478 * Add a semicolon if missing. We do this to enable
479 * the conversion of entities to ASCII later.
480 *
481 */
482 $str = preg_replace('#(&\#*\w+)[\x00-\x20]+;#u',"\\1;",$str);
483
484 /*
485 * Validate UTF16 two byte encoding (x00)
486 *
487 * Just as above, adds a semicolon if missing.
488 *
489 */
490 $str = preg_replace('#(&\#x*)([0-9A-F]+);*#iu',"\\1\\2;",$str);
491
492 /*
493 * URL Decode
494 *
495 * Just in case stuff like this is submitted:
496 *
497 * <a href="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">Google</a>
498 *
499 * Note: Normally urldecode() would be easier but it removes plus signs
500 *
501 */
Derek Jones01f72ca2007-05-04 18:19:17 +0000502 $str = preg_replace("/(%20)+/", '9u3iovBnRThju941s89rKozm', $str);
Derek Allarda72b60d2007-01-31 23:56:11 +0000503 $str = preg_replace("/%u0([a-z0-9]{3})/i", "&#x\\1;", $str);
Derek Jones01f72ca2007-05-04 18:19:17 +0000504 $str = preg_replace("/%([a-z0-9]{2})/i", "&#x\\1;", $str);
505 $str = str_replace('9u3iovBnRThju941s89rKozm', "%20", $str);
Derek Allarda72b60d2007-01-31 23:56:11 +0000506
507 /*
508 * Convert character entities to ASCII
509 *
510 * This permits our tests below to work reliably.
511 * We only convert entities that are within tags since
512 * these are the ones that will pose security problems.
513 *
514 */
515 if (preg_match_all("/<(.+?)>/si", $str, $matches))
516 {
517 for ($i = 0; $i < count($matches['0']); $i++)
518 {
519 $str = str_replace($matches['1'][$i],
520 $this->_html_entity_decode($matches['1'][$i], $charset),
521 $str);
522 }
523 }
524
525 /*
526 * Not Allowed Under Any Conditions
527 */
528 $bad = array(
529 'document.cookie' => '[removed]',
530 'document.write' => '[removed]',
531 'window.location' => '[removed]',
532 "javascript\s*:" => '[removed]',
533 "Redirect\s+302" => '[removed]',
534 '<!--' => '&lt;!--',
535 '-->' => '--&gt;'
536 );
537
538 foreach ($bad as $key => $val)
539 {
540 $str = preg_replace("#".$key."#i", $val, $str);
541 }
542
543 /*
544 * Convert all tabs to spaces
545 *
546 * This prevents strings like this: ja vascript
547 * Note: we deal with spaces between characters later.
548 *
549 */
550 $str = preg_replace("#\t+#", " ", $str);
551
552 /*
553 * Makes PHP tags safe
554 *
555 * Note: XML tags are inadvertently replaced too:
556 *
557 * <?xml
558 *
559 * But it doesn't seem to pose a problem.
560 *
561 */
562 $str = str_replace(array('<?php', '<?PHP', '<?', '?>'), array('&lt;?php', '&lt;?PHP', '&lt;?', '?&gt;'), $str);
563
564 /*
565 * Compact any exploded words
566 *
567 * This corrects words like: j a v a s c r i p t
568 * These words are compacted back to their correct state.
569 *
570 */
571 $words = array('javascript', 'vbscript', 'script', 'applet', 'alert', 'document', 'write', 'cookie', 'window');
572 foreach ($words as $word)
573 {
574 $temp = '';
575 for ($i = 0; $i < strlen($word); $i++)
576 {
577 $temp .= substr($word, $i, 1)."\s*";
578 }
579
Derek Jones01f72ca2007-05-04 18:19:17 +0000580 // We only want to do this when it is followed by a non-word character
581 // That way valid stuff like "dealer to" does not become "dealerto"
582 $str = preg_replace('#('.substr($temp, 0, -3).')(\W)#ise', "preg_replace('/\s+/s', '', '\\1').'\\2'", $str);
Derek Allarda72b60d2007-01-31 23:56:11 +0000583 }
584
585 /*
586 * Remove disallowed Javascript in links or img tags
587 */
Derek Jones01f72ca2007-05-04 18:19:17 +0000588 $str = preg_replace_callback("#<a.*?</a>#si", array($this, '_js_link_removal'), $str);
589 $str = preg_replace_callback("#<img.*?>#si", array($this, '_js_img_removal'), $str);
590 $str = preg_replace("#<(script|xss).*?\>#si", "", $str);
Derek Allarda72b60d2007-01-31 23:56:11 +0000591
592 /*
593 * Remove JavaScript Event Handlers
594 *
595 * Note: This code is a little blunt. It removes
596 * the event handler and anything up to the closing >,
597 * but it's unlikely to be a problem.
598 *
599 */
Derek Jones01f72ca2007-05-04 18:19:17 +0000600 $event_handlers = array('onblur','onchange','onclick','onfocus','onload','onmouseover','onmouseup','onmousedown','onselect','onsubmit','onunload','onkeypress','onkeydown','onkeyup','onresize', 'xmlns');
601 $str = preg_replace("#<([^>]+)(".implode('|', $event_handlers).")([^>]*)>#iU", "&lt;\\1\\2\\3&gt;", $str);
Derek Allarda72b60d2007-01-31 23:56:11 +0000602
603 /*
604 * Sanitize naughty HTML elements
605 *
606 * If a tag containing any of the words in the list
607 * below is found, the tag gets converted to entities.
608 *
609 * So this: <blink>
610 * Becomes: &lt;blink&gt;
611 *
612 */
613 $str = preg_replace('#<(/*\s*)(alert|applet|basefont|base|behavior|bgsound|blink|body|embed|expression|form|frameset|frame|head|html|ilayer|iframe|input|layer|link|meta|object|plaintext|style|script|textarea|title|xml|xss)([^>]*)>#is', "&lt;\\1\\2\\3&gt;", $str);
614
615 /*
616 * Sanitize naughty scripting elements
617 *
618 * Similar to above, only instead of looking for
619 * tags it looks for PHP and JavaScript commands
620 * that are disallowed. Rather than removing the
621 * code, it simply converts the parenthesis to entities
622 * rendering the code un-executable.
623 *
624 * For example: eval('some code')
625 * Becomes: eval&#40;'some code'&#41;
626 *
627 */
628 $str = preg_replace('#(alert|cmd|passthru|eval|exec|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', "\\1\\2&#40;\\3&#41;", $str);
629
630 /*
631 * Final clean up
632 *
633 * This adds a bit of extra precaution in case
634 * something got through the above filters
635 *
636 */
637 $bad = array(
638 'document.cookie' => '[removed]',
639 'document.write' => '[removed]',
640 'window.location' => '[removed]',
641 "javascript\s*:" => '[removed]',
642 "Redirect\s+302" => '[removed]',
643 '<!--' => '&lt;!--',
644 '-->' => '--&gt;'
645 );
646
647 foreach ($bad as $key => $val)
648 {
649 $str = preg_replace("#".$key."#i", $val, $str);
650 }
651
652
653 log_message('debug', "XSS Filtering completed");
654 return $str;
655 }
656
657 // --------------------------------------------------------------------
Derek Jones01f72ca2007-05-04 18:19:17 +0000658
659 /**
660 * JS Link Removal
661 *
662 * Callback function for xss_clean() to sanitize links
663 * This limits the PCRE backtracks, making it more performance friendly
664 * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
665 * PHP 5.2+ on link-heavy strings
666 *
667 * @access private
668 * @param array
669 * @return string
670 */
671 function _js_link_removal($match)
672 {
673 return preg_replace("#<a.+?href=.*?(alert\(|alert&\#40;|javascript\:|window\.|document\.|\.cookie|<script|<xss).*?\>.*?</a>#si", "", $match[0]);
674 }
675
676 /**
677 * JS Image Removal
678 *
679 * Callback function for xss_clean() to sanitize image tags
680 * This limits the PCRE backtracks, making it more performance friendly
681 * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
682 * PHP 5.2+ on image tag heavy strings
683 *
684 * @access private
685 * @param array
686 * @return string
687 */
688 function _js_img_removal($match)
689 {
690 return preg_replace("#<img.+?src=.*?(alert\(|alert&\#40;|javascript\:|window\.|document\.|\.cookie|<script|<xss).*?\>#si", "", $match[0]);
691 }
Derek Allarda72b60d2007-01-31 23:56:11 +0000692
Derek Jones01f72ca2007-05-04 18:19:17 +0000693 // --------------------------------------------------------------------
694
Derek Allarda72b60d2007-01-31 23:56:11 +0000695 /**
696 * HTML Entities Decode
697 *
698 * This function is a replacement for html_entity_decode()
699 *
700 * In some versions of PHP the native function does not work
701 * when UTF-8 is the specified character set, so this gives us
702 * a work-around. More info here:
703 * http://bugs.php.net/bug.php?id=25670
704 *
705 * @access private
706 * @param string
707 * @param string
708 * @return string
709 */
710 /* -------------------------------------------------
711 /* Replacement for html_entity_decode()
712 /* -------------------------------------------------*/
713
714 /*
715 NOTE: html_entity_decode() has a bug in some PHP versions when UTF-8 is the
716 character set, and the PHP developers said they were not back porting the
717 fix to versions other than PHP 5.x.
718 */
719 function _html_entity_decode($str, $charset='ISO-8859-1')
720 {
721 if (stristr($str, '&') === FALSE) return $str;
722
723 // The reason we are not using html_entity_decode() by itself is because
724 // while it is not technically correct to leave out the semicolon
725 // at the end of an entity most browsers will still interpret the entity
726 // correctly. html_entity_decode() does not convert entities without
727 // semicolons, so we are left with our own little solution here. Bummer.
728
729 if (function_exists('html_entity_decode') && (strtolower($charset) != 'utf-8' OR version_compare(phpversion(), '5.0.0', '>=')))
730 {
731 $str = html_entity_decode($str, ENT_COMPAT, $charset);
732 $str = preg_replace('~&#x([0-9a-f]{2,5})~ei', 'chr(hexdec("\\1"))', $str);
733 return preg_replace('~&#([0-9]{2,4})~e', 'chr(\\1)', $str);
734 }
735
736 // Numeric Entities
737 $str = preg_replace('~&#x([0-9a-f]{2,5});{0,1}~ei', 'chr(hexdec("\\1"))', $str);
738 $str = preg_replace('~&#([0-9]{2,4});{0,1}~e', 'chr(\\1)', $str);
739
740 // Literal Entities - Slightly slow so we do another check
741 if (stristr($str, '&') === FALSE)
742 {
743 $str = strtr($str, array_flip(get_html_translation_table(HTML_ENTITIES)));
744 }
745
746 return $str;
747 }
748
749}
750// END Input class
adminb0dd10f2006-08-25 17:25:49 +0000751?>