From c9c045a7feee07563c8d14bac3381f7af0e17280 Mon Sep 17 00:00:00 2001 From: Pascal Kriete Date: Tue, 5 Apr 2011 14:50:41 -0400 Subject: Improving parameter security in xss clean --- system/libraries/Security.php | 620 ++++++++++++++++++++++++------------------ 1 file changed, 351 insertions(+), 269 deletions(-) (limited to 'system/libraries/Security.php') diff --git a/system/libraries/Security.php b/system/libraries/Security.php index 58db4e79c..ceef9779c 100644 --- a/system/libraries/Security.php +++ b/system/libraries/Security.php @@ -22,44 +22,44 @@ * @subpackage Libraries * @category Security * @author ExpressionEngine Dev Team - * @link http://codeigniter.com/user_guide/libraries/sessions.html + * @link http://codeigniter.com/user_guide/libraries/security.html */ class CI_Security { - - public $xss_hash = ''; - public $csrf_hash = ''; - public $csrf_expire = 7200; // Two hours (in seconds) - public $csrf_token_name = 'ci_csrf_token'; - public $csrf_cookie_name = 'ci_csrf_token'; + + protected $_xss_hash = ''; + protected $_csrf_hash = ''; + protected $_csrf_expire = 7200; // Two hours (in seconds) + protected $_csrf_token_name = 'ci_csrf_token'; + protected $_csrf_cookie_name = 'ci_csrf_token'; /* never allowed, string replacement */ - public $never_allowed_str = array( - 'document.cookie' => '[removed]', - 'document.write' => '[removed]', - '.parentNode' => '[removed]', - '.innerHTML' => '[removed]', - 'window.location' => '[removed]', - '-moz-binding' => '[removed]', - '' => '-->', - ' '<![CDATA[' - ); - /* never allowed, regex replacement */ - public $never_allowed_regex = array( - "javascript\s*:" => '[removed]', - "expression\s*(\(|&\#40;)" => '[removed]', // CSS and IE - "vbscript\s*:" => '[removed]', // IE, surprise! - "Redirect\s+302" => '[removed]' - ); + protected $_never_allowed_str = array( + 'document.cookie' => '[removed]', + 'document.write' => '[removed]', + '.parentNode' => '[removed]', + '.innerHTML' => '[removed]', + 'window.location' => '[removed]', + '-moz-binding' => '[removed]', + '' => '-->', + ' '<![CDATA[' + ); + /* never allowed, regex replacement */ + protected $_never_allowed_regex = array( + "javascript\s*:" => '[removed]', + "expression\s*(\(|&\#40;)" => '[removed]', // CSS and IE + "vbscript\s*:" => '[removed]', // IE, surprise! + "Redirect\s+302" => '[removed]' + ); + + /** + * Constructor + */ public function __construct() { - $this->csrf_token_name = (config_item('csrf_token_name')) ? config_item('csrf_token_name') : 'csrf_token_name'; - $this->csrf_cookie_name = (config_item('csrf_cookie_name')) ? config_item('csrf_cookie_name') : 'csrf_cookie_name'; - $this->csrf_expire = (config_item('csrf_expire')) ? config_item('csrf_expire') : 7200; - // Append application specific cookie prefix to token name - $this->csrf_cookie_name = (config_item('cookie_prefix')) ? config_item('cookie_prefix').$this->csrf_token_name : $this->csrf_token_name; + $this->_csrf_cookie_name = (config_item('cookie_prefix')) ? config_item('cookie_prefix').$this->_csrf_token_name : $this->_csrf_token_name; // Set the CSRF hash $this->_csrf_set_hash(); @@ -72,8 +72,7 @@ class CI_Security { /** * Verify Cross Site Request Forgery Protection * - * @access public - * @return null + * @return object */ public function csrf_verify() { @@ -84,26 +83,30 @@ class CI_Security { } // Do the tokens exist in both the _POST and _COOKIE arrays? - if ( ! isset($_POST[$this->csrf_token_name]) OR ! isset($_COOKIE[$this->csrf_cookie_name])) + if ( ! isset($_POST[$this->_csrf_token_name]) OR + ! isset($_COOKIE[$this->_csrf_cookie_name])) { $this->csrf_show_error(); } // Do the tokens match? - if ($_POST[$this->csrf_token_name] != $_COOKIE[$this->csrf_cookie_name]) + if ($_POST[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_cookie_name]) { $this->csrf_show_error(); } - // We kill this since we're done and we don't want to polute the _POST array - unset($_POST[$this->csrf_token_name]); + // We kill this since we're done and we don't want to + // polute the _POST array + unset($_POST[$this->_csrf_token_name]); // Nothing should last forever - unset($_COOKIE[$this->csrf_cookie_name]); + unset($_COOKIE[$this->_csrf_cookie_name]); $this->_csrf_set_hash(); $this->csrf_set_cookie(); log_message('debug', "CSRF token verified "); + + return $this; } // -------------------------------------------------------------------- @@ -111,57 +114,68 @@ class CI_Security { /** * Set Cross Site Request Forgery Protection Cookie * - * @access public - * @return null + * @return object */ public function csrf_set_cookie() { - $expire = time() + $this->csrf_expire; + $expire = time() + $this->_csrf_expire; $secure_cookie = (config_item('cookie_secure') === TRUE) ? 1 : 0; - setcookie($this->csrf_cookie_name, $this->csrf_hash, $expire, config_item('cookie_path'), config_item('cookie_domain'), $secure_cookie); + if ($secure_cookie) + { + $req = isset($_SERVER['HTTPS']) ? $_SERVER['HTTPS'] : FALSE; + + if ( ! $req OR $req == 'off') + { + return FALSE; + } + } + + setcookie($this->_csrf_cookie_name, $this->_csrf_hash, $expire, config_item('cookie_path'), config_item('cookie_domain'), $secure_cookie); log_message('debug', "CRSF cookie Set"); + + return $this; } // -------------------------------------------------------------------- /** - * Set Cross Site Request Forgery Protection Cookie + * Show CSRF Error * - * @access private - * @return null + * @return void */ - private function _csrf_set_hash() + public function csrf_show_error() { - if ($this->csrf_hash == '') - { - // If the cookie exists we will use it's value. We don't necessarily want to regenerate it with - // each page load since a page could contain embedded sub-pages causing this feature to fail - if (isset($_COOKIE[$this->csrf_cookie_name]) AND $_COOKIE[$this->csrf_cookie_name] != '') - { - $this->csrf_hash = $_COOKIE[$this->csrf_cookie_name]; - } - else - { - $this->csrf_hash = md5(uniqid(rand(), TRUE)); - } - } + show_error('The action you have requested is not allowed.'); + } - return $this->csrf_hash; + // -------------------------------------------------------------------- + + /** + * Get CSRF Hash + * + * Getter Method + * + * @return string self::_csrf_hash + */ + public function get_csrf_hash() + { + return $this->_csrf_hash; } // -------------------------------------------------------------------- /** - * Show CSRF Error + * Get CSRF Token Name * - * @access public - * @return null + * Getter Method + * + * @return string self::csrf_token_name */ - public function csrf_show_error() + public function get_csrf_token_name() { - show_error('The action you have requested is not allowed.'); + return $this->_csrf_token_name; } // -------------------------------------------------------------------- @@ -188,7 +202,6 @@ class CI_Security { * harvested from examining vulnerabilities in other programs: * http://ha.ckers.org/xss.html * - * @access public * @param mixed string or array * @return string */ @@ -213,35 +226,8 @@ class CI_Security { */ $str = remove_invisible_characters($str); - /* - * Protect GET variables in URLs - */ - - // 901119URL5918AMP18930PROTECT8198 - - $str = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-]+)|i', $this->xss_hash()."\\1=\\2", $str); - - /* - * Validate standard character entities - * - * Add a semicolon if missing. We do this to enable - * the conversion of entities to ASCII later. - * - */ - $str = preg_replace('#(&\#?[0-9a-z]{2,})([\x00-\x20])*;?#i', "\\1;\\2", $str); - - /* - * Validate UTF16 two byte encoding (x00) - * - * Just as above, adds a semicolon if missing. - * - */ - $str = preg_replace('#(&\#x?)([0-9A-F]+);?#i',"\\1\\2;",$str); - - /* - * Un-Protect GET variables in URLs - */ - $str = str_replace($this->xss_hash(), '&', $str); + // Validate Entities in URLs + $str = $this->_validate_entities($str); /* * URL Decode @@ -265,7 +251,7 @@ class CI_Security { */ $str = preg_replace_callback("/[a-z]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str); - + $str = preg_replace_callback("/<\w+.*?(?=>|<|$)/si", array($this, '_decode_entity'), $str); /* @@ -278,9 +264,8 @@ class CI_Security { * * This prevents strings like this: ja vascript * NOTE: we deal with spaces between characters later. - * NOTE: preg_replace was found to be amazingly slow here on large blocks of data, - * so we use str_replace. - * + * NOTE: preg_replace was found to be amazingly slow here on + * large blocks of data, so we use str_replace. */ if (strpos($str, "\t") !== FALSE) @@ -293,34 +278,23 @@ class CI_Security { */ $converted_string = $str; - /* - * Not Allowed Under Any Conditions - */ - - foreach ($this->never_allowed_str as $key => $val) - { - $str = str_replace($key, $val, $str); - } - - foreach ($this->never_allowed_regex as $key => $val) - { - $str = preg_replace("#".$key."#i", $val, $str); - } + // Remove Strings that are never allowed + $str = $this->_do_never_allowed($str); /* * Makes PHP tags safe * - * Note: XML tags are inadvertently replaced too: + * Note: XML tags are inadvertently replaced too: * - * #si", '[removed]', $str); } } - while ($original != $str); + while($original != $str); unset($original); - /* - * Remove JavaScript Event Handlers - * - * Note: This code is a little blunt. It removes - * the event handler and anything up to the closing >, - * but it's unlikely to be a problem. - * - */ - $event_handlers = array('[^a-z_\-]on\w*','xmlns'); - - if ($is_image === TRUE) - { - /* - * Adobe Photoshop puts XML metadata into JFIF images, including namespacing, - * so we have to allow this for images. -Paul - */ - unset($event_handlers[array_search('xmlns', $event_handlers)]); - } - - $str = preg_replace("#<([^><]+?)(".implode('|', $event_handlers).")(\s*=\s*[^><]*)([><]*)#i", "<\\1\\4", $str); + // Remove evil attributes such as style, onclick and xmlns + $str = $this->_remove_evil_attributes($str, $is_image); /* * Sanitize naughty HTML elements @@ -407,7 +367,6 @@ class CI_Security { * * So this: * Becomes: <blink> - * */ $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'; $str = preg_replace_callback('#<(/*\s*)('.$naughty.')([^><]*)([><]*)#is', array($this, '_sanitize_naughty_html'), $str); @@ -423,45 +382,28 @@ class CI_Security { * * For example: eval('some code') * Becomes: eval('some code') - * */ $str = preg_replace('#(alert|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', "\\1\\2(\\3)", $str); - /* - * Final clean up - * - * This adds a bit of extra precaution in case - * something got through the above filters - * - */ - foreach ($this->never_allowed_str as $key => $val) - { - $str = str_replace($key, $val, $str); - } - foreach ($this->never_allowed_regex as $key => $val) - { - $str = preg_replace("#".$key."#i", $val, $str); - } + // Final clean up + // This adds a bit of extra precaution in case + // something got through the above filters + $str = $this->_do_never_allowed($str); /* - * Images are Handled in a Special Way - * - Essentially, we want to know that after all of the character conversion is done whether - * any unwanted, likely XSS, code was found. If not, we return TRUE, as the image is clean. - * However, if the string post-conversion does not matched the string post-removal of XSS, - * then it fails, as there was unwanted XSS code found and removed/changed during processing. + * Images are Handled in a Special Way + * - Essentially, we want to know that after all of the character + * conversion is done whether any unwanted, likely XSS, code was found. + * If not, we return TRUE, as the image is clean. + * However, if the string post-conversion does not matched the + * string post-removal of XSS, then it fails, as there was unwanted XSS + * code found and removed/changed during processing. */ if ($is_image === TRUE) { - if ($str == $converted_string) - { - return TRUE; - } - else - { - return FALSE; - } + return ($str == $converted_string) ? TRUE: FALSE; } log_message('debug', "XSS Filtering completed"); @@ -473,41 +415,190 @@ class CI_Security { /** * Random Hash for protecting URLs * - * @access public * @return string */ public function xss_hash() { - if ($this->xss_hash == '') + if ($this->_xss_hash == '') { if (phpversion() >= 4.2) + { mt_srand(); + } else + { mt_srand(hexdec(substr(md5(microtime()), -8)) & 0x7fffffff); + } - $this->xss_hash = md5(time() + mt_rand(0, 1999999999)); + $this->_xss_hash = md5(time() + mt_rand(0, 1999999999)); } - return $this->xss_hash; + return $this->_xss_hash; } // -------------------------------------------------------------------- + /** + * HTML Entities Decode + * + * This function is a replacement for html_entity_decode() + * + * In some versions of PHP the native function does not work + * when UTF-8 is the specified character set, so this gives us + * a work-around. More info here: + * http://bugs.php.net/bug.php?id=25670 + * + * NOTE: html_entity_decode() has a bug in some PHP versions when UTF-8 is the + * character set, and the PHP developers said they were not back porting the + * fix to versions other than PHP 5.x. + * + * @param string + * @param string + * @return string + */ + public function entity_decode($str, $charset='UTF-8') + { + if (stristr($str, '&') === FALSE) return $str; + + // The reason we are not using html_entity_decode() by itself is because + // while it is not technically correct to leave out the semicolon + // at the end of an entity most browsers will still interpret the entity + // correctly. html_entity_decode() does not convert entities without + // semicolons, so we are left with our own little solution here. Bummer. + + if (function_exists('html_entity_decode') && + (strtolower($charset) != 'utf-8')) + { + $str = html_entity_decode($str, ENT_COMPAT, $charset); + $str = preg_replace('~&#x(0*[0-9a-f]{2,5})~ei', 'chr(hexdec("\\1"))', $str); + return preg_replace('~&#([0-9]{2,4})~e', 'chr(\\1)', $str); + } + + // Numeric Entities + $str = preg_replace('~&#x(0*[0-9a-f]{2,5});{0,1}~ei', 'chr(hexdec("\\1"))', $str); + $str = preg_replace('~&#([0-9]{2,4});{0,1}~e', 'chr(\\1)', $str); + + // Literal Entities - Slightly slow so we do another check + if (stristr($str, '&') === FALSE) + { + $str = strtr($str, array_flip(get_html_translation_table(HTML_ENTITIES))); + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Filename Security + * + * @param string + * @return string + */ + public function sanitize_filename($str, $relative_path = FALSE) + { + $bad = array( + "../", + "", + "<", + ">", + "'", + '"', + '&', + '$', + '#', + '{', + '}', + '[', + ']', + '=', + ';', + '?', + "%20", + "%22", + "%3c", // < + "%253c", // < + "%3e", // > + "%0e", // > + "%28", // ( + "%29", // ) + "%2528", // ( + "%26", // & + "%24", // $ + "%3f", // ? + "%3b", // ; + "%3d" // = + ); + + if ( ! $relative_path) + { + $bad[] = './'; + $bad[] = '/'; + } + + $str = remove_invisible_characters($str, FALSE); + return stripslashes(str_replace($bad, '', $str)); + } + + // ---------------------------------------------------------------- + /** * Compact Exploded Words * * Callback function for xss_clean() to remove whitespace from * things like j a v a s c r i p t * - * @access private * @param type * @return type */ - private function _compact_exploded_words($matches) + protected function _compact_exploded_words($matches) { return preg_replace('/\s+/s', '', $matches[1]).$matches[2]; } + // -------------------------------------------------------------------- + + /* + * Remove Evil HTML Attributes (like evenhandlers and style) + * + * It removes the evil attribute and either: + * - Everything up until a space + * For example, everything between the pipes: + * + * - Everything inside the quotes + * For example, everything between the pipes: + * + * + * @param string $str The string to check + * @param boolean $is_image TRUE if this is an image + * @return string The string with the evil attributes removed + */ + protected function _remove_evil_attributes($str, $is_image) + { + // All javascript event handlers (e.g. onload, onclick, onmouseover), style, and xmlns + $evil_attributes = array('on\w*', 'style', 'xmlns'); + + if ($is_image === TRUE) + { + /* + * Adobe Photoshop puts XML metadata into JFIF images, + * including namespacing, so we have to allow this for images. + */ + unset($evil_attributes[array_search('xmlns', $evil_attributes)]); + } + + do { + $str = preg_replace( + "#<(/?[^><]+?)([^A-Za-z\-])(".implode('|', $evil_attributes).")(\s*=\s*)([\"][^>]*?[\"]|[\'][^>]*?[\']|[^>]*?)([\s><])([><]*)#i", + "<$1$6", + $str, -1, $count + ); + } while ($count); + + return $str; + } + // -------------------------------------------------------------------- /** @@ -515,17 +606,17 @@ class CI_Security { * * Callback function for xss_clean() to remove naughty HTML elements * - * @access private * @param array * @return string */ - private function _sanitize_naughty_html($matches) + protected function _sanitize_naughty_html($matches) { // encode opening brace $str = '<'.$matches[1].$matches[2].$matches[3]; // encode captured opening or closing brace to prevent recursive vectors - $str .= str_replace(array('>', '<'), array('>', '<'), $matches[4]); + $str .= str_replace(array('>', '<'), array('>', '<'), + $matches[4]); return $str; } @@ -540,16 +631,18 @@ class CI_Security { * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in * PHP 5.2+ on link-heavy strings * - * @access private * @param array * @return string */ - private function _js_link_removal($match) + protected function _js_link_removal($match) { $attributes = $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1])); - return str_replace($match[1], preg_replace("#href=.*?(alert\(|alert&\#40;|javascript\:|charset\=|window\.|document\.|\.cookie|_filter_attributes(str_replace(array('<', '>'), '', $match[1])); - return str_replace($match[1], preg_replace("#src=.*?(alert\(|alert&\#40;|javascript\:|charset\=|window\.|document\.|\.cookie|', '<', '\\'), array('>', '<', '\\\\'), $match[0]); } @@ -591,11 +683,10 @@ class CI_Security { * * Filters tag attributes for consistency and safety * - * @access private * @param string * @return string */ - private function _filter_attributes($str) + protected function _filter_attributes($str) { $out = ''; @@ -617,118 +708,109 @@ class CI_Security { * * Used as a callback for XSS Clean * - * @access private * @param array * @return string */ - private function _decode_entity($match) + protected function _decode_entity($match) { return $this->entity_decode($match[0], strtoupper(config_item('charset'))); } // -------------------------------------------------------------------- - + /** - * HTML Entities Decode - * - * This function is a replacement for html_entity_decode() - * - * In some versions of PHP the native function does not work - * when UTF-8 is the specified character set, so this gives us - * a work-around. More info here: - * http://bugs.php.net/bug.php?id=25670 + * Validate URL entities * - * NOTE: html_entity_decode() has a bug in some PHP versions when UTF-8 is the - * character set, and the PHP developers said they were not back porting the - * fix to versions other than PHP 5.x. + * Called by xss_clean() * - * @access public - * @param string - * @param string - * @return string + * @param string + * @return string */ - public function entity_decode($str, $charset='UTF-8') + protected function _validate_entities($str) { - if (stristr($str, '&') === FALSE) return $str; + /* + * Protect GET variables in URLs + */ + + // 901119URL5918AMP18930PROTECT8198 + + $str = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-]+)|i', $this->xss_hash()."\\1=\\2", $str); - // The reason we are not using html_entity_decode() by itself is because - // while it is not technically correct to leave out the semicolon - // at the end of an entity most browsers will still interpret the entity - // correctly. html_entity_decode() does not convert entities without - // semicolons, so we are left with our own little solution here. Bummer. + /* + * Validate standard character entities + * + * Add a semicolon if missing. We do this to enable + * the conversion of entities to ASCII later. + * + */ + $str = preg_replace('#(&\#?[0-9a-z]{2,})([\x00-\x20])*;?#i', "\\1;\\2", $str); - if (function_exists('html_entity_decode') && (strtolower($charset) != 'utf-8' OR is_php('5.0.0'))) - { - $str = html_entity_decode($str, ENT_COMPAT, $charset); - $str = preg_replace('~&#x(0*[0-9a-f]{2,5})~ei', 'chr(hexdec("\\1"))', $str); - return preg_replace('~&#([0-9]{2,4})~e', 'chr(\\1)', $str); - } + /* + * Validate UTF16 two byte encoding (x00) + * + * Just as above, adds a semicolon if missing. + * + */ + $str = preg_replace('#(&\#x?)([0-9A-F]+);?#i',"\\1\\2;",$str); - // Numeric Entities - $str = preg_replace('~&#x(0*[0-9a-f]{2,5});{0,1}~ei', 'chr(hexdec("\\1"))', $str); - $str = preg_replace('~&#([0-9]{2,4});{0,1}~e', 'chr(\\1)', $str); + /* + * Un-Protect GET variables in URLs + */ + $str = str_replace($this->xss_hash(), '&', $str); + + return $str; + } - // Literal Entities - Slightly slow so we do another check - if (stristr($str, '&') === FALSE) + // ---------------------------------------------------------------------- + + /** + * Do Never Allowed + * + * A utility function for xss_clean() + * + * @param string + * @return string + */ + protected function _do_never_allowed($str) + { + foreach ($this->_never_allowed_str as $key => $val) { - $str = strtr($str, array_flip(get_html_translation_table(HTML_ENTITIES))); + $str = str_replace($key, $val, $str); } + foreach ($this->_never_allowed_regex as $key => $val) + { + $str = preg_replace("#".$key."#i", $val, $str); + } + return $str; } // -------------------------------------------------------------------- /** - * Filename Security + * Set Cross Site Request Forgery Protection Cookie * - * @access public - * @param string * @return string */ - public function sanitize_filename($str, $relative_path = FALSE) + protected function _csrf_set_hash() { - $bad = array( - "../", - "", - "<", - ">", - "'", - '"', - '&', - '$', - '#', - '{', - '}', - '[', - ']', - '=', - ';', - '?', - "%20", - "%22", - "%3c", // < - "%253c", // < - "%3e", // > - "%0e", // > - "%28", // ( - "%29", // ) - "%2528", // ( - "%26", // & - "%24", // $ - "%3f", // ? - "%3b", // ; - "%3d" // = - ); - - if ( ! $relative_path) + if ($this->_csrf_hash == '') { - $bad[] = './'; - $bad[] = '/'; + // If the cookie exists we will use it's value. + // We don't necessarily want to regenerate it with + // each page load since a page could contain embedded + // sub-pages causing this feature to fail + if (isset($_COOKIE[$this->_csrf_cookie_name]) && + $_COOKIE[$this->_csrf_cookie_name] != '') + { + return $this->_csrf_hash = $_COOKIE[$this->_csrf_cookie_name]; + } + + return $this->_csrf_hash = md5(uniqid(rand(), TRUE)); } - return stripslashes(str_replace($bad, '', $str)); + return $this->_csrf_hash; } } -- cgit v1.2.3-24-g4f1b From 14a0ac63a9dfb72e4681c37f7727cd48882152bd Mon Sep 17 00:00:00 2001 From: Pascal Kriete Date: Tue, 5 Apr 2011 14:55:56 -0400 Subject: Moving security to core. --- system/libraries/Security.php | 820 ------------------------------------------ 1 file changed, 820 deletions(-) delete mode 100644 system/libraries/Security.php (limited to 'system/libraries/Security.php') diff --git a/system/libraries/Security.php b/system/libraries/Security.php deleted file mode 100644 index ceef9779c..000000000 --- a/system/libraries/Security.php +++ /dev/null @@ -1,820 +0,0 @@ - '[removed]', - 'document.write' => '[removed]', - '.parentNode' => '[removed]', - '.innerHTML' => '[removed]', - 'window.location' => '[removed]', - '-moz-binding' => '[removed]', - '' => '-->', - ' '<![CDATA[' - ); - - /* never allowed, regex replacement */ - protected $_never_allowed_regex = array( - "javascript\s*:" => '[removed]', - "expression\s*(\(|&\#40;)" => '[removed]', // CSS and IE - "vbscript\s*:" => '[removed]', // IE, surprise! - "Redirect\s+302" => '[removed]' - ); - - /** - * Constructor - */ - public function __construct() - { - // Append application specific cookie prefix to token name - $this->_csrf_cookie_name = (config_item('cookie_prefix')) ? config_item('cookie_prefix').$this->_csrf_token_name : $this->_csrf_token_name; - - // Set the CSRF hash - $this->_csrf_set_hash(); - - log_message('debug', "Security Class Initialized"); - } - - // -------------------------------------------------------------------- - - /** - * Verify Cross Site Request Forgery Protection - * - * @return object - */ - public function csrf_verify() - { - // If no POST data exists we will set the CSRF cookie - if (count($_POST) == 0) - { - return $this->csrf_set_cookie(); - } - - // Do the tokens exist in both the _POST and _COOKIE arrays? - if ( ! isset($_POST[$this->_csrf_token_name]) OR - ! isset($_COOKIE[$this->_csrf_cookie_name])) - { - $this->csrf_show_error(); - } - - // Do the tokens match? - if ($_POST[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_cookie_name]) - { - $this->csrf_show_error(); - } - - // We kill this since we're done and we don't want to - // polute the _POST array - unset($_POST[$this->_csrf_token_name]); - - // Nothing should last forever - unset($_COOKIE[$this->_csrf_cookie_name]); - $this->_csrf_set_hash(); - $this->csrf_set_cookie(); - - log_message('debug', "CSRF token verified "); - - return $this; - } - - // -------------------------------------------------------------------- - - /** - * Set Cross Site Request Forgery Protection Cookie - * - * @return object - */ - public function csrf_set_cookie() - { - $expire = time() + $this->_csrf_expire; - $secure_cookie = (config_item('cookie_secure') === TRUE) ? 1 : 0; - - if ($secure_cookie) - { - $req = isset($_SERVER['HTTPS']) ? $_SERVER['HTTPS'] : FALSE; - - if ( ! $req OR $req == 'off') - { - return FALSE; - } - } - - setcookie($this->_csrf_cookie_name, $this->_csrf_hash, $expire, config_item('cookie_path'), config_item('cookie_domain'), $secure_cookie); - - log_message('debug', "CRSF cookie Set"); - - return $this; - } - - // -------------------------------------------------------------------- - - /** - * Show CSRF Error - * - * @return void - */ - public function csrf_show_error() - { - show_error('The action you have requested is not allowed.'); - } - - // -------------------------------------------------------------------- - - /** - * Get CSRF Hash - * - * Getter Method - * - * @return string self::_csrf_hash - */ - public function get_csrf_hash() - { - return $this->_csrf_hash; - } - - // -------------------------------------------------------------------- - - /** - * Get CSRF Token Name - * - * Getter Method - * - * @return string self::csrf_token_name - */ - public function get_csrf_token_name() - { - return $this->_csrf_token_name; - } - - // -------------------------------------------------------------------- - - /** - * XSS Clean - * - * Sanitizes data so that Cross Site Scripting Hacks can be - * prevented. This function does a fair amount of work but - * it is extremely thorough, designed to prevent even the - * most obscure XSS attempts. Nothing is ever 100% foolproof, - * of course, but I haven't been able to get anything passed - * the filter. - * - * Note: This function should only be used to deal with data - * upon submission. It's not something that should - * be used for general runtime processing. - * - * This function was based in part on some code and ideas I - * got from Bitflux: http://channel.bitflux.ch/wiki/XSS_Prevention - * - * To help develop this script I used this great list of - * vulnerabilities along with a few other hacks I've - * harvested from examining vulnerabilities in other programs: - * http://ha.ckers.org/xss.html - * - * @param mixed string or array - * @return string - */ - public function xss_clean($str, $is_image = FALSE) - { - /* - * Is the string an array? - * - */ - if (is_array($str)) - { - while (list($key) = each($str)) - { - $str[$key] = $this->xss_clean($str[$key]); - } - - return $str; - } - - /* - * Remove Invisible Characters - */ - $str = remove_invisible_characters($str); - - // Validate Entities in URLs - $str = $this->_validate_entities($str); - - /* - * URL Decode - * - * Just in case stuff like this is submitted: - * - * Google - * - * Note: Use rawurldecode() so it does not remove plus signs - * - */ - $str = rawurldecode($str); - - /* - * Convert character entities to ASCII - * - * This permits our tests below to work reliably. - * We only convert entities that are within tags since - * these are the ones that will pose security problems. - * - */ - - $str = preg_replace_callback("/[a-z]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str); - - $str = preg_replace_callback("/<\w+.*?(?=>|<|$)/si", array($this, '_decode_entity'), $str); - - /* - * Remove Invisible Characters Again! - */ - $str = remove_invisible_characters($str); - - /* - * Convert all tabs to spaces - * - * This prevents strings like this: ja vascript - * NOTE: we deal with spaces between characters later. - * NOTE: preg_replace was found to be amazingly slow here on - * large blocks of data, so we use str_replace. - */ - - if (strpos($str, "\t") !== FALSE) - { - $str = str_replace("\t", ' ', $str); - } - - /* - * Capture converted string for later comparison - */ - $converted_string = $str; - - // Remove Strings that are never allowed - $str = $this->_do_never_allowed($str); - - /* - * Makes PHP tags safe - * - * Note: XML tags are inadvertently replaced too: - * - * '), array('<?', '?>'), $str); - } - - /* - * Compact any exploded words - * - * This corrects words like: j a v a s c r i p t - * These words are compacted back to their correct state. - */ - $words = array( - 'javascript', 'expression', 'vbscript', 'script', - 'applet', 'alert', 'document', 'write', 'cookie', 'window' - ); - - foreach ($words as $word) - { - $temp = ''; - - for ($i = 0, $wordlen = strlen($word); $i < $wordlen; $i++) - { - $temp .= substr($word, $i, 1)."\s*"; - } - - // We only want to do this when it is followed by a non-word character - // That way valid stuff like "dealer to" does not become "dealerto" - $str = preg_replace_callback('#('.substr($temp, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str); - } - - /* - * Remove disallowed Javascript in links or img tags - * We used to do some version comparisons and use of stripos for PHP5, - * but it is dog slow compared to these simplified non-capturing - * preg_match(), especially if the pattern exists in the string - */ - do - { - $original = $str; - - if (preg_match("/]*?)(>|$)#si", array($this, '_js_link_removal'), $str); - } - - if (preg_match("/]*?)(\s?/?>|$)#si", array($this, '_js_img_removal'), $str); - } - - if (preg_match("/script/i", $str) OR preg_match("/xss/i", $str)) - { - $str = preg_replace("#<(/*)(script|xss)(.*?)\>#si", '[removed]', $str); - } - } - while($original != $str); - - unset($original); - - // Remove evil attributes such as style, onclick and xmlns - $str = $this->_remove_evil_attributes($str, $is_image); - - /* - * Sanitize naughty HTML elements - * - * If a tag containing any of the words in the list - * below is found, the tag gets converted to entities. - * - * So this: - * Becomes: <blink> - */ - $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'; - $str = preg_replace_callback('#<(/*\s*)('.$naughty.')([^><]*)([><]*)#is', array($this, '_sanitize_naughty_html'), $str); - - /* - * Sanitize naughty scripting elements - * - * Similar to above, only instead of looking for - * tags it looks for PHP and JavaScript commands - * that are disallowed. Rather than removing the - * code, it simply converts the parenthesis to entities - * rendering the code un-executable. - * - * For example: eval('some code') - * Becomes: eval('some code') - */ - $str = preg_replace('#(alert|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', "\\1\\2(\\3)", $str); - - - // Final clean up - // This adds a bit of extra precaution in case - // something got through the above filters - $str = $this->_do_never_allowed($str); - - /* - * Images are Handled in a Special Way - * - Essentially, we want to know that after all of the character - * conversion is done whether any unwanted, likely XSS, code was found. - * If not, we return TRUE, as the image is clean. - * However, if the string post-conversion does not matched the - * string post-removal of XSS, then it fails, as there was unwanted XSS - * code found and removed/changed during processing. - */ - - if ($is_image === TRUE) - { - return ($str == $converted_string) ? TRUE: FALSE; - } - - log_message('debug', "XSS Filtering completed"); - return $str; - } - - // -------------------------------------------------------------------- - - /** - * Random Hash for protecting URLs - * - * @return string - */ - public function xss_hash() - { - if ($this->_xss_hash == '') - { - if (phpversion() >= 4.2) - { - mt_srand(); - } - else - { - mt_srand(hexdec(substr(md5(microtime()), -8)) & 0x7fffffff); - } - - $this->_xss_hash = md5(time() + mt_rand(0, 1999999999)); - } - - return $this->_xss_hash; - } - - // -------------------------------------------------------------------- - - /** - * HTML Entities Decode - * - * This function is a replacement for html_entity_decode() - * - * In some versions of PHP the native function does not work - * when UTF-8 is the specified character set, so this gives us - * a work-around. More info here: - * http://bugs.php.net/bug.php?id=25670 - * - * NOTE: html_entity_decode() has a bug in some PHP versions when UTF-8 is the - * character set, and the PHP developers said they were not back porting the - * fix to versions other than PHP 5.x. - * - * @param string - * @param string - * @return string - */ - public function entity_decode($str, $charset='UTF-8') - { - if (stristr($str, '&') === FALSE) return $str; - - // The reason we are not using html_entity_decode() by itself is because - // while it is not technically correct to leave out the semicolon - // at the end of an entity most browsers will still interpret the entity - // correctly. html_entity_decode() does not convert entities without - // semicolons, so we are left with our own little solution here. Bummer. - - if (function_exists('html_entity_decode') && - (strtolower($charset) != 'utf-8')) - { - $str = html_entity_decode($str, ENT_COMPAT, $charset); - $str = preg_replace('~&#x(0*[0-9a-f]{2,5})~ei', 'chr(hexdec("\\1"))', $str); - return preg_replace('~&#([0-9]{2,4})~e', 'chr(\\1)', $str); - } - - // Numeric Entities - $str = preg_replace('~&#x(0*[0-9a-f]{2,5});{0,1}~ei', 'chr(hexdec("\\1"))', $str); - $str = preg_replace('~&#([0-9]{2,4});{0,1}~e', 'chr(\\1)', $str); - - // Literal Entities - Slightly slow so we do another check - if (stristr($str, '&') === FALSE) - { - $str = strtr($str, array_flip(get_html_translation_table(HTML_ENTITIES))); - } - - return $str; - } - - // -------------------------------------------------------------------- - - /** - * Filename Security - * - * @param string - * @return string - */ - public function sanitize_filename($str, $relative_path = FALSE) - { - $bad = array( - "../", - "", - "<", - ">", - "'", - '"', - '&', - '$', - '#', - '{', - '}', - '[', - ']', - '=', - ';', - '?', - "%20", - "%22", - "%3c", // < - "%253c", // < - "%3e", // > - "%0e", // > - "%28", // ( - "%29", // ) - "%2528", // ( - "%26", // & - "%24", // $ - "%3f", // ? - "%3b", // ; - "%3d" // = - ); - - if ( ! $relative_path) - { - $bad[] = './'; - $bad[] = '/'; - } - - $str = remove_invisible_characters($str, FALSE); - return stripslashes(str_replace($bad, '', $str)); - } - - // ---------------------------------------------------------------- - - /** - * Compact Exploded Words - * - * Callback function for xss_clean() to remove whitespace from - * things like j a v a s c r i p t - * - * @param type - * @return type - */ - protected function _compact_exploded_words($matches) - { - return preg_replace('/\s+/s', '', $matches[1]).$matches[2]; - } - - // -------------------------------------------------------------------- - - /* - * Remove Evil HTML Attributes (like evenhandlers and style) - * - * It removes the evil attribute and either: - * - Everything up until a space - * For example, everything between the pipes: - * - * - Everything inside the quotes - * For example, everything between the pipes: - * - * - * @param string $str The string to check - * @param boolean $is_image TRUE if this is an image - * @return string The string with the evil attributes removed - */ - protected function _remove_evil_attributes($str, $is_image) - { - // All javascript event handlers (e.g. onload, onclick, onmouseover), style, and xmlns - $evil_attributes = array('on\w*', 'style', 'xmlns'); - - if ($is_image === TRUE) - { - /* - * Adobe Photoshop puts XML metadata into JFIF images, - * including namespacing, so we have to allow this for images. - */ - unset($evil_attributes[array_search('xmlns', $evil_attributes)]); - } - - do { - $str = preg_replace( - "#<(/?[^><]+?)([^A-Za-z\-])(".implode('|', $evil_attributes).")(\s*=\s*)([\"][^>]*?[\"]|[\'][^>]*?[\']|[^>]*?)([\s><])([><]*)#i", - "<$1$6", - $str, -1, $count - ); - } while ($count); - - return $str; - } - - // -------------------------------------------------------------------- - - /** - * Sanitize Naughty HTML - * - * Callback function for xss_clean() to remove naughty HTML elements - * - * @param array - * @return string - */ - protected function _sanitize_naughty_html($matches) - { - // encode opening brace - $str = '<'.$matches[1].$matches[2].$matches[3]; - - // encode captured opening or closing brace to prevent recursive vectors - $str .= str_replace(array('>', '<'), array('>', '<'), - $matches[4]); - - return $str; - } - - // -------------------------------------------------------------------- - - /** - * JS Link Removal - * - * Callback function for xss_clean() to sanitize links - * This limits the PCRE backtracks, making it more performance friendly - * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in - * PHP 5.2+ on link-heavy strings - * - * @param array - * @return string - */ - protected function _js_link_removal($match) - { - $attributes = $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1])); - - return str_replace($match[1], preg_replace("#href=.*?(alert\(|alert&\#40;|javascript\:|livescript\:|mocha\:|charset\=|window\.|document\.|\.cookie|_filter_attributes(str_replace(array('<', '>'), '', $match[1])); - - return str_replace($match[1], preg_replace("#src=.*?(alert\(|alert&\#40;|javascript\:|livescript\:|mocha\:|charset\=|window\.|document\.|\.cookie|', '<', '\\'), array('>', '<', '\\\\'), $match[0]); - } - - // -------------------------------------------------------------------- - - /** - * Filter Attributes - * - * Filters tag attributes for consistency and safety - * - * @param string - * @return string - */ - protected function _filter_attributes($str) - { - $out = ''; - - if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches)) - { - foreach ($matches[0] as $match) - { - $out .= preg_replace("#/\*.*?\*/#s", '', $match); - } - } - - return $out; - } - - // -------------------------------------------------------------------- - - /** - * HTML Entity Decode Callback - * - * Used as a callback for XSS Clean - * - * @param array - * @return string - */ - protected function _decode_entity($match) - { - return $this->entity_decode($match[0], strtoupper(config_item('charset'))); - } - - // -------------------------------------------------------------------- - - /** - * Validate URL entities - * - * Called by xss_clean() - * - * @param string - * @return string - */ - protected function _validate_entities($str) - { - /* - * Protect GET variables in URLs - */ - - // 901119URL5918AMP18930PROTECT8198 - - $str = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-]+)|i', $this->xss_hash()."\\1=\\2", $str); - - /* - * Validate standard character entities - * - * Add a semicolon if missing. We do this to enable - * the conversion of entities to ASCII later. - * - */ - $str = preg_replace('#(&\#?[0-9a-z]{2,})([\x00-\x20])*;?#i', "\\1;\\2", $str); - - /* - * Validate UTF16 two byte encoding (x00) - * - * Just as above, adds a semicolon if missing. - * - */ - $str = preg_replace('#(&\#x?)([0-9A-F]+);?#i',"\\1\\2;",$str); - - /* - * Un-Protect GET variables in URLs - */ - $str = str_replace($this->xss_hash(), '&', $str); - - return $str; - } - - // ---------------------------------------------------------------------- - - /** - * Do Never Allowed - * - * A utility function for xss_clean() - * - * @param string - * @return string - */ - protected function _do_never_allowed($str) - { - foreach ($this->_never_allowed_str as $key => $val) - { - $str = str_replace($key, $val, $str); - } - - foreach ($this->_never_allowed_regex as $key => $val) - { - $str = preg_replace("#".$key."#i", $val, $str); - } - - return $str; - } - - // -------------------------------------------------------------------- - - /** - * Set Cross Site Request Forgery Protection Cookie - * - * @return string - */ - protected function _csrf_set_hash() - { - if ($this->_csrf_hash == '') - { - // If the cookie exists we will use it's value. - // We don't necessarily want to regenerate it with - // each page load since a page could contain embedded - // sub-pages causing this feature to fail - if (isset($_COOKIE[$this->_csrf_cookie_name]) && - $_COOKIE[$this->_csrf_cookie_name] != '') - { - return $this->_csrf_hash = $_COOKIE[$this->_csrf_cookie_name]; - } - - return $this->_csrf_hash = md5(uniqid(rand(), TRUE)); - } - - return $this->_csrf_hash; - } - -} -// END Security Class - -/* End of file Security.php */ -/* Location: ./system/libraries/Security.php */ \ No newline at end of file -- cgit v1.2.3-24-g4f1b