Security.php 23 KB

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