PageRenderTime 51ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/html/AppCode/codeigniter/system/libraries/Security.php

https://github.com/w3bg/www.hsifin.com
PHP | 732 lines | 563 code | 35 blank | 134 comment | 12 complexity | a6e018be4774c53605591fc3ccca34b0 MD5 | raw file
Possible License(s): AGPL-3.0
  1. <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
  2. /**
  3. * CodeIgniter
  4. *
  5. * An open source application development framework for PHP 4.3.2 or newer
  6. *
  7. * @package CodeIgniter
  8. * @author ExpressionEngine Dev Team
  9. * @copyright Copyright (c) 2008 - 2010, 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. * Security Class
  18. *
  19. * @package CodeIgniter
  20. * @subpackage Libraries
  21. * @category Security
  22. * @author ExpressionEngine Dev Team
  23. * @link http://codeigniter.com/user_guide/libraries/sessions.html
  24. */
  25. class CI_Security {
  26. var $xss_hash = '';
  27. var $csrf_hash = '';
  28. var $csrf_expire = 7200; // Two hours (in seconds)
  29. var $csrf_token_name = 'ci_csrf_token';
  30. var $csrf_cookie_name = 'ci_csrf_token';
  31. /* never allowed, string replacement */
  32. var $never_allowed_str = array(
  33. 'document.cookie' => '[removed]',
  34. 'document.write' => '[removed]',
  35. '.parentNode' => '[removed]',
  36. '.innerHTML' => '[removed]',
  37. 'window.location' => '[removed]',
  38. '-moz-binding' => '[removed]',
  39. '<!--' => '&lt;!--',
  40. '-->' => '--&gt;',
  41. '<![CDATA[' => '&lt;![CDATA['
  42. );
  43. /* never allowed, regex replacement */
  44. var $never_allowed_regex = array(
  45. "javascript\s*:" => '[removed]',
  46. "expression\s*(\(|&\#40;)" => '[removed]', // CSS and IE
  47. "vbscript\s*:" => '[removed]', // IE, surprise!
  48. "Redirect\s+302" => '[removed]'
  49. );
  50. function CI_Security()
  51. {
  52. // Append application specific cookie prefix to token name
  53. $this->csrf_cookie_name = (config_item('cookie_prefix')) ? config_item('cookie_prefix').$this->csrf_token_name : $this->csrf_token_name;
  54. // Set the CSRF hash
  55. $this->_csrf_set_hash();
  56. log_message('debug', "Security Class Initialized");
  57. }
  58. // --------------------------------------------------------------------
  59. /**
  60. * Verify Cross Site Request Forgery Protection
  61. *
  62. * @access public
  63. * @return null
  64. */
  65. function csrf_verify()
  66. {
  67. // If no POST data exists we will set the CSRF cookie
  68. if (count($_POST) == 0)
  69. {
  70. return $this->csrf_set_cookie();
  71. }
  72. // Do the tokens exist in both the _POST and _COOKIE arrays?
  73. if ( ! isset($_POST[$this->csrf_token_name]) OR ! isset($_COOKIE[$this->csrf_cookie_name]))
  74. {
  75. $this->csrf_show_error();
  76. }
  77. // Do the tokens match?
  78. if ($_POST[$this->csrf_token_name] != $_COOKIE[$this->csrf_cookie_name])
  79. {
  80. $this->csrf_show_error();
  81. }
  82. // We kill this since we're done and we don't want to polute the _POST array
  83. unset($_POST[$this->csrf_token_name]);
  84. // Nothing should last forever
  85. unset($_COOKIE[$this->csrf_cookie_name]);
  86. $this->_csrf_set_hash();
  87. $this->csrf_set_cookie();
  88. log_message('debug', "CSRF token verified ");
  89. }
  90. // --------------------------------------------------------------------
  91. /**
  92. * Set Cross Site Request Forgery Protection Cookie
  93. *
  94. * @access public
  95. * @return null
  96. */
  97. function csrf_set_cookie()
  98. {
  99. $expire = time() + $this->csrf_expire;
  100. setcookie($this->csrf_cookie_name, $this->csrf_hash, $expire, config_item('cookie_path'), config_item('cookie_domain'), 0);
  101. log_message('debug', "CRSF cookie Set");
  102. }
  103. // --------------------------------------------------------------------
  104. /**
  105. * Set Cross Site Request Forgery Protection Cookie
  106. *
  107. * @access public
  108. * @return null
  109. */
  110. function _csrf_set_hash()
  111. {
  112. if ($this->csrf_hash == '')
  113. {
  114. // If the cookie exists we will use it's value. We don't necessarily want to regenerate it with
  115. // each page load since a page could contain embedded sub-pages causing this feature to fail
  116. if (isset($_COOKIE[$this->csrf_cookie_name]) AND $_COOKIE[$this->csrf_cookie_name] != '')
  117. {
  118. $this->csrf_hash = $_COOKIE[$this->csrf_cookie_name];
  119. }
  120. else
  121. {
  122. $this->csrf_hash = md5(uniqid(rand(), TRUE));
  123. }
  124. }
  125. return $this->csrf_hash;
  126. }
  127. // --------------------------------------------------------------------
  128. /**
  129. * Show CSRF Error
  130. *
  131. * @access public
  132. * @return null
  133. */
  134. function csrf_show_error()
  135. {
  136. show_error('The action you have requested is not allowed.');
  137. }
  138. // --------------------------------------------------------------------
  139. /**
  140. * XSS Clean
  141. *
  142. * Sanitizes data so that Cross Site Scripting Hacks can be
  143. * prevented. This function does a fair amount of work but
  144. * it is extremely thorough, designed to prevent even the
  145. * most obscure XSS attempts. Nothing is ever 100% foolproof,
  146. * of course, but I haven't been able to get anything passed
  147. * the filter.
  148. *
  149. * Note: This function should only be used to deal with data
  150. * upon submission. It's not something that should
  151. * be used for general runtime processing.
  152. *
  153. * This function was based in part on some code and ideas I
  154. * got from Bitflux: http://channel.bitflux.ch/wiki/XSS_Prevention
  155. *
  156. * To help develop this script I used this great list of
  157. * vulnerabilities along with a few other hacks I've
  158. * harvested from examining vulnerabilities in other programs:
  159. * http://ha.ckers.org/xss.html
  160. *
  161. * @access public
  162. * @param mixed string or array
  163. * @return string
  164. */
  165. function xss_clean($str, $is_image = FALSE)
  166. {
  167. /*
  168. * Is the string an array?
  169. *
  170. */
  171. if (is_array($str))
  172. {
  173. while (list($key) = each($str))
  174. {
  175. $str[$key] = $this->xss_clean($str[$key]);
  176. }
  177. return $str;
  178. }
  179. /*
  180. * Remove Invisible Characters
  181. */
  182. $str = remove_invisible_characters($str);
  183. /*
  184. * Protect GET variables in URLs
  185. */
  186. // 901119URL5918AMP18930PROTECT8198
  187. $str = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-]+)|i', $this->xss_hash()."\\1=\\2", $str);
  188. /*
  189. * Validate standard character entities
  190. *
  191. * Add a semicolon if missing. We do this to enable
  192. * the conversion of entities to ASCII later.
  193. *
  194. */
  195. $str = preg_replace('#(&\#?[0-9a-z]{2,})([\x00-\x20])*;?#i', "\\1;\\2", $str);
  196. /*
  197. * Validate UTF16 two byte encoding (x00)
  198. *
  199. * Just as above, adds a semicolon if missing.
  200. *
  201. */
  202. $str = preg_replace('#(&\#x?)([0-9A-F]+);?#i',"\\1\\2;",$str);
  203. /*
  204. * Un-Protect GET variables in URLs
  205. */
  206. $str = str_replace($this->xss_hash(), '&', $str);
  207. /*
  208. * URL Decode
  209. *
  210. * Just in case stuff like this is submitted:
  211. *
  212. * <a href="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">Google</a>
  213. *
  214. * Note: Use rawurldecode() so it does not remove plus signs
  215. *
  216. */
  217. $str = rawurldecode($str);
  218. /*
  219. * Convert character entities to ASCII
  220. *
  221. * This permits our tests below to work reliably.
  222. * We only convert entities that are within tags since
  223. * these are the ones that will pose security problems.
  224. *
  225. */
  226. $str = preg_replace_callback("/[a-z]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str);
  227. $str = preg_replace_callback("/<\w+.*?(?=>|<|$)/si", array($this, '_decode_entity'), $str);
  228. /*
  229. * Remove Invisible Characters Again!
  230. */
  231. $str = remove_invisible_characters($str);
  232. /*
  233. * Convert all tabs to spaces
  234. *
  235. * This prevents strings like this: ja vascript
  236. * NOTE: we deal with spaces between characters later.
  237. * NOTE: preg_replace was found to be amazingly slow here on large blocks of data,
  238. * so we use str_replace.
  239. *
  240. */
  241. if (strpos($str, "\t") !== FALSE)
  242. {
  243. $str = str_replace("\t", ' ', $str);
  244. }
  245. /*
  246. * Capture converted string for later comparison
  247. */
  248. $converted_string = $str;
  249. /*
  250. * Not Allowed Under Any Conditions
  251. */
  252. foreach ($this->never_allowed_str as $key => $val)
  253. {
  254. $str = str_replace($key, $val, $str);
  255. }
  256. foreach ($this->never_allowed_regex as $key => $val)
  257. {
  258. $str = preg_replace("#".$key."#i", $val, $str);
  259. }
  260. /*
  261. * Makes PHP tags safe
  262. *
  263. * Note: XML tags are inadvertently replaced too:
  264. *
  265. * <?xml
  266. *
  267. * But it doesn't seem to pose a problem.
  268. *
  269. */
  270. if ($is_image === TRUE)
  271. {
  272. // Images have a tendency to have the PHP short opening and closing tags every so often
  273. // so we skip those and only do the long opening tags.
  274. $str = preg_replace('/<\?(php)/i', "&lt;?\\1", $str);
  275. }
  276. else
  277. {
  278. $str = str_replace(array('<?', '?'.'>'), array('&lt;?', '?&gt;'), $str);
  279. }
  280. /*
  281. * Compact any exploded words
  282. *
  283. * This corrects words like: j a v a s c r i p t
  284. * These words are compacted back to their correct state.
  285. *
  286. */
  287. $words = array('javascript', 'expression', 'vbscript', 'script', 'applet', 'alert', 'document', 'write', 'cookie', 'window');
  288. foreach ($words as $word)
  289. {
  290. $temp = '';
  291. for ($i = 0, $wordlen = strlen($word); $i < $wordlen; $i++)
  292. {
  293. $temp .= substr($word, $i, 1)."\s*";
  294. }
  295. // We only want to do this when it is followed by a non-word character
  296. // That way valid stuff like "dealer to" does not become "dealerto"
  297. $str = preg_replace_callback('#('.substr($temp, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str);
  298. }
  299. /*
  300. * Remove disallowed Javascript in links or img tags
  301. * We used to do some version comparisons and use of stripos for PHP5, but it is dog slow compared
  302. * to these simplified non-capturing preg_match(), especially if the pattern exists in the string
  303. */
  304. do
  305. {
  306. $original = $str;
  307. if (preg_match("/<a/i", $str))
  308. {
  309. $str = preg_replace_callback("#<a\s+([^>]*?)(>|$)#si", array($this, '_js_link_removal'), $str);
  310. }
  311. if (preg_match("/<img/i", $str))
  312. {
  313. $str = preg_replace_callback("#<img\s+([^>]*?)(\s?/?>|$)#si", array($this, '_js_img_removal'), $str);
  314. }
  315. if (preg_match("/script/i", $str) OR preg_match("/xss/i", $str))
  316. {
  317. $str = preg_replace("#<(/*)(script|xss)(.*?)\>#si", '[removed]', $str);
  318. }
  319. }
  320. while($original != $str);
  321. unset($original);
  322. /*
  323. * Remove JavaScript Event Handlers
  324. *
  325. * Note: This code is a little blunt. It removes
  326. * the event handler and anything up to the closing >,
  327. * but it's unlikely to be a problem.
  328. *
  329. */
  330. $event_handlers = array('[^a-z_\-]on\w*','xmlns');
  331. if ($is_image === TRUE)
  332. {
  333. /*
  334. * Adobe Photoshop puts XML metadata into JFIF images, including namespacing,
  335. * so we have to allow this for images. -Paul
  336. */
  337. unset($event_handlers[array_search('xmlns', $event_handlers)]);
  338. }
  339. $str = preg_replace("#<([^><]+?)(".implode('|', $event_handlers).")(\s*=\s*[^><]*)([><]*)#i", "<\\1\\4", $str);
  340. /*
  341. * Sanitize naughty HTML elements
  342. *
  343. * If a tag containing any of the words in the list
  344. * below is found, the tag gets converted to entities.
  345. *
  346. * So this: <blink>
  347. * Becomes: &lt;blink&gt;
  348. *
  349. */
  350. $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';
  351. $str = preg_replace_callback('#<(/*\s*)('.$naughty.')([^><]*)([><]*)#is', array($this, '_sanitize_naughty_html'), $str);
  352. /*
  353. * Sanitize naughty scripting elements
  354. *
  355. * Similar to above, only instead of looking for
  356. * tags it looks for PHP and JavaScript commands
  357. * that are disallowed. Rather than removing the
  358. * code, it simply converts the parenthesis to entities
  359. * rendering the code un-executable.
  360. *
  361. * For example: eval('some code')
  362. * Becomes: eval&#40;'some code'&#41;
  363. *
  364. */
  365. $str = preg_replace('#(alert|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', "\\1\\2&#40;\\3&#41;", $str);
  366. /*
  367. * Final clean up
  368. *
  369. * This adds a bit of extra precaution in case
  370. * something got through the above filters
  371. *
  372. */
  373. foreach ($this->never_allowed_str as $key => $val)
  374. {
  375. $str = str_replace($key, $val, $str);
  376. }
  377. foreach ($this->never_allowed_regex as $key => $val)
  378. {
  379. $str = preg_replace("#".$key."#i", $val, $str);
  380. }
  381. /*
  382. * Images are Handled in a Special Way
  383. * - Essentially, we want to know that after all of the character conversion is done whether
  384. * any unwanted, likely XSS, code was found. If not, we return TRUE, as the image is clean.
  385. * However, if the string post-conversion does not matched the string post-removal of XSS,
  386. * then it fails, as there was unwanted XSS code found and removed/changed during processing.
  387. */
  388. if ($is_image === TRUE)
  389. {
  390. if ($str == $converted_string)
  391. {
  392. return TRUE;
  393. }
  394. else
  395. {
  396. return FALSE;
  397. }
  398. }
  399. log_message('debug', "XSS Filtering completed");
  400. return $str;
  401. }
  402. // --------------------------------------------------------------------
  403. /**
  404. * Random Hash for protecting URLs
  405. *
  406. * @access public
  407. * @return string
  408. */
  409. function xss_hash()
  410. {
  411. if ($this->xss_hash == '')
  412. {
  413. if (phpversion() >= 4.2)
  414. mt_srand();
  415. else
  416. mt_srand(hexdec(substr(md5(microtime()), -8)) & 0x7fffffff);
  417. $this->xss_hash = md5(time() + mt_rand(0, 1999999999));
  418. }
  419. return $this->xss_hash;
  420. }
  421. // --------------------------------------------------------------------
  422. /**
  423. * Compact Exploded Words
  424. *
  425. * Callback function for xss_clean() to remove whitespace from
  426. * things like j a v a s c r i p t
  427. *
  428. * @access public
  429. * @param type
  430. * @return type
  431. */
  432. function _compact_exploded_words($matches)
  433. {
  434. return preg_replace('/\s+/s', '', $matches[1]).$matches[2];
  435. }
  436. // --------------------------------------------------------------------
  437. /**
  438. * Sanitize Naughty HTML
  439. *
  440. * Callback function for xss_clean() to remove naughty HTML elements
  441. *
  442. * @access private
  443. * @param array
  444. * @return string
  445. */
  446. function _sanitize_naughty_html($matches)
  447. {
  448. // encode opening brace
  449. $str = '&lt;'.$matches[1].$matches[2].$matches[3];
  450. // encode captured opening or closing brace to prevent recursive vectors
  451. $str .= str_replace(array('>', '<'), array('&gt;', '&lt;'), $matches[4]);
  452. return $str;
  453. }
  454. // --------------------------------------------------------------------
  455. /**
  456. * JS Link Removal
  457. *
  458. * Callback function for xss_clean() to sanitize links
  459. * This limits the PCRE backtracks, making it more performance friendly
  460. * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
  461. * PHP 5.2+ on link-heavy strings
  462. *
  463. * @access private
  464. * @param array
  465. * @return string
  466. */
  467. function _js_link_removal($match)
  468. {
  469. $attributes = $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1]));
  470. return str_replace($match[1], preg_replace("#href=.*?(alert\(|alert&\#40;|javascript\:|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)#si", "", $attributes), $match[0]);
  471. }
  472. /**
  473. * JS Image Removal
  474. *
  475. * Callback function for xss_clean() to sanitize image tags
  476. * This limits the PCRE backtracks, making it more performance friendly
  477. * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
  478. * PHP 5.2+ on image tag heavy strings
  479. *
  480. * @access private
  481. * @param array
  482. * @return string
  483. */
  484. function _js_img_removal($match)
  485. {
  486. $attributes = $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1]));
  487. return str_replace($match[1], preg_replace("#src=.*?(alert\(|alert&\#40;|javascript\:|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)#si", "", $attributes), $match[0]);
  488. }
  489. // --------------------------------------------------------------------
  490. /**
  491. * Attribute Conversion
  492. *
  493. * Used as a callback for XSS Clean
  494. *
  495. * @access public
  496. * @param array
  497. * @return string
  498. */
  499. function _convert_attribute($match)
  500. {
  501. return str_replace(array('>', '<', '\\'), array('&gt;', '&lt;', '\\\\'), $match[0]);
  502. }
  503. // --------------------------------------------------------------------
  504. /**
  505. * Filter Attributes
  506. *
  507. * Filters tag attributes for consistency and safety
  508. *
  509. * @access public
  510. * @param string
  511. * @return string
  512. */
  513. function _filter_attributes($str)
  514. {
  515. $out = '';
  516. if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches))
  517. {
  518. foreach ($matches[0] as $match)
  519. {
  520. $out .= preg_replace("#/\*.*?\*/#s", '', $match);
  521. }
  522. }
  523. return $out;
  524. }
  525. // --------------------------------------------------------------------
  526. /**
  527. * HTML Entity Decode Callback
  528. *
  529. * Used as a callback for XSS Clean
  530. *
  531. * @access public
  532. * @param array
  533. * @return string
  534. */
  535. function _decode_entity($match)
  536. {
  537. return $this->entity_decode($match[0], strtoupper(config_item('charset')));
  538. }
  539. // --------------------------------------------------------------------
  540. /**
  541. * HTML Entities Decode
  542. *
  543. * This function is a replacement for html_entity_decode()
  544. *
  545. * In some versions of PHP the native function does not work
  546. * when UTF-8 is the specified character set, so this gives us
  547. * a work-around. More info here:
  548. * http://bugs.php.net/bug.php?id=25670
  549. *
  550. * NOTE: html_entity_decode() has a bug in some PHP versions when UTF-8 is the
  551. * character set, and the PHP developers said they were not back porting the
  552. * fix to versions other than PHP 5.x.
  553. *
  554. * @access public
  555. * @param string
  556. * @param string
  557. * @return string
  558. */
  559. function entity_decode($str, $charset='UTF-8')
  560. {
  561. if (stristr($str, '&') === FALSE) return $str;
  562. // The reason we are not using html_entity_decode() by itself is because
  563. // while it is not technically correct to leave out the semicolon
  564. // at the end of an entity most browsers will still interpret the entity
  565. // correctly. html_entity_decode() does not convert entities without
  566. // semicolons, so we are left with our own little solution here. Bummer.
  567. if (function_exists('html_entity_decode') && (strtolower($charset) != 'utf-8' OR is_php('5.0.0')))
  568. {
  569. $str = html_entity_decode($str, ENT_COMPAT, $charset);
  570. $str = preg_replace('~&#x(0*[0-9a-f]{2,5})~ei', 'chr(hexdec("\\1"))', $str);
  571. return preg_replace('~&#([0-9]{2,4})~e', 'chr(\\1)', $str);
  572. }
  573. // Numeric Entities
  574. $str = preg_replace('~&#x(0*[0-9a-f]{2,5});{0,1}~ei', 'chr(hexdec("\\1"))', $str);
  575. $str = preg_replace('~&#([0-9]{2,4});{0,1}~e', 'chr(\\1)', $str);
  576. // Literal Entities - Slightly slow so we do another check
  577. if (stristr($str, '&') === FALSE)
  578. {
  579. $str = strtr($str, array_flip(get_html_translation_table(HTML_ENTITIES)));
  580. }
  581. return $str;
  582. }
  583. // --------------------------------------------------------------------
  584. /**
  585. * Filename Security
  586. *
  587. * @access public
  588. * @param string
  589. * @return string
  590. */
  591. function sanitize_filename($str, $relative_path = FALSE)
  592. {
  593. $bad = array(
  594. "../",
  595. "<!--",
  596. "-->",
  597. "<",
  598. ">",
  599. "'",
  600. '"',
  601. '&',
  602. '$',
  603. '#',
  604. '{',
  605. '}',
  606. '[',
  607. ']',
  608. '=',
  609. ';',
  610. '?',
  611. "%20",
  612. "%22",
  613. "%3c", // <
  614. "%253c", // <
  615. "%3e", // >
  616. "%0e", // >
  617. "%28", // (
  618. "%29", // )
  619. "%2528", // (
  620. "%26", // &
  621. "%24", // $
  622. "%3f", // ?
  623. "%3b", // ;
  624. "%3d" // =
  625. );
  626. if ( ! $relative_path)
  627. {
  628. $bad[] = './';
  629. $bad[] = '/';
  630. }
  631. return stripslashes(str_replace($bad, '', $str));
  632. }
  633. }
  634. // END Security Class
  635. /* End of file Security.php */
  636. /* Location: ./system/libraries/Security.php */