]>
Commit | Line | Data |
---|---|---|
59c06b17 CS |
1 | <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); |
2 | /** | |
3 | * CodeIgniter | |
4 | * | |
5 | * An open source application development framework for PHP 5.1.6 or newer | |
6 | * | |
7 | * @package CodeIgniter | |
8 | * @author ExpressionEngine Dev Team | |
9 | * @copyright Copyright (c) 2008 - 2011, EllisLab, Inc. | |
10 | * @license http://codeigniter.com/user_guide/license.html | |
11 | * @link http://codeigniter.com | |
12 | * @since Version 1.0 | |
13 | * @filesource | |
14 | */ | |
15 | ||
16 | // ------------------------------------------------------------------------ | |
17 | ||
18 | /** | |
19 | * Security Class | |
20 | * | |
21 | * @package CodeIgniter | |
22 | * @subpackage Libraries | |
23 | * @category Security | |
24 | * @author ExpressionEngine Dev Team | |
25 | * @link http://codeigniter.com/user_guide/libraries/security.html | |
26 | */ | |
27 | class CI_Security { | |
28 | ||
29 | /** | |
30 | * Random Hash for protecting URLs | |
31 | * | |
32 | * @var string | |
33 | * @access protected | |
34 | */ | |
35 | protected $_xss_hash = ''; | |
36 | /** | |
37 | * Random Hash for Cross Site Request Forgery Protection Cookie | |
38 | * | |
39 | * @var string | |
40 | * @access protected | |
41 | */ | |
42 | protected $_csrf_hash = ''; | |
43 | /** | |
44 | * Expiration time for Cross Site Request Forgery Protection Cookie | |
45 | * Defaults to two hours (in seconds) | |
46 | * | |
47 | * @var int | |
48 | * @access protected | |
49 | */ | |
50 | protected $_csrf_expire = 7200; | |
51 | /** | |
52 | * Token name for Cross Site Request Forgery Protection Cookie | |
53 | * | |
54 | * @var string | |
55 | * @access protected | |
56 | */ | |
57 | protected $_csrf_token_name = 'ci_csrf_token'; | |
58 | /** | |
59 | * Cookie name for Cross Site Request Forgery Protection Cookie | |
60 | * | |
61 | * @var string | |
62 | * @access protected | |
63 | */ | |
64 | protected $_csrf_cookie_name = 'ci_csrf_token'; | |
65 | /** | |
66 | * List of never allowed strings | |
67 | * | |
68 | * @var array | |
69 | * @access protected | |
70 | */ | |
71 | protected $_never_allowed_str = array( | |
72 | 'document.cookie' => '[removed]', | |
73 | 'document.write' => '[removed]', | |
74 | '.parentNode' => '[removed]', | |
75 | '.innerHTML' => '[removed]', | |
76 | 'window.location' => '[removed]', | |
77 | '-moz-binding' => '[removed]', | |
78 | '<!--' => '<!--', | |
79 | '-->' => '-->', | |
80 | '<![CDATA[' => '<![CDATA[', | |
81 | '<comment>' => '<comment>' | |
82 | ); | |
83 | ||
84 | /* never allowed, regex replacement */ | |
85 | /** | |
86 | * List of never allowed regex replacement | |
87 | * | |
88 | * @var array | |
89 | * @access protected | |
90 | */ | |
91 | protected $_never_allowed_regex = array( | |
92 | 'javascript\s*:', | |
93 | 'expression\s*(\(|&\#40;)', // CSS and IE | |
94 | 'vbscript\s*:', // IE, surprise! | |
95 | 'Redirect\s+302', | |
96 | "([\"'])?data\s*:[^\\1]*?base64[^\\1]*?,[^\\1]*?\\1?" | |
97 | ); | |
98 | ||
99 | /** | |
100 | * Constructor | |
101 | * | |
102 | * @return void | |
103 | */ | |
104 | public function __construct() | |
105 | { | |
106 | // Is CSRF protection enabled? | |
107 | if (config_item('csrf_protection') === TRUE) | |
108 | { | |
109 | // CSRF config | |
110 | foreach (array('csrf_expire', 'csrf_token_name', 'csrf_cookie_name') as $key) | |
111 | { | |
112 | if (FALSE !== ($val = config_item($key))) | |
113 | { | |
114 | $this->{'_'.$key} = $val; | |
115 | } | |
116 | } | |
117 | ||
118 | // Append application specific cookie prefix | |
119 | if (config_item('cookie_prefix')) | |
120 | { | |
121 | $this->_csrf_cookie_name = config_item('cookie_prefix').$this->_csrf_cookie_name; | |
122 | } | |
123 | ||
124 | // Set the CSRF hash | |
125 | $this->_csrf_set_hash(); | |
126 | } | |
127 | ||
128 | log_message('debug', "Security Class Initialized"); | |
129 | } | |
130 | ||
131 | // -------------------------------------------------------------------- | |
132 | ||
133 | /** | |
134 | * Verify Cross Site Request Forgery Protection | |
135 | * | |
136 | * @return object | |
137 | */ | |
138 | public function csrf_verify() | |
139 | { | |
140 | // If it's not a POST request we will set the CSRF cookie | |
141 | if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST') | |
142 | { | |
143 | return $this->csrf_set_cookie(); | |
144 | } | |
145 | ||
146 | // Do the tokens exist in both the _POST and _COOKIE arrays? | |
147 | if ( ! isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])) | |
148 | { | |
149 | $this->csrf_show_error(); | |
150 | } | |
151 | ||
152 | // Do the tokens match? | |
153 | if ($_POST[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_cookie_name]) | |
154 | { | |
155 | $this->csrf_show_error(); | |
156 | } | |
157 | ||
158 | // We kill this since we're done and we don't want to | |
159 | // polute the _POST array | |
160 | unset($_POST[$this->_csrf_token_name]); | |
161 | ||
162 | // Nothing should last forever | |
163 | unset($_COOKIE[$this->_csrf_cookie_name]); | |
164 | $this->_csrf_set_hash(); | |
165 | $this->csrf_set_cookie(); | |
166 | ||
167 | log_message('debug', 'CSRF token verified'); | |
168 | ||
169 | return $this; | |
170 | } | |
171 | ||
172 | // -------------------------------------------------------------------- | |
173 | ||
174 | /** | |
175 | * Set Cross Site Request Forgery Protection Cookie | |
176 | * | |
177 | * @return object | |
178 | */ | |
179 | public function csrf_set_cookie() | |
180 | { | |
181 | $expire = time() + $this->_csrf_expire; | |
182 | $secure_cookie = (config_item('cookie_secure') === TRUE) ? 1 : 0; | |
183 | ||
184 | if ($secure_cookie && (empty($_SERVER['HTTPS']) OR strtolower($_SERVER['HTTPS']) === 'off')) | |
185 | { | |
186 | return FALSE; | |
187 | } | |
188 | ||
189 | setcookie($this->_csrf_cookie_name, $this->_csrf_hash, $expire, config_item('cookie_path'), config_item('cookie_domain'), $secure_cookie); | |
190 | ||
191 | log_message('debug', "CRSF cookie Set"); | |
192 | ||
193 | return $this; | |
194 | } | |
195 | ||
196 | // -------------------------------------------------------------------- | |
197 | ||
198 | /** | |
199 | * Show CSRF Error | |
200 | * | |
201 | * @return void | |
202 | */ | |
203 | public function csrf_show_error() | |
204 | { | |
205 | show_error('The action you have requested is not allowed.'); | |
206 | } | |
207 | ||
208 | // -------------------------------------------------------------------- | |
209 | ||
210 | /** | |
211 | * Get CSRF Hash | |
212 | * | |
213 | * Getter Method | |
214 | * | |
215 | * @return string self::_csrf_hash | |
216 | */ | |
217 | public function get_csrf_hash() | |
218 | { | |
219 | return $this->_csrf_hash; | |
220 | } | |
221 | ||
222 | // -------------------------------------------------------------------- | |
223 | ||
224 | /** | |
225 | * Get CSRF Token Name | |
226 | * | |
227 | * Getter Method | |
228 | * | |
229 | * @return string self::csrf_token_name | |
230 | */ | |
231 | public function get_csrf_token_name() | |
232 | { | |
233 | return $this->_csrf_token_name; | |
234 | } | |
235 | ||
236 | // -------------------------------------------------------------------- | |
237 | ||
238 | /** | |
239 | * XSS Clean | |
240 | * | |
241 | * Sanitizes data so that Cross Site Scripting Hacks can be | |
242 | * prevented. This function does a fair amount of work but | |
243 | * it is extremely thorough, designed to prevent even the | |
244 | * most obscure XSS attempts. Nothing is ever 100% foolproof, | |
245 | * of course, but I haven't been able to get anything passed | |
246 | * the filter. | |
247 | * | |
248 | * Note: This function should only be used to deal with data | |
249 | * upon submission. It's not something that should | |
250 | * be used for general runtime processing. | |
251 | * | |
252 | * This function was based in part on some code and ideas I | |
253 | * got from Bitflux: http://channel.bitflux.ch/wiki/XSS_Prevention | |
254 | * | |
255 | * To help develop this script I used this great list of | |
256 | * vulnerabilities along with a few other hacks I've | |
257 | * harvested from examining vulnerabilities in other programs: | |
258 | * http://ha.ckers.org/xss.html | |
259 | * | |
260 | * @param mixed string or array | |
261 | * @param bool | |
262 | * @return string | |
263 | */ | |
264 | public function xss_clean($str, $is_image = FALSE) | |
265 | { | |
266 | /* | |
267 | * Is the string an array? | |
268 | * | |
269 | */ | |
270 | if (is_array($str)) | |
271 | { | |
272 | while (list($key) = each($str)) | |
273 | { | |
274 | $str[$key] = $this->xss_clean($str[$key]); | |
275 | } | |
276 | ||
277 | return $str; | |
278 | } | |
279 | ||
280 | /* | |
281 | * Remove Invisible Characters | |
282 | */ | |
283 | $str = remove_invisible_characters($str); | |
284 | ||
285 | // Validate Entities in URLs | |
286 | $str = $this->_validate_entities($str); | |
287 | ||
288 | /* | |
289 | * URL Decode | |
290 | * | |
291 | * Just in case stuff like this is submitted: | |
292 | * | |
293 | * <a href="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">Google</a> | |
294 | * | |
295 | * Note: Use rawurldecode() so it does not remove plus signs | |
296 | * | |
297 | */ | |
298 | $str = rawurldecode($str); | |
299 | ||
300 | /* | |
301 | * Convert character entities to ASCII | |
302 | * | |
303 | * This permits our tests below to work reliably. | |
304 | * We only convert entities that are within tags since | |
305 | * these are the ones that will pose security problems. | |
306 | * | |
307 | */ | |
308 | ||
309 | $str = preg_replace_callback("/[a-z]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str); | |
310 | ||
311 | $str = preg_replace_callback("/<\w+.*?(?=>|<|$)/si", array($this, '_decode_entity'), $str); | |
312 | ||
313 | /* | |
314 | * Remove Invisible Characters Again! | |
315 | */ | |
316 | $str = remove_invisible_characters($str); | |
317 | ||
318 | /* | |
319 | * Convert all tabs to spaces | |
320 | * | |
321 | * This prevents strings like this: ja vascript | |
322 | * NOTE: we deal with spaces between characters later. | |
323 | * NOTE: preg_replace was found to be amazingly slow here on | |
324 | * large blocks of data, so we use str_replace. | |
325 | */ | |
326 | ||
327 | if (strpos($str, "\t") !== FALSE) | |
328 | { | |
329 | $str = str_replace("\t", ' ', $str); | |
330 | } | |
331 | ||
332 | /* | |
333 | * Capture converted string for later comparison | |
334 | */ | |
335 | $converted_string = $str; | |
336 | ||
337 | // Remove Strings that are never allowed | |
338 | $str = $this->_do_never_allowed($str); | |
339 | ||
340 | /* | |
341 | * Makes PHP tags safe | |
342 | * | |
343 | * Note: XML tags are inadvertently replaced too: | |
344 | * | |
345 | * <?xml | |
346 | * | |
347 | * But it doesn't seem to pose a problem. | |
348 | */ | |
349 | if ($is_image === TRUE) | |
350 | { | |
351 | // Images have a tendency to have the PHP short opening and | |
352 | // closing tags every so often so we skip those and only | |
353 | // do the long opening tags. | |
354 | $str = preg_replace('/<\?(php)/i', "<?\\1", $str); | |
355 | } | |
356 | else | |
357 | { | |
358 | $str = str_replace(array('<?', '?'.'>'), array('<?', '?>'), $str); | |
359 | } | |
360 | ||
361 | /* | |
362 | * Compact any exploded words | |
363 | * | |
364 | * This corrects words like: j a v a s c r i p t | |
365 | * These words are compacted back to their correct state. | |
366 | */ | |
367 | $words = array( | |
368 | 'javascript', 'expression', 'vbscript', 'script', 'base64', | |
369 | 'applet', 'alert', 'document', 'write', 'cookie', 'window' | |
370 | ); | |
371 | ||
372 | foreach ($words as $word) | |
373 | { | |
374 | $temp = ''; | |
375 | ||
376 | for ($i = 0, $wordlen = strlen($word); $i < $wordlen; $i++) | |
377 | { | |
378 | $temp .= substr($word, $i, 1)."\s*"; | |
379 | } | |
380 | ||
381 | // We only want to do this when it is followed by a non-word character | |
382 | // That way valid stuff like "dealer to" does not become "dealerto" | |
383 | $str = preg_replace_callback('#('.substr($temp, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str); | |
384 | } | |
385 | ||
386 | /* | |
387 | * Remove disallowed Javascript in links or img tags | |
388 | * We used to do some version comparisons and use of stripos for PHP5, | |
389 | * but it is dog slow compared to these simplified non-capturing | |
390 | * preg_match(), especially if the pattern exists in the string | |
391 | */ | |
392 | do | |
393 | { | |
394 | $original = $str; | |
395 | ||
396 | if (preg_match("/<a/i", $str)) | |
397 | { | |
398 | $str = preg_replace_callback("#<a\s+([^>]*?)(>|$)#si", array($this, '_js_link_removal'), $str); | |
399 | } | |
400 | ||
401 | if (preg_match("/<img/i", $str)) | |
402 | { | |
403 | $str = preg_replace_callback("#<img\s+([^>]*?)(\s?/?>|$)#si", array($this, '_js_img_removal'), $str); | |
404 | } | |
405 | ||
406 | if (preg_match("/script/i", $str) OR preg_match("/xss/i", $str)) | |
407 | { | |
408 | $str = preg_replace("#<(/*)(script|xss)(.*?)\>#si", '[removed]', $str); | |
409 | } | |
410 | } | |
411 | while($original != $str); | |
412 | ||
413 | unset($original); | |
414 | ||
415 | // Remove evil attributes such as style, onclick and xmlns | |
416 | $str = $this->_remove_evil_attributes($str, $is_image); | |
417 | ||
418 | /* | |
419 | * Sanitize naughty HTML elements | |
420 | * | |
421 | * If a tag containing any of the words in the list | |
422 | * below is found, the tag gets converted to entities. | |
423 | * | |
424 | * So this: <blink> | |
425 | * Becomes: <blink> | |
426 | */ | |
427 | $naughty = 'alert|applet|audio|basefont|base|behavior|bgsound|blink|body|embed|expression|form|frameset|frame|head|html|ilayer|iframe|input|isindex|layer|link|meta|object|plaintext|style|script|textarea|title|video|xml|xss'; | |
428 | $str = preg_replace_callback('#<(/*\s*)('.$naughty.')([^><]*)([><]*)#is', array($this, '_sanitize_naughty_html'), $str); | |
429 | ||
430 | /* | |
431 | * Sanitize naughty scripting elements | |
432 | * | |
433 | * Similar to above, only instead of looking for | |
434 | * tags it looks for PHP and JavaScript commands | |
435 | * that are disallowed. Rather than removing the | |
436 | * code, it simply converts the parenthesis to entities | |
437 | * rendering the code un-executable. | |
438 | * | |
439 | * For example: eval('some code') | |
440 | * Becomes: eval('some code') | |
441 | */ | |
442 | $str = preg_replace('#(alert|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', "\\1\\2(\\3)", $str); | |
443 | ||
444 | ||
445 | // Final clean up | |
446 | // This adds a bit of extra precaution in case | |
447 | // something got through the above filters | |
448 | $str = $this->_do_never_allowed($str); | |
449 | ||
450 | /* | |
451 | * Images are Handled in a Special Way | |
452 | * - Essentially, we want to know that after all of the character | |
453 | * conversion is done whether any unwanted, likely XSS, code was found. | |
454 | * If not, we return TRUE, as the image is clean. | |
455 | * However, if the string post-conversion does not matched the | |
456 | * string post-removal of XSS, then it fails, as there was unwanted XSS | |
457 | * code found and removed/changed during processing. | |
458 | */ | |
459 | ||
460 | if ($is_image === TRUE) | |
461 | { | |
462 | return ($str == $converted_string) ? TRUE: FALSE; | |
463 | } | |
464 | ||
465 | log_message('debug', "XSS Filtering completed"); | |
466 | return $str; | |
467 | } | |
468 | ||
469 | // -------------------------------------------------------------------- | |
470 | ||
471 | /** | |
472 | * Random Hash for protecting URLs | |
473 | * | |
474 | * @return string | |
475 | */ | |
476 | public function xss_hash() | |
477 | { | |
478 | if ($this->_xss_hash == '') | |
479 | { | |
480 | mt_srand(); | |
481 | $this->_xss_hash = md5(time() + mt_rand(0, 1999999999)); | |
482 | } | |
483 | ||
484 | return $this->_xss_hash; | |
485 | } | |
486 | ||
487 | // -------------------------------------------------------------------- | |
488 | ||
489 | /** | |
490 | * HTML Entities Decode | |
491 | * | |
492 | * This function is a replacement for html_entity_decode() | |
493 | * | |
494 | * The reason we are not using html_entity_decode() by itself is because | |
495 | * while it is not technically correct to leave out the semicolon | |
496 | * at the end of an entity most browsers will still interpret the entity | |
497 | * correctly. html_entity_decode() does not convert entities without | |
498 | * semicolons, so we are left with our own little solution here. Bummer. | |
499 | * | |
500 | * @param string | |
501 | * @param string | |
502 | * @return string | |
503 | */ | |
504 | public function entity_decode($str, $charset='UTF-8') | |
505 | { | |
506 | if (stristr($str, '&') === FALSE) | |
507 | { | |
508 | return $str; | |
509 | } | |
510 | ||
511 | $str = html_entity_decode($str, ENT_COMPAT, $charset); | |
512 | $str = preg_replace('~&#x(0*[0-9a-f]{2,5})~ei', 'chr(hexdec("\\1"))', $str); | |
513 | return preg_replace('~&#([0-9]{2,4})~e', 'chr(\\1)', $str); | |
514 | } | |
515 | ||
516 | // -------------------------------------------------------------------- | |
517 | ||
518 | /** | |
519 | * Filename Security | |
520 | * | |
521 | * @param string | |
522 | * @param bool | |
523 | * @return string | |
524 | */ | |
525 | public function sanitize_filename($str, $relative_path = FALSE) | |
526 | { | |
527 | $bad = array( | |
528 | "../", | |
529 | "<!--", | |
530 | "-->", | |
531 | "<", | |
532 | ">", | |
533 | "'", | |
534 | '"', | |
535 | '&', | |
536 | '$', | |
537 | '#', | |
538 | '{', | |
539 | '}', | |
540 | '[', | |
541 | ']', | |
542 | '=', | |
543 | ';', | |
544 | '?', | |
545 | "%20", | |
546 | "%22", | |
547 | "%3c", // < | |
548 | "%253c", // < | |
549 | "%3e", // > | |
550 | "%0e", // > | |
551 | "%28", // ( | |
552 | "%29", // ) | |
553 | "%2528", // ( | |
554 | "%26", // & | |
555 | "%24", // $ | |
556 | "%3f", // ? | |
557 | "%3b", // ; | |
558 | "%3d" // = | |
559 | ); | |
560 | ||
561 | if ( ! $relative_path) | |
562 | { | |
563 | $bad[] = './'; | |
564 | $bad[] = '/'; | |
565 | } | |
566 | ||
567 | $str = remove_invisible_characters($str, FALSE); | |
568 | return stripslashes(str_replace($bad, '', $str)); | |
569 | } | |
570 | ||
571 | // ---------------------------------------------------------------- | |
572 | ||
573 | /** | |
574 | * Compact Exploded Words | |
575 | * | |
576 | * Callback function for xss_clean() to remove whitespace from | |
577 | * things like j a v a s c r i p t | |
578 | * | |
579 | * @param type | |
580 | * @return type | |
581 | */ | |
582 | protected function _compact_exploded_words($matches) | |
583 | { | |
584 | return preg_replace('/\s+/s', '', $matches[1]).$matches[2]; | |
585 | } | |
586 | ||
587 | // -------------------------------------------------------------------- | |
588 | ||
589 | /* | |
590 | * Remove Evil HTML Attributes (like evenhandlers and style) | |
591 | * | |
592 | * It removes the evil attribute and either: | |
593 | * - Everything up until a space | |
594 | * For example, everything between the pipes: | |
595 | * <a |style=document.write('hello');alert('world');| class=link> | |
596 | * - Everything inside the quotes | |
597 | * For example, everything between the pipes: | |
598 | * <a |style="document.write('hello'); alert('world');"| class="link"> | |
599 | * | |
600 | * @param string $str The string to check | |
601 | * @param boolean $is_image TRUE if this is an image | |
602 | * @return string The string with the evil attributes removed | |
603 | */ | |
604 | protected function _remove_evil_attributes($str, $is_image) | |
605 | { | |
606 | // All javascript event handlers (e.g. onload, onclick, onmouseover), style, and xmlns | |
607 | $evil_attributes = array('on\w*', 'style', 'xmlns', 'formaction'); | |
608 | ||
609 | if ($is_image === TRUE) | |
610 | { | |
611 | /* | |
612 | * Adobe Photoshop puts XML metadata into JFIF images, | |
613 | * including namespacing, so we have to allow this for images. | |
614 | */ | |
615 | unset($evil_attributes[array_search('xmlns', $evil_attributes)]); | |
616 | } | |
617 | ||
618 | do { | |
619 | $count = 0; | |
620 | $attribs = array(); | |
621 | ||
622 | // find occurrences of illegal attribute strings without quotes | |
623 | preg_match_all('/('.implode('|', $evil_attributes).')\s*=\s*([^\s>]*)/is', $str, $matches, PREG_SET_ORDER); | |
624 | ||
625 | foreach ($matches as $attr) | |
626 | { | |
627 | ||
628 | $attribs[] = preg_quote($attr[0], '/'); | |
629 | } | |
630 | ||
631 | // find occurrences of illegal attribute strings with quotes (042 and 047 are octal quotes) | |
632 | preg_match_all("/(".implode('|', $evil_attributes).")\s*=\s*(\042|\047)([^\\2]*?)(\\2)/is", $str, $matches, PREG_SET_ORDER); | |
633 | ||
634 | foreach ($matches as $attr) | |
635 | { | |
636 | $attribs[] = preg_quote($attr[0], '/'); | |
637 | } | |
638 | ||
639 | // replace illegal attribute strings that are inside an html tag | |
640 | if (count($attribs) > 0) | |
641 | { | |
642 | $str = preg_replace("/<(\/?[^><]+?)([^A-Za-z<>\-])(.*?)(".implode('|', $attribs).")(.*?)([\s><])([><]*)/i", '<$1 $3$5$6$7', $str, -1, $count); | |
643 | } | |
644 | ||
645 | } while ($count); | |
646 | ||
647 | return $str; | |
648 | } | |
649 | ||
650 | // -------------------------------------------------------------------- | |
651 | ||
652 | /** | |
653 | * Sanitize Naughty HTML | |
654 | * | |
655 | * Callback function for xss_clean() to remove naughty HTML elements | |
656 | * | |
657 | * @param array | |
658 | * @return string | |
659 | */ | |
660 | protected function _sanitize_naughty_html($matches) | |
661 | { | |
662 | // encode opening brace | |
663 | $str = '<'.$matches[1].$matches[2].$matches[3]; | |
664 | ||
665 | // encode captured opening or closing brace to prevent recursive vectors | |
666 | $str .= str_replace(array('>', '<'), array('>', '<'), | |
667 | $matches[4]); | |
668 | ||
669 | return $str; | |
670 | } | |
671 | ||
672 | // -------------------------------------------------------------------- | |
673 | ||
674 | /** | |
675 | * JS Link Removal | |
676 | * | |
677 | * Callback function for xss_clean() to sanitize links | |
678 | * This limits the PCRE backtracks, making it more performance friendly | |
679 | * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in | |
680 | * PHP 5.2+ on link-heavy strings | |
681 | * | |
682 | * @param array | |
683 | * @return string | |
684 | */ | |
685 | protected function _js_link_removal($match) | |
686 | { | |
687 | return str_replace( | |
688 | $match[1], | |
689 | preg_replace( | |
690 | '#href=.*?(alert\(|alert&\#40;|javascript\:|livescript\:|mocha\:|charset\=|window\.|document\.|\.cookie|<script|<xss|data\s*:)#si', | |
691 | '', | |
692 | $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1])) | |
693 | ), | |
694 | $match[0] | |
695 | ); | |
696 | } | |
697 | ||
698 | // -------------------------------------------------------------------- | |
699 | ||
700 | /** | |
701 | * JS Image Removal | |
702 | * | |
703 | * Callback function for xss_clean() to sanitize image tags | |
704 | * This limits the PCRE backtracks, making it more performance friendly | |
705 | * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in | |
706 | * PHP 5.2+ on image tag heavy strings | |
707 | * | |
708 | * @param array | |
709 | * @return string | |
710 | */ | |
711 | protected function _js_img_removal($match) | |
712 | { | |
713 | return str_replace( | |
714 | $match[1], | |
715 | preg_replace( | |
716 | '#src=.*?(alert\(|alert&\#40;|javascript\:|livescript\:|mocha\:|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)#si', | |
717 | '', | |
718 | $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1])) | |
719 | ), | |
720 | $match[0] | |
721 | ); | |
722 | } | |
723 | ||
724 | // -------------------------------------------------------------------- | |
725 | ||
726 | /** | |
727 | * Attribute Conversion | |
728 | * | |
729 | * Used as a callback for XSS Clean | |
730 | * | |
731 | * @param array | |
732 | * @return string | |
733 | */ | |
734 | protected function _convert_attribute($match) | |
735 | { | |
736 | return str_replace(array('>', '<', '\\'), array('>', '<', '\\\\'), $match[0]); | |
737 | } | |
738 | ||
739 | // -------------------------------------------------------------------- | |
740 | ||
741 | /** | |
742 | * Filter Attributes | |
743 | * | |
744 | * Filters tag attributes for consistency and safety | |
745 | * | |
746 | * @param string | |
747 | * @return string | |
748 | */ | |
749 | protected function _filter_attributes($str) | |
750 | { | |
751 | $out = ''; | |
752 | ||
753 | if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches)) | |
754 | { | |
755 | foreach ($matches[0] as $match) | |
756 | { | |
757 | $out .= preg_replace("#/\*.*?\*/#s", '', $match); | |
758 | } | |
759 | } | |
760 | ||
761 | return $out; | |
762 | } | |
763 | ||
764 | // -------------------------------------------------------------------- | |
765 | ||
766 | /** | |
767 | * HTML Entity Decode Callback | |
768 | * | |
769 | * Used as a callback for XSS Clean | |
770 | * | |
771 | * @param array | |
772 | * @return string | |
773 | */ | |
774 | protected function _decode_entity($match) | |
775 | { | |
776 | return $this->entity_decode($match[0], strtoupper(config_item('charset'))); | |
777 | } | |
778 | ||
779 | // -------------------------------------------------------------------- | |
780 | ||
781 | /** | |
782 | * Validate URL entities | |
783 | * | |
784 | * Called by xss_clean() | |
785 | * | |
786 | * @param string | |
787 | * @return string | |
788 | */ | |
789 | protected function _validate_entities($str) | |
790 | { | |
791 | /* | |
792 | * Protect GET variables in URLs | |
793 | */ | |
794 | ||
795 | // 901119URL5918AMP18930PROTECT8198 | |
796 | ||
797 | $str = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-]+)|i', $this->xss_hash()."\\1=\\2", $str); | |
798 | ||
799 | /* | |
800 | * Validate standard character entities | |
801 | * | |
802 | * Add a semicolon if missing. We do this to enable | |
803 | * the conversion of entities to ASCII later. | |
804 | * | |
805 | */ | |
806 | $str = preg_replace('#(&\#?[0-9a-z]{2,})([\x00-\x20])*;?#i', "\\1;\\2", $str); | |
807 | ||
808 | /* | |
809 | * Validate UTF16 two byte encoding (x00) | |
810 | * | |
811 | * Just as above, adds a semicolon if missing. | |
812 | * | |
813 | */ | |
814 | $str = preg_replace('#(&\#x?)([0-9A-F]+);?#i',"\\1\\2;",$str); | |
815 | ||
816 | /* | |
817 | * Un-Protect GET variables in URLs | |
818 | */ | |
819 | $str = str_replace($this->xss_hash(), '&', $str); | |
820 | ||
821 | return $str; | |
822 | } | |
823 | ||
824 | // ---------------------------------------------------------------------- | |
825 | ||
826 | /** | |
827 | * Do Never Allowed | |
828 | * | |
829 | * A utility function for xss_clean() | |
830 | * | |
831 | * @param string | |
832 | * @return string | |
833 | */ | |
834 | protected function _do_never_allowed($str) | |
835 | { | |
836 | $str = str_replace(array_keys($this->_never_allowed_str), $this->_never_allowed_str, $str); | |
837 | ||
838 | foreach ($this->_never_allowed_regex as $regex) | |
839 | { | |
840 | $str = preg_replace('#'.$regex.'#is', '[removed]', $str); | |
841 | } | |
842 | ||
843 | return $str; | |
844 | } | |
845 | ||
846 | // -------------------------------------------------------------------- | |
847 | ||
848 | /** | |
849 | * Set Cross Site Request Forgery Protection Cookie | |
850 | * | |
851 | * @return string | |
852 | */ | |
853 | protected function _csrf_set_hash() | |
854 | { | |
855 | if ($this->_csrf_hash == '') | |
856 | { | |
857 | // If the cookie exists we will use it's value. | |
858 | // We don't necessarily want to regenerate it with | |
859 | // each page load since a page could contain embedded | |
860 | // sub-pages causing this feature to fail | |
861 | if (isset($_COOKIE[$this->_csrf_cookie_name]) && | |
862 | preg_match('#^[0-9a-f]{32}$#iS', $_COOKIE[$this->_csrf_cookie_name]) === 1) | |
863 | { | |
864 | return $this->_csrf_hash = $_COOKIE[$this->_csrf_cookie_name]; | |
865 | } | |
866 | ||
867 | return $this->_csrf_hash = md5(uniqid(rand(), TRUE)); | |
868 | } | |
869 | ||
870 | return $this->_csrf_hash; | |
871 | } | |
872 | ||
873 | } | |
874 | ||
875 | /* End of file Security.php */ | |
876 | /* Location: ./system/libraries/Security.php */ |