Merge pull request #366 from kijin/pr/security-refactor

보안관련 클래스 전반적 정리 및 기능 개선 프로젝트
This commit is contained in:
Kijin Sung 2016-03-14 22:19:59 +09:00
commit 4f015f7bbc
78 changed files with 3860 additions and 3336 deletions

View file

@ -394,11 +394,11 @@ class Context
}
if (strpos($current_url, 'xn--') !== false)
{
$current_url = self::decodeIdna($current_url);
$current_url = Rhymix\Framework\URL::decodeIdna($current_url);
}
if (strpos($request_uri, 'xn--') !== false)
{
$request_uri = self::decodeIdna($request_uri);
$request_uri = Rhymix\Framework\URL::decodeIdna($request_uri);
}
self::set('current_url', $current_url);
self::set('request_uri', $request_uri);
@ -574,8 +574,8 @@ class Context
$db_info->sitelock_title = $config['lock']['title'];
$db_info->sitelock_message = $config['lock']['message'];
$db_info->sitelock_whitelist = count($config['lock']['allow']) ? $config['lock']['allow'] : array('127.0.0.1');
$db_info->embed_white_iframe = $config['embedfilter']['iframe'];
$db_info->embed_white_object = $config['embedfilter']['object'];
$db_info->embed_white_iframe = $config['mediafilter']['iframe'] ?: $config['embedfilter']['iframe'];
$db_info->embed_white_object = $config['mediafilter']['object'] ?: $config['embedfilter']['object'];
$db_info->use_mobile_view = $config['use_mobile_view'] ? 'Y' : 'N';
$db_info->use_prepared_statements = $config['use_prepared_statements'] ? 'Y' : 'N';
$db_info->use_rewrite = $config['use_rewrite'] ? 'Y' : 'N';
@ -712,61 +712,66 @@ class Context
$current_site = self::getRequestUri();
// Step 1: if the current site is not the default site, send SSO validation request to the default site
if($default_url !== $current_site && !self::get('SSOID') && $_COOKIE['sso'] !== md5($current_site))
if($default_url !== $current_site && !self::get('sso_response') && $_COOKIE['sso'] !== md5($current_site))
{
// Set sso cookie to prevent multiple simultaneous SSO validation requests
setcookie('sso', md5($current_site), 0, '/');
// Redirect to the default site
$redirect_url = sprintf('%s?return_url=%s', $default_url, urlencode(base64_encode($current_site)));
$sso_request = Rhymix\Framework\Security::encrypt(Rhymix\Framework\URL::getCurrentURL());
$redirect_url = $default_url . '?sso_request=' . urlencode($sso_request);
header('Location:' . $redirect_url);
return FALSE;
return false;
}
// Step 2: receive and process SSO validation request at the default site
if($default_url === $current_site && self::get('return_url'))
if($default_url === $current_site && self::get('sso_request'))
{
// Get the URL of the origin site
$url = base64_decode(self::get('return_url'));
$url_info = parse_url($url);
$sso_request = Rhymix\Framework\Security::decrypt(self::get('sso_request'));
if (!$sso_request || !preg_match('!^https?://!', $sso_request))
{
self::displayErrorPage('SSO Error', 'Invalid SSO Request', 400);
return false;
}
// Check that the origin site is a valid site in this XE installation (to prevent open redirect vuln)
if(!getModel('module')->getSiteInfoByDomain(rtrim($url, '/'))->site_srl)
{
htmlHeader();
echo self::getLang("msg_invalid_request");
htmlFooter();
return FALSE;
self::displayErrorPage('SSO Error', 'Invalid SSO Request', 400);
return false;
}
// Redirect back to the origin site
$url_info['query'] .= ($url_info['query'] ? '&' : '') . 'SSOID=' . session_id();
$redirect_url = sprintf('%s://%s%s%s%s', $url_info['scheme'], $url_info['host'], $url_info['port'] ? (':' . $url_info['port']) : '', $url_info['path'], ($url_info['query'] ? ('?' . $url_info['query']) : ''));
header('Location:' . $redirect_url);
return FALSE;
$sso_response = Rhymix\Framework\Security::encrypt(session_id());
header('Location: ' . Rhymix\Framework\URL::modifyURL($sso_request, array('sso_response' => $sso_response)));
return false;
}
// Step 3: back at the origin site, set session ID to be the same as the default site
if($default_url !== $current_site && self::get('SSOID'))
if($default_url !== $current_site && self::get('sso_response'))
{
// Check that the session ID was given by the default site (to prevent session fixation CSRF)
// Check SSO response
$sso_response = Rhymix\Framework\Security::decrypt(self::get('sso_response'));
if ($sso_response === false)
{
self::displayErrorPage('SSO Error', 'Invalid SSO Response', 400);
return false;
}
// Check that the response was given by the default site (to prevent session fixation CSRF)
if(isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], $default_url) !== 0)
{
htmlHeader();
echo self::getLang("msg_invalid_request");
htmlFooter();
return FALSE;
self::displayErrorPage('SSO Error', 'Invalid SSO Response', 400);
return false;
}
// Set session ID
setcookie(session_name(), self::get('SSOID'));
setcookie(session_name(), $sso_response);
// Finally, redirect to the originally requested URL
$url_info = parse_url(self::getRequestUrl());
$url_info['query'] = preg_replace('/(^|\b)SSOID=([^&?]+)/', '', $url_info['query']);
$redirect_url = sprintf('%s://%s%s%s%s', $url_info['scheme'], $url_info['host'], $url_info['port'] ? (':' . $url_info['port']) : '', $url_info['path'], ($url_info['query'] ? ('?' . $url_info['query']) : ''));
header('Location:' . $redirect_url);
return FALSE;
header('Location: ' . Rhymix\Framework\URL::getCurrentURL(array('sso_response' => null)));
return false;
}
// If none of the conditions above apply, proceed normally
@ -1073,15 +1078,7 @@ class Context
*/
public static function encodeIdna($domain)
{
if(function_exists('idn_to_ascii'))
{
return idn_to_ascii($domain);
}
else
{
$encoder = new TrueBV\Punycode();
return $encoder->encode($domain);
}
return Rhymix\Framework\URL::encodeIdna($domain);
}
/**
@ -1092,15 +1089,7 @@ class Context
*/
public static function decodeIdna($domain)
{
if(function_exists('idn_to_utf8'))
{
return idn_to_utf8($domain);
}
else
{
$decoder = new TrueBV\Punycode();
return $decoder->decode($domain);
}
return Rhymix\Framework\URL::decodeIdna($domain);
}
/**
@ -1283,11 +1272,15 @@ class Context
}
$xml = $GLOBALS['HTTP_RAW_POST_DATA'];
if(Security::detectingXEE($xml))
if(!Rhymix\Framework\Security::checkXEE($xml))
{
header("HTTP/1.0 400 Bad Request");
exit;
}
if(function_exists('libxml_disable_entity_loader'))
{
libxml_disable_entity_loader(true);
}
$oXml = new XmlParser();
$xml_obj = $oXml->parse($xml);
@ -1490,13 +1483,9 @@ class Context
}
// Allow if the current user is in the list of allowed IPs.
$allowed_list = config('lock.allow');
foreach ($allowed_list as $allowed_ip)
if (Rhymix\Framework\Filters\IpFilter::inRanges(RX_CLIENT_IP, config('lock.allow')))
{
if (Rhymix\Framework\IpFilter::inRange(RX_CLIENT_IP, $allowed_ip))
{
return;
}
return;
}
// Set headers and constants for backward compatibility.

View file

@ -3,313 +3,78 @@
class EmbedFilter
{
/**
* allow script access list
* Deprecated properties
* @var array
*/
var $allowscriptaccessList = array();
/**
* allow script access key
* @var int
*/
var $allowscriptaccessKey = 0;
var $whiteUrlList = array();
var $whiteIframeUrlList = array();
var $mimeTypeList = array();
var $extList = array();
var $parser = NULL;
/**
* @constructor
* @return void
*/
function __construct()
{
$this->_makeWhiteDomainList();
}
public $whiteUrlList = array();
public $whiteIframeUrlList = array();
public $mimeTypeList = array();
public $extList = array();
/**
* Return EmbedFilter object
* This method for singleton
*
* @return EmbedFilter
*/
function getInstance()
{
if(!isset($GLOBALS['__EMBEDFILTER_INSTANCE__']))
{
$GLOBALS['__EMBEDFILTER_INSTANCE__'] = new EmbedFilter();
}
return $GLOBALS['__EMBEDFILTER_INSTANCE__'];
return new self();
}
public function getWhiteUrlList()
{
return $this->whiteUrlList;
return Rhymix\Framework\Filters\MediaFilter::getObjectWhitelist();
}
public function getWhiteIframeUrlList()
{
return $this->whiteIframeUrlList;
return Rhymix\Framework\Filters\MediaFilter::getIframeWhitelist();
}
/**
* Check the content.
* @return void
*/
function check(&$content)
{
$content = preg_replace_callback('/<(object|param|embed)[^>]*/is', array($this, '_checkAllowScriptAccess'), $content);
$content = preg_replace_callback('/<object[^>]*>/is', array($this, '_addAllowScriptAccess'), $content);
$this->checkObjectTag($content);
$this->checkEmbedTag($content);
$this->checkParamTag($content);
}
/**
* Check iframe tag in the content.
* @return void
*/
function checkIframeTag(&$content)
{
// check in Purifier class
return;
}
/**
* Check object tag in the content.
* @return void
*/
function checkObjectTag(&$content)
{
$content = preg_replace_callback('/<\s*object\s*[^>]+(?:\/?>?)/is', function($m) {
$html = Sunra\PhpSimple\HtmlDomParser::str_get_html($m[0]);
foreach ($html->find('object') as $element)
{
if ($element->data && !$this->isWhiteDomain($element->data))
{
return escape($m[0], false);
}
if ($element->type && !$this->isWhiteMimetype($element->type))
{
return escape($m[0], false);
}
}
return $m[0];
}, $content);
}
/**
* Check embed tag in the content.
* @return void
*/
function checkEmbedTag(&$content)
{
$content = preg_replace_callback('/<\s*embed\s*[^>]+(?:\/?>?)/is', function($m) {
$html = Sunra\PhpSimple\HtmlDomParser::str_get_html($m[0]);
foreach ($html->find('embed') as $element)
{
if ($element->src && !$this->isWhiteDomain($element->src))
{
return escape($m[0], false);
}
if ($element->type && !$this->isWhiteMimetype($element->type))
{
return escape($m[0], false);
}
}
return $m[0];
}, $content);
}
/**
* Check param tag in the content.
* @return void
*/
function checkParamTag(&$content)
{
$content = preg_replace_callback('/<\s*param\s*[^>]+(?:\/?>?)/is', function($m) {
$html = Sunra\PhpSimple\HtmlDomParser::str_get_html($m[0]);
foreach ($html->find('param') as $element)
{
foreach (array('movie', 'src', 'href', 'url', 'source') as $attr)
{
if ($element->$attr && !$this->isWhiteDomain($element->$attr))
{
return escape($m[0], false);
}
}
}
return $m[0];
}, $content);
}
/**
* Check white domain in object data attribute or embed src attribute.
* @return string
*/
function isWhiteDomain($urlAttribute)
{
if(is_array($this->whiteUrlList))
{
foreach($this->whiteUrlList AS $key => $value)
{
if(preg_match('@^https?://' . preg_quote($value, '@') . '@i', $urlAttribute))
{
return TRUE;
}
}
}
return FALSE;
return Rhymix\Framework\Filters\MediaFilter::matchObjectWhitelist($urlAttribute);
}
/**
* Check white domain in iframe src attribute.
* @return string
*/
function isWhiteIframeDomain($urlAttribute)
{
if(is_array($this->whiteIframeUrlList))
{
foreach($this->whiteIframeUrlList AS $key => $value)
{
if(preg_match('@^https?://' . preg_quote($value, '@') . '@i', $urlAttribute))
{
return TRUE;
}
}
}
return FALSE;
return Rhymix\Framework\Filters\MediaFilter::matchIframeWhitelist($urlAttribute);
}
/**
* Check white mime type in object type attribute or embed type attribute.
* @return string
*/
function isWhiteMimetype($mimeType)
{
if(isset($this->mimeTypeList[$mimeType]))
{
return TRUE;
}
return FALSE;
return true;
}
function isWhiteExt($ext)
{
if(isset($this->extList[$ext]))
{
return TRUE;
}
return FALSE;
return true;
}
function _checkAllowScriptAccess($m)
function check(&$content)
{
if($m[1] == 'object')
{
$this->allowscriptaccessList[] = 1;
}
if($m[1] == 'param')
{
if(stripos($m[0], 'allowscriptaccess'))
{
$m[0] = '<param name="allowscriptaccess" value="never"';
if(substr($m[0], -1) == '/')
{
$m[0] .= '/';
}
$this->allowscriptaccessList[count($this->allowscriptaccessList) - 1]--;
}
}
else if($m[1] == 'embed')
{
if(stripos($m[0], 'allowscriptaccess'))
{
$m[0] = preg_replace('/always|samedomain/i', 'never', $m[0]);
}
else
{
$m[0] = preg_replace('/\<embed/i', '<embed allowscriptaccess="never"', $m[0]);
}
}
return $m[0];
// This functionality has been moved to the HTMLFilter class.
}
function _addAllowScriptAccess($m)
function checkIframeTag(&$content)
{
if($this->allowscriptaccessList[$this->allowscriptaccessKey] == 1)
{
$m[0] = $m[0] . '<param name="allowscriptaccess" value="never"></param>';
}
$this->allowscriptaccessKey++;
return $m[0];
// This functionality has been moved to the HTMLFilter class.
}
/**
* Make white domain list cache file from xml config file.
* @param $whitelist array
* @return void
*/
function _makeWhiteDomainList($whitelist = NULL)
function checkObjectTag(&$content)
{
$whiteUrlDefaultList = (include RX_BASEDIR . 'common/defaults/whitelist.php');
$this->extList = $whiteUrlDefaultList['extensions'];
$this->mimeTypeList = $whiteUrlDefaultList['mime'];
$this->whiteUrlList = array();
$this->whiteIframeUrlList = array();
if($whitelist !== NULL)
{
if(!is_array($whitelist) || !isset($whitelist['object']) || !isset($whitelist['iframe']))
{
$whitelist = array(
'object' => isset($whitelist->object) ? $whitelist->object : array(),
'iframe' => isset($whitelist->iframe) ? $whitelist->iframe : array(),
);
}
foreach ($whitelist['object'] as $prefix)
{
$this->whiteUrlList[] = preg_match('@^https?://(.*)$@i', $prefix, $matches) ? $matches[1] : $prefix;
}
foreach ($whitelist['iframe'] as $prefix)
{
$this->whiteIframeUrlList[] = preg_match('@^https?://(.*)$@i', $prefix, $matches) ? $matches[1] : $prefix;
}
}
else
{
foreach ($whiteUrlDefaultList['object'] as $prefix)
{
$this->whiteUrlList[] = $prefix;
}
foreach ($whiteUrlDefaultList['iframe'] as $prefix)
{
$this->whiteIframeUrlList[] = $prefix;
}
if ($embedfilter_object = config('embedfilter.object'))
{
foreach ($embedfilter_object as $prefix)
{
$this->whiteUrlList[] = preg_match('@^https?://(.*)$@i', $prefix, $matches) ? $matches[1] : $prefix;
}
}
if ($embedfilter_iframe = config('embedfilter.iframe'))
{
foreach ($embedfilter_iframe as $prefix)
{
$this->whiteIframeUrlList[] = preg_match('@^https?://(.*)$@i', $prefix, $matches) ? $matches[1] : $prefix;
}
}
}
$this->whiteUrlList = array_unique($this->whiteUrlList);
$this->whiteIframeUrlList = array_unique($this->whiteIframeUrlList);
natcasesort($this->whiteUrlList);
natcasesort($this->whiteIframeUrlList);
// This functionality has been moved to the HTMLFilter class.
}
function checkEmbedTag(&$content)
{
// This functionality has been moved to the HTMLFilter class.
}
function checkParamTag(&$content)
{
// This functionality has been moved to the HTMLFilter class.
}
}
/* End of file : EmbedFilter.class.php */

