Skip to content

Commit

Permalink
파일 업로드 시 위장한 파일 형식을 이용한 보안 문제 수정
Browse files Browse the repository at this point in the history
  • Loading branch information
dorami committed Oct 13, 2018
1 parent 300e162 commit c7f7934
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 84 deletions.
33 changes: 17 additions & 16 deletions classes/context/Context.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -1156,15 +1156,12 @@ function _filterRequestVar($key, $val, $do_stripslashes = true, $remove_hack = f
if($key === 'page' || $key === 'cpage' || substr_compare($key, 'srl', -3) === 0){
$result[$k] = !preg_match('/^[0-9,]+$/', $v) ? (int) $v : $v;
}
elseif($key === 'mid' || $key === 'search_keyword'){
elseif(in_array($key, array('mid','search_keyword','search_target','xe_validator_id'))){
$result[$k] = escape($v, false);
}
elseif($key === 'vid'){
$result[$k] = urlencode($v);
}
elseif($key === 'xe_validator_id'){
$result[$k] = escape($v, false);
}
elseif(stripos($key, 'XE_VALIDATOR', 0) === 0){
unset($result[$k]);
}
Expand Down Expand Up @@ -1218,7 +1215,7 @@ function _setUploadedArgument(){
foreach($_FILES as $key => $val){
$tmp_name = $val['tmp_name'];
if(!is_array($tmp_name)){
if(!$tmp_name || !is_uploaded_file($tmp_name)){
if(!UploadFileFilter::check($tmp_name, $val['name'])){
continue;
}
$val['name'] = htmlspecialchars($val['name'], ENT_COMPAT | ENT_HTML401, 'UTF-8', FALSE);
Expand All @@ -1227,19 +1224,23 @@ function _setUploadedArgument(){
}
else{
$files = array();
$count_files = count($tmp_name);
for($i = 0; $i < $count_files; $i++){
if($val['size'][$i] > 0){
$file = array();
$file['name'] = $val['name'][$i];
$file['type'] = $val['type'][$i];
$file['tmp_name'] = $val['tmp_name'][$i];
$file['error'] = $val['error'][$i];
$file['size'] = $val['size'][$i];
$files[] = $file;
foreach ($tmp_name as $i => $j){
if(!UploadFileFilter::check($val['tmp_name'][$i], $val['name'][$i])){
$files = array();
unset($_FILES[$key]);
break;
}
$file = array();
$file['name'] = $val['name'][$i];
$file['type'] = $val['type'][$i];
$file['tmp_name'] = $val['tmp_name'][$i];
$file['error'] = $val['error'][$i];
$file['size'] = $val['size'][$i];
$files[] = $file;
}
if(count($files)){
self::set($key, $files, true);
}
if($files) $this->set($key, $files, TRUE);
}
}
}
Expand Down
156 changes: 126 additions & 30 deletions classes/security/UploadFileFilter.class.php
Original file line number Diff line number Diff line change
@@ -1,40 +1,136 @@
<?php
/* Copyright (C) NAVER <http://www.navercorp.com> */
/**
* @copyright Rhymix Developers and Contributors
* @link https://github.com/rhymix/rhymix
*/
class UploadFileFilter {
/**
* Generic checker
*
* @param string $file
* @param string $filename
* @return bool
*/
public static function check($file, $filename = null){
// Return error if the file is not uploaded.
if(!$file || !file_exists($file) || !is_uploaded_file($file)){
return false;
}

/* Copyright (C) DAOL Project <http://www.daolcms.org> */
// Return error if the file size is zero.
if(($filesize = filesize($file)) == 0){
return false;
}

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);
// Get the extension.
$ext = $filename ? strtolower(substr(strrchr($filename, '.'), 1)) : '';

// Check the first 4KB of the file for possible XML content.
$fp = fopen($file, 'rb');
$first4kb = fread($fp, 4096);
$is_xml = preg_match('/<(?:\?xml|!DOCTYPE|html|head|body|meta|script|svg)\b/i', $first4kb);

// Check SVG files.
if(($ext === 'svg' || $is_xml) && !self::_checkSVG($fp, 0, $filesize)){
fclose($fp);
return false;
}

// Check XML files.
if(($ext === 'xml' || $is_xml) && !self::_checkXML($fp, 0, $filesize)){
fclose($fp);
return false;
}

// Check HTML files.
if(($ext === 'html' || $ext === 'shtml' || $ext === 'xhtml' || $ext === 'phtml' || $is_xml) && !self::_checkHTML($fp, 0, $filesize)){
fclose($fp);
return false;
}

// Return true if everything is OK.
fclose($fp);
return true;
}

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;
}

/**
* Check SVG file for XSS or SSRF vulnerabilities (#1088, #1089)
*
* @param resource $fp
* @param int $from
* @param int $to
* @return bool
*/
protected static function _checkSVG($fp, $from, $to){
if(self::_matchStream('/<script|<handler\b|xlink:href\s*=\s*"(?!data:)/i', $fp, $from, $to)){
return false;
}
if(self::_matchStream('/\b(?:ev:(?:event|listener|observer)|on[a-z]+)\s*=/i', $fp, $from, $to)) {
return false;
}

return true;
}

/**
* Check XML file for external entity inclusion.
*
* @param resource $fp
* @param int $from
* @param int $to
* @return bool
*/
protected static function _checkXML($fp, $from, $to){
if(self::_matchStream('/<!ENTITY/i', $fp, $from, $to)){
return false;
}

return true;
}

/**
* Check HTML file for PHP code, server-side includes, and other nastiness.
*
* @param resource $fp
* @param int $from
* @param int $to
* @return bool
*/
protected static function _checkHTML($fp, $from, $to){
if(self::_matchStream('/<\?(?!xml\b)|<!--#(?:include|exec|echo|config|fsize|flastmod|printenv)\b/i', $fp, $from, $to)){
return false;
}

return true;
}

/**
* Match a stream against a regular expression.
*
* This method is useful when dealing with large files,
* because we don't need to load the entire file into memory.
* We allow a generous overlap in case the matching string
* occurs across a block boundary.
*
* @param string $regexp
* @param resource $fp
* @param int $from
* @param int $to
* @param int $block_size (optional)
* @param int $overlap_size (optional)
* @return bool
*/
protected static function _matchStream($regexp, $fp, $from, $to, $block_size = 16384, $overlap_size = 1024){
fseek($fp, $position = $from);
while (strlen($content = fread($fp, $block_size + $overlap_size)) > 0){
if(preg_match($regexp, $content))
{
return true;
}
fseek($fp, min($to, $position += $block_size));
}

fclose($fp);

return TRUE;
return false;
}
}

/* End of file : UploadFileFilter.class.php */
/* Location: ./classes/security/UploadFileFilter.class.php */
91 changes: 64 additions & 27 deletions classes/template/TemplateHandler.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,25 @@
**/
class TemplateHandler {

var $compiled_path = './files/cache/template_compiled/'; ///< path of compiled caches files

var $path = null; ///< target directory
var $filename = null; ///< target filename
var $file = null; ///< target file (fullpath)
var $xe_path = null; ///< XpressEngine base path
var $web_path = null; ///< tpl file web path
var $compiled_file = null; ///< tpl file web path
var $skipTags = null;

var $handler_mtime = 0;
private $compiled_path = './files/cache/template_compiled/'; ///< path of compiled caches files
private $path = NULL; ///< target directory
private $filename = NULL; ///< target filename
private $file = NULL; ///< target file (fullpath)
private $xe_path = NULL; ///< XpressEngine base path
private $web_path = NULL; ///< tpl file web path
private $compiled_file = NULL; ///< tpl file web path
private $config = NULL;
private $skipTags = NULL;
private $handler_mtime = 0;

/**
* constructor
* @return void
**/
function TemplateHandler() {
// TODO: replace this with static variable in PHP5
global $__templatehandler_root_tpl;

$__templatehandler_root_tpl = null;

ini_set('pcre.jit', "0");
$this->xe_path = rtrim(getScriptPath(), '/');
public function __construct(){
$this->xe_path = rtrim(preg_replace('/([^\.^\/]+)\.php$/i', '', $_SERVER['SCRIPT_NAME']), '/');
$this->compiled_path = _DAOL_PATH_ . $this->compiled_path;
$this->config = new stdClass();
}

/**
Expand Down Expand Up @@ -185,6 +180,13 @@ function parse($buff = null) {
$this->skipTags = array('marquee');
}

// reset config for this buffer (this step is necessary because we use a singleton for every template)
$previous_config = clone $this->config;
$this->config = new stdClass();

// detect existence of autoescape config
$this->config->autoescape = (strpos($buff, ' autoescape="') === FALSE) ? NULL : 'off';

// replace comments
$buff = preg_replace('@<!--//.*?-->@s', '', $buff);

Expand All @@ -195,7 +197,7 @@ function parse($buff = null) {
$buff = $this->_parseInline($buff);

// include, unload/load, import
$buff = preg_replace_callback('/{(@[\s\S]+?|(?=\$\w+|_{1,2}[A-Z]+|[!\(+-]|\w+(?:\(|::)|\d+|[\'"].*?[\'"]).+?)}|<(!--[#%])?(include|import|(un)?load(?(4)|(?:_js_plugin)?))(?(2)\(["\']([^"\']+)["\'])(.*?)(?(2)\)--|\/)>|<!--(@[a-z@]*)([\s\S]*?)-->(\s*)/', array($this, '_parseResource'), $buff);
$buff = preg_replace_callback('/{(@[\s\S]+?|(?=\$\w+|_{1,2}[A-Z]+|[!\(+-]|\w+(?:\(|::)|\d+|[\'"].*?[\'"]).+?)}|<(!--[#%])?(include|import|(un)?load(?(4)|(?:_js_plugin)?)|config)(?(2)\(["\']([^"\']+)["\'])(.*?)(?(2)\)--|\/)>|<!--(@[a-z@]*)([\s\S]*?)-->(\s*)/', array($this, '_parseResource'), $buff);

// remove block which is a virtual tag
$buff = preg_replace('@</?block\s*>@is', '', $buff);
Expand All @@ -212,6 +214,9 @@ function parse($buff = null) {
// remove php script reopening
$buff = preg_replace(array('/(\n|\r\n)+/', '/(;)?( )*\?\>\<\?php([\n\t ]+)?/'), array("\n", ";\n"), $buff);

// restore config to previous value
$this->config = $previous_config;

return $buff;
}

Expand Down Expand Up @@ -441,15 +446,36 @@ function _parseInline($buff) {
**/
function _parseResource($m) {
// {@ ... } or {$var} or {func(...)}
if($m[1]) {
if(preg_match('@^(\w+)\(@', $m[1], $mm) && !function_exists($mm[1])) return $m[0];
if($m[1]){
if(preg_match('@^(\w+)\(@', $m[1], $mm) && !function_exists($mm[1])){
return $m[0];
}

$echo = 'echo ';
if($m[1]{0} == '@') {
$echo = '';
$m[1] = substr($m[1], 1);
if($m[1]{0} == '@'){
$m[1] = $this->_replaceVar(substr($m[1], 1));
return "<?php {$m[1]} ?>";
}
else{
$escape_option = $this->config->autoescape !== null ? 'auto' : 'noescape';
if(preg_match('@^(.+)\\|((?:auto|no)?escape)$@', $m[1], $mm)){
$m[1] = $mm[1];
$escape_option = $mm[2];
}
elseif($m[1] === '$content' && preg_match('@/layouts/.+/layout\.html$@', $this->file)){
$escape_option = 'noescape';
}
$m[1] = $this->_replaceVar($m[1]);
switch($escape_option){
case 'auto':
return "<?php echo (\$this->config->autoescape === 'on' ? htmlspecialchars({$m[1]}, ENT_COMPAT, 'UTF-8', false) : {$m[1]}) ?>";
case 'autoescape':
return "<?php echo htmlspecialchars({$m[1]}, ENT_COMPAT, 'UTF-8', false) ?>";
case 'escape':
return "<?php echo htmlspecialchars({$m[1]}, ENT_COMPAT, 'UTF-8', true) ?>";
case 'noescape':
return "<?php echo {$m[1]} ?>";
}
}
return '<?php ' . $echo . $this->_replaceVar($m[1]) . ' ?>';
}

if($m[3]) {
Expand Down Expand Up @@ -541,6 +567,17 @@ function _parseResource($m) {
if($metafile) $result = "<!--#Meta:{$metafile}-->" . $result;

return $result;
// <config ...>
case 'config':
$result = '';
if(preg_match_all('@ (\w+)="([^"]+)"@', $m[6], $config_matches, PREG_SET_ORDER))
{
foreach($config_matches as $config_match)
{
$result .= "\$this->config->{$config_match[1]} = '" . trim(strtolower($config_match[2])) . "';";
}
}
return "<?php {$result} ?>";
}
}

Expand Down
2 changes: 1 addition & 1 deletion modules/admin/admin.admin.controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ function cleanFavorite() {
*/
function procAdminUpdateConfig() {
$adminTitle = Context::get('adminTitle');
$file = $_FILES['adminLogo'];
$file = Context::get('adminLogo');

$oModuleModel = &getModel('module');
$oAdminConfig = $oModuleModel->getModuleConfig('admin');
Expand Down
2 changes: 1 addition & 1 deletion modules/file/file.controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function init() {
**/
function procFileUpload() {
Context::setRequestMethod('JSON');
$file_info = $_FILES['Filedata'];
$file_info = Context::get('Filedata');

// An error appears if not a normally uploaded file
if(!is_uploaded_file($file_info['tmp_name'])) exit();
Expand Down
Loading

0 comments on commit c7f7934

Please sign in to comment.