View file

@ -6,26 +6,12 @@ class IpFilter
public function filter($ip_list, $ip = NULL)
{
if(!$ip) $ip = $_SERVER['REMOTE_ADDR'];
foreach($ip_list as $filter)
{
if(Rhymix\Framework\IpFilter::inRange($ip, $filter))
{
return true;
}
}
return false;
return Rhymix\Framework\Filters\IpFilter::inRanges($ip, $ip_list);
}
public function validate($ip_list = array())
{
foreach($ip_list as $filter)
{
if(!Rhymix\Framework\IpFilter::validateRange($filter))
{
return false;
}
}
return true;
return Rhymix\Framework\Filters\IpFilter::validateRanges($ip_list);
}
}

View file

@ -1,458 +1,79 @@
<?php
/* Copyright (C) NAVER <http://www.navercorp.com> */
/**
* This class can be used to hash passwords using various algorithms and check their validity.
* It is fully compatible with previous defaults, while also supporting bcrypt and pbkdf2.
*
* @file Password.class.php
* @author Kijin Sung (kijin@kijinsung.com)
* @package /classes/security
* @version 1.1
*/
class Password
{
/**
* @brief Custom algorithms are stored here
* @var array
*/
protected static $_custom = array();
/**
* @brief Register a custom algorithm for password checking
* @param string $name The name of the algorithm
* @param string $regexp The regular expression to detect the algorithm
* @param callable $callback The function to call to regenerate the hash
* @return void
*/
public static function registerCustomAlgorithm($name, $regexp, $callback)
{
self::$_custom[$name] = array('regexp' => $regexp, 'callback' => $callback);
Rhymix\Framework\Password::addAlgorithm($name, $regexp, $callback);
}
/**
* @brief Return the list of hashing algorithms supported by this server
* @return array
*/
public function getSupportedAlgorithms()
{
$retval = array();
if(function_exists('hash_hmac') && in_array('sha256', hash_algos()))
{
$retval['pbkdf2'] = 'pbkdf2';
}
if(version_compare(PHP_VERSION, '5.3.7', '>=') && defined('CRYPT_BLOWFISH'))
{
$retval['bcrypt'] = 'bcrypt';
}
$retval['md5'] = 'md5';
return $retval;
return Rhymix\Framework\Password::getSupportedAlgorithms();
}
/**
* @brief Return the best hashing algorithm supported by this server
* @return string
*/
public function getBestAlgorithm()
{
$algos = $this->getSupportedAlgorithms();
return key($algos);
return Rhymix\Framework\Password::getBestSupportedAlgorithm();
}
/**
* @brief Return the currently selected hashing algorithm
* @return string
*/
public function getCurrentlySelectedAlgorithm()
{
if(function_exists('getModel'))
{
$config = getModel('member')->getMemberConfig();
$algorithm = $config->password_hashing_algorithm;
if(strval($algorithm) === '')
{
$algorithm = 'md5'; // Historical default for XE
}
}
else
{
$algorithm = 'md5';
}
return $algorithm;
return Rhymix\Framework\Password::getDefaultAlgorithm();
}
/**
* @brief Return the currently configured work factor for bcrypt and other adjustable algorithms
* @return int
*/
public function getWorkFactor()
{
if(function_exists('getModel'))
{
$config = getModel('member')->getMemberConfig();
$work_factor = $config->password_hashing_work_factor;
if(!$work_factor || $work_factor < 4 || $work_factor > 31)
{
$work_factor = 8; // Reasonable default
}
}
else
{
$work_factor = 8;
}
return $work_factor;
return Rhymix\Framework\Password::getWorkFactor();
}
/**
* @brief Create a hash using the specified algorithm
* @param string $password The password
* @param string $algorithm The algorithm (optional)
* @return string
*/
public function createHash($password, $algorithm = null)
{
if($algorithm === null)
{
$algorithm = $this->getCurrentlySelectedAlgorithm();
}
if(!array_key_exists($algorithm, $this->getSupportedAlgorithms()))
{
return false;
}
$password = trim($password);
switch($algorithm)
{
case 'md5':
return md5($password);
case 'pbkdf2':
$iterations = pow(2, $this->getWorkFactor() + 5);
$salt = $this->createSecureSalt(12, 'alnum');
$hash = base64_encode($this->pbkdf2($password, $salt, 'sha256', $iterations, 24));
return 'sha256:'.sprintf('%07d', $iterations).':'.$salt.':'.$hash;
case 'bcrypt':
return $this->bcrypt($password);
default:
return false;
}
return Rhymix\Framework\Password::hashPassword($password, $algorithm);
}
/**
* @brief Check if a password matches a hash
* @param string $password The password
* @param string $hash The hash
* @param string $algorithm The algorithm (optional)
* @return bool
*/
public function checkPassword($password, $hash, $algorithm = null)
{
if($algorithm === null)
{
$algorithm = $this->checkAlgorithm($hash);
}
$password = trim($password);
switch($algorithm)
{
case 'md5':
return md5($password) === $hash || md5(sha1(md5($password))) === $hash;
case 'mysql_old_password':
return (class_exists('Context') && substr(Context::getDBType(), 0, 5) === 'mysql') ?
DB::getInstance()->isValidOldPassword($password, $hash) : false;
case 'mysql_password':
return $hash[0] === '*' && substr($hash, 1) === strtoupper(sha1(sha1($password, true)));
case 'pbkdf2':
$hash = explode(':', $hash);
$hash[3] = base64_decode($hash[3]);
$hash_to_compare = $this->pbkdf2($password, $hash[2], $hash[0], intval($hash[1], 10), strlen($hash[3]));
return $this->strcmpConstantTime($hash_to_compare, $hash[3]);
case 'bcrypt':
$hash_to_compare = $this->bcrypt($password, $hash);
return $this->strcmpConstantTime($hash_to_compare, $hash);
default:
if($algorithm && isset(self::$_custom[$algorithm]))
{
$hash_callback = self::$_custom[$algorithm]['callback'];
$hash_to_compare = $hash_callback($password, $hash);
return $this->strcmpConstantTime($hash_to_compare, $hash);
}
if(in_array($algorithm, hash_algos()))
{
return $this->strcmpConstantTime(hash($algorithm, $password), $hash);
}
return false;
}
return Rhymix\Framework\Password::checkPassword($password, $hash, $algorithm);
}
/**
* @brief Check the algorithm used to create a hash
* @param string $hash The hash
* @return string
*/
function checkAlgorithm($hash)
{
foreach(self::$_custom as $name => $definition)
{
if(preg_match($definition['regexp'], $hash))
{
return $name;
}
}
if(preg_match('/^\$2[axy]\$([0-9]{2})\$/', $hash, $matches))
{
return 'bcrypt';
}
elseif(preg_match('/^sha[0-9]+:([0-9]+):/', $hash, $matches))
{
return 'pbkdf2';
}
elseif(strlen($hash) === 32 && ctype_xdigit($hash))
{
return 'md5';
}
elseif(strlen($hash) === 40 && ctype_xdigit($hash))
{
return 'sha1';
}
elseif(strlen($hash) === 64 && ctype_xdigit($hash))
{
return 'sha256';
}
elseif(strlen($hash) === 96 && ctype_xdigit($hash))
{
return 'sha384';
}
elseif(strlen($hash) === 128 && ctype_xdigit($hash))
{
return 'sha512';
}
elseif(strlen($hash) === 16 && ctype_xdigit($hash))
{
return 'mysql_old_password';
}
elseif(strlen($hash) === 41 && $hash[0] === '*')
{
return 'mysql_password';
}
else
{
return false;
}
$algos = Rhymix\Framework\Password::checkAlgorithm($hash);
return count($algos) ? $algos[0] : false;
}
/**
* @brief Check the work factor of a hash
* @param string $hash The hash
* @return int
*/
function checkWorkFactor($hash)
{
if(preg_match('/^\$2[axy]\$([0-9]{2})\$/', $hash, $matches))
{
return intval($matches[1], 10);
}
elseif(preg_match('/^sha[0-9]+:([0-9]+):/', $hash, $matches))
{
return max(0, round(log($matches[1], 2)) - 5);
}
else
{
return false;
}
return Rhymix\Framework\Password::checkWorkFactor($hash);
}
/**
* @brief Generate a cryptographically secure random string to use as a salt
* @param int $length The number of bytes to return
* @param string $format hex or alnum
* @return string
*/
public function createSecureSalt($length, $format = 'hex')
{
// Find out how many bytes of entropy we really need
switch($format)
{
case 'hex':
$entropy_required_bytes = ceil($length / 2);
break;
case 'alnum':
case 'printable':
$entropy_required_bytes = ceil($length * 3 / 4);
break;
default:
$entropy_required_bytes = $length;
}
// Cap entropy to 256 bits from any one source, because anything more is meaningless
$entropy_capped_bytes = min(32, $entropy_required_bytes);
// Find and use the most secure way to generate a random string
$entropy = false;
$is_windows = (defined('PHP_OS') && strtoupper(substr(PHP_OS, 0, 3)) === 'WIN');
if(function_exists('random_bytes')) // PHP 7
{
$entropy = random_bytes($entropy_capped_bytes);
}
elseif(function_exists('openssl_random_pseudo_bytes') && (!$is_windows || version_compare(PHP_VERSION, '5.4', '>=')))
{
$entropy = openssl_random_pseudo_bytes($entropy_capped_bytes);
}
elseif(function_exists('mcrypt_create_iv') && (!$is_windows || version_compare(PHP_VERSION, '5.3.7', '>=')))
{
$entropy = mcrypt_create_iv($entropy_capped_bytes, MCRYPT_DEV_URANDOM);
}
elseif(function_exists('mcrypt_create_iv') && $is_windows)
{
$entropy = mcrypt_create_iv($entropy_capped_bytes, MCRYPT_RAND);
}
elseif(!$is_windows && @is_readable('/dev/urandom'))
{
$fp = fopen('/dev/urandom', 'rb');
if (function_exists('stream_set_read_buffer')) // This function does not exist in HHVM
{
stream_set_read_buffer($fp, 0); // Prevent reading several KB of unnecessary data from urandom
}
$entropy = fread($fp, $entropy_capped_bytes);
fclose($fp);
}
// Use built-in source of entropy if an error occurs while using other functions
if($entropy === false || strlen($entropy) < $entropy_capped_bytes)
{
$entropy = '';
for($i = 0; $i < $entropy_capped_bytes; $i += 2)
{
$entropy .= pack('S', rand(0, 65536) ^ mt_rand(0, 65535));
}
}
// Mixing (see RFC 4086 section 5)
$output = '';
for($i = 0; $i < $entropy_required_bytes; $i += 32)
{
$output .= hash('sha256', $entropy . $i . rand(), true);
}
// Encode and return the random string
switch($format)
{
case 'hex':
return substr(bin2hex($output), 0, $length);
case 'binary':
return substr($output, 0, $length);
case 'printable':
$salt = '';
for($i = 0; $i < $length; $i++)
{
$salt .= chr(33 + (crc32(sha1($i . $output)) % 94));
}
return $salt;
case 'alnum':
default:
$salt = substr(base64_encode($output), 0, $length);
$replacements = chr(rand(65, 90)) . chr(rand(97, 122)) . rand(0, 9);
return strtr($salt, '+/=', $replacements);
}
return Rhymix\Framework\Security::getRandom($length, $format);
}
/**
* @brief Generate a temporary password using the secure salt generator
* @param int $length The number of bytes to return
* @return string
*/
public function createTemporaryPassword($length = 16)
{
while(true)
{
$source = base64_encode($this->createSecureSalt(64, 'binary'));
$source = strtr($source, 'iIoOjl10/', '@#$%&*-!?');
$source_length = strlen($source);
for($i = 0; $i < $source_length - $length; $i++)
{
$candidate = substr($source, $i, $length);
if(preg_match('/[a-z]/', $candidate) && preg_match('/[A-Z]/', $candidate) &&
preg_match('/[0-9]/', $candidate) && preg_match('/[^a-zA-Z0-9]/', $candidate))
{
return $candidate;
}
}
}
return Rhymix\Framework\Password::getRandomPassword($length);
}
/**
* @brief Generate the PBKDF2 hash of a string using a salt
* @param string $password The password
* @param string $salt The salt
* @param string $algorithm The algorithm (optional, default is sha256)
* @param int $iterations Iteration count (optional, default is 8192)
* @param int $length The length of the hash (optional, default is 32)
* @return string
*/
public function pbkdf2($password, $salt, $algorithm = 'sha256', $iterations = 8192, $length = 24)
{
if(function_exists('hash_pbkdf2'))
{
return hash_pbkdf2($algorithm, $password, $salt, $iterations, $length, true);
}
else
{
$output = '';
$block_count = ceil($length / strlen(hash($algorithm, '', true))); // key length divided by the length of one hash
for($i = 1; $i <= $block_count; $i++)
{
$last = $salt . pack('N', $i); // $i encoded as 4 bytes, big endian
$last = $xorsum = hash_hmac($algorithm, $last, $password, true); // first iteration
for($j = 1; $j < $iterations; $j++) // The other $count - 1 iterations
{
$xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
}
$output .= $xorsum;
}
return substr($output, 0, $length);
}
$hash = Rhymix\Framework\Security::pbkdf2($password, $salt, $algorithm, $iterations, $length);
$hash = explode(':', $hash);
return base64_decode($hash[3]);
}
/**
* @brief Generate the bcrypt hash of a string using a salt
* @param string $password The password
* @param string $salt The salt (optional, auto-generated if empty)
* @return string
*/
public function bcrypt($password, $salt = null)
{
if($salt === null)
{
$salt = '$2y$'.sprintf('%02d', $this->getWorkFactor()).'$'.$this->createSecureSalt(22, 'alnum');
}
return crypt($password, $salt);
return Rhymix\Framework\Security::bcrypt($password, $salt);
}
/**
* @brief Compare two strings in constant time
* @param string $a The first string
* @param string $b The second string
* @return bool
*/
function strcmpConstantTime($a, $b)
{
$diff = strlen($a) ^ strlen($b);
$maxlen = min(strlen($a), strlen($b));
for($i = 0; $i < $maxlen; $i++)
{
$diff |= ord($a[$i]) ^ ord($b[$i]);
}
return $diff === 0;
return Rhymix\Framework\Security::compareStrings($a, $b);
}
}
/* End of file : Password.class.php */

View file

@ -3,161 +3,14 @@
class Purifier
{
private $_cacheDir;
private $_htmlPurifier;
private $_config;
private $_def;
public function __construct()
public static function getInstance()
{
$this->_checkCacheDir();
$this->_setConfig();
return new self();
}
public function getInstance()
{
if(!isset($GLOBALS['__PURIFIER_INSTANCE__']))
{
$GLOBALS['__PURIFIER_INSTANCE__'] = new Purifier();
}
return $GLOBALS['__PURIFIER_INSTANCE__'];
}
private function _setConfig()
{
$this->_config = HTMLPurifier_Config::createDefault();
$this->_config->set('HTML.TidyLevel', 'light');
$this->_config->set('Output.FlashCompat', TRUE);
$this->_config->set('HTML.SafeObject', TRUE);
$this->_config->set('HTML.SafeEmbed', TRUE);
$this->_config->set('HTML.SafeIframe', TRUE);
$this->_config->set('URI.SafeIframeRegexp', $this->_getWhiteDomainRegexp());
$this->_config->set('Cache.SerializerPath', $this->_cacheDir);
$this->_config->set('Attr.AllowedFrameTargets', array('_blank'));
//$allowdClasses = array('emoticon');
//$this->_config->set('Attr.AllowedClasses', $allowdClasses);
$this->_def = $this->_config->getHTMLDefinition(TRUE);
}
private function _setDefinition(&$content)
{
// add attribute for edit component
$editComponentAttrs = $this->_searchEditComponent($content);
if(is_array($editComponentAttrs))
{
foreach($editComponentAttrs AS $k => $v)
{
$this->_def->addAttribute('img', $v, 'CDATA');
$this->_def->addAttribute('div', $v, 'CDATA');
}
}
// add attribute for widget component
$widgetAttrs = $this->_searchWidget($content);
if(is_array($widgetAttrs))
{
foreach($widgetAttrs AS $k => $v)
{
$this->_def->addAttribute('img', $v, 'CDATA');
}
}
}
/**
* Search attribute of edit component tag
* @param string $content
* @return array
*/
private function _searchEditComponent($content)
{
preg_match_all('!<(?:(div)|img)([^>]*)editor_component=([^>]*)>(?(1)(.*?)</div>)!is', $content, $m);
$attributeList = array();
if(is_array($m[2]))
{
foreach($m[2] as $key => $value)
{
unset($script, $m2);
$script = " {$m[2][$key]} editor_component={$m[3][$key]}";
if(preg_match_all('/([a-z0-9_-]+)="([^"]+)"/is', $script, $m2))
{
foreach($m2[1] as $value2)
{
//SECISSUE check style attr
if($value2 == 'style')
{
continue;
}
$attributeList[] = $value2;
}
}
}
}
return array_unique($attributeList);
}
/**
* Search edit component tag
* @param string $content
* @return array
*/
private function _searchWidget(&$content)
{
preg_match_all('!<(?:(div)|img)([^>]*)class="zbxe_widget_output"([^>]*)>(?(1)(.*?)</div>)!is', $content, $m);
$attributeList = array();
if(is_array($m[3]))
{
$content = str_replace('<img class="zbxe_widget_output"', '<img src="" class="zbxe_widget_output"', $content);
foreach($m[3] as $key => $value)
{
if (preg_match_all('/([a-z0-9_-]+)="([^"]+)"/is', $m[3][$key], $m2))
{
foreach($m2[1] as $value2)
{
//SECISSUE check style attr
if($value2 == 'style')
{
continue;
}
$attributeList[] = $value2;
}
}
}
}
return array_unique($attributeList);
}
private function _getWhiteDomainRegexp()
{
$oEmbedFilter = EmbedFilter::getInstance();
$whiteIframeUrlList = $oEmbedFilter->getWhiteIframeUrlList();
$whiteDomains = array();
foreach($whiteIframeUrlList as $domain)
{
$whiteDomains[] = preg_quote($domain, '%');
}
return '%^https?://(' . implode('|', $whiteDomains) . ')%';
}
private function _checkCacheDir()
{
// check htmlpurifier cache directory
$this->_cacheDir = _XE_PATH_ . 'files/cache/htmlpurifier';
FileHandler::makeDir($this->_cacheDir);
}
public function purify(&$content)
{
$this->_setDefinition($content);
$this->_htmlPurifier = new HTMLPurifier($this->_config);
$content = $this->_htmlPurifier->purify($content);
$content = Rhymix\Framework\Filters\HTMLFilter::clean($content);
}
}

View file

@ -15,14 +15,14 @@ class Security
* Action target variable. If this value is null, the method will use Context variables
* @var mixed
*/
var $_targetVar = NULL;
public $_targetVar = NULL;
/**
* @constructor
* @param mixed $var Target context
* @return void
*/
function __construct($var = NULL)
public function __construct($var = NULL)
{
$this->_targetVar = $var;
}
@ -34,7 +34,7 @@ class Security
* separate the owner(object or array) and the item(property or element) using a dot(.)
* @return mixed
*/
function encodeHTML(/* , $varName1, $varName2, ... */)
public function encodeHTML(/* , $varName1, $varName2, ... */)
{
$varNames = func_get_args();
if(count($varNames) < 0)
@ -109,7 +109,7 @@ class Security
* @param array $name
* @return mixed
*/
function _encodeHTML($var, $name = array())
protected function _encodeHTML($var, $name = array())
{
if(is_string($var))
{
@ -183,46 +183,9 @@ class Security
* @param string $xml
* @return bool
*/
static function detectingXEE($xml)
public static function detectingXEE($xml)
{
if(!$xml) return FALSE;
if(strpos($xml, '<!ENTITY') !== FALSE)
{
return TRUE;
}
// Strip XML declaration.
$header = preg_replace('/<\?xml.*?\?'.'>/s', '', substr($xml, 0, 100), 1);
$xml = trim(substr_replace($xml, $header, 0, 100));
if($xml == '')
{
return TRUE;
}
// Strip DTD.
$header = preg_replace('/^<!DOCTYPE[^>]*+>/i', '', substr($xml, 0, 200), 1);
$xml = trim(substr_replace($xml, $header, 0, 200));
if($xml == '')
{
return TRUE;
}
// Confirm the XML now starts with a valid root tag. A root tag can end in [> \t\r\n]
$root_tag = substr($xml, 0, strcspn(substr($xml, 0, 20), "> \t\r\n"));
// Reject a second DTD.
if(strtoupper($root_tag) == '<!DOCTYPE')
{
return TRUE;
}
if(!in_array($root_tag, array('<methodCall', '<methodResponse', '<fault')))
{
return TRUE;
}
return FALSE;
return !Rhymix\Framework\Security::checkXEE($xml);
}
}
/* End of file : Security.class.php */

View file

@ -3,40 +3,9 @@
class UploadFileFilter
{
private static $_block_list = array ('exec', 'system', 'passthru', 'show_source', 'phpinfo', 'fopen', 'file_get_contents', 'file_put_contents', 'fwrite', 'proc_open', 'popen');
public function check($file)
{
// TODO: 기능개선후 enable
return TRUE; // disable
if (! $file || ! FileHandler::exists($file)) return TRUE;
return self::_check ( $file );
}
private function _check($file)
{
if (! ($fp = fopen ( $file, 'r' ))) return FALSE;
$has_php_tag = FALSE;
while ( ! feof ( $fp ) )
{
$content = fread ( $fp, 8192 );
if (FALSE === $has_php_tag) $has_php_tag = strpos ( $content, '<?' );
foreach ( self::$_block_list as $v )
{
if (FALSE !== $has_php_tag && FALSE !== strpos ( $content, $v ))
{
fclose ( $fp );
return FALSE;
}
}
}
fclose ( $fp );
return TRUE;
return true;
}
}