diff --git a/common/framework/Storage.php b/common/framework/Storage.php index 6637c5cc0..20ea4e04f 100644 --- a/common/framework/Storage.php +++ b/common/framework/Storage.php @@ -873,6 +873,45 @@ class Storage } } + /** + * Prevent access to a directory by creating .htaccess and index.html files in it. + * + * This is a best-effort measure only, and depends on web server configuration. + * It is recommended to use proper server configuration to protect sensitive directories. + * + * @param string $dirname + * @return bool + */ + public static function protectDirectory(string $dirname): bool + { + $dirname = rtrim($dirname, '/\\'); + if (!self::isDirectory($dirname) || !self::isWritable($dirname)) + { + return false; + } + + $result = self::write($dirname . '/index.html', ''); + if (!$result) + { + return false; + } + $result = self::write($dirname . '/.htaccess', preg_replace('/\\t/', '', << + Require all denied + + + Order deny,allow + Deny from all + + END)); + if (!$result) + { + return false; + } + + return true; + } + /** * Get the current umask. * diff --git a/modules/admin/controllers/systemconfig/Advanced.php b/modules/admin/controllers/systemconfig/Advanced.php index 8622d1599..0f497fb80 100644 --- a/modules/admin/controllers/systemconfig/Advanced.php +++ b/modules/admin/controllers/systemconfig/Advanced.php @@ -76,7 +76,7 @@ class Advanced extends Base // Thumbnail settings $oDocumentModel = getModel('document'); $config = $oDocumentModel->getDocumentConfig(); - Context::set('thumbnail_target', $config->thumbnail_target ?: 'all'); + Context::set('thumbnail_target', $config->thumbnail_target ?: 'attachment'); Context::set('thumbnail_type', $config->thumbnail_type ?: 'fill'); Context::set('thumbnail_quality', $config->thumbnail_quality ?: 75); if ($config->thumbnail_type === 'none') @@ -190,7 +190,7 @@ class Advanced extends Base // Thumbnail settings $oDocumentModel = getModel('document'); $document_config = $oDocumentModel->getDocumentConfig(); - $document_config->thumbnail_target = $vars->thumbnail_target ?: 'all'; + $document_config->thumbnail_target = $vars->thumbnail_target ?: 'attachment'; $document_config->thumbnail_type = $vars->thumbnail_type ?: 'fill'; $document_config->thumbnail_quality = intval($vars->thumbnail_quality) ?: 75; $oModuleController = getController('module'); diff --git a/modules/admin/lang/en.php b/modules/admin/lang/en.php index e62c2c512..5b00752d3 100644 --- a/modules/admin/lang/en.php +++ b/modules/admin/lang/en.php @@ -338,8 +338,9 @@ $lang->mobile_viewport = 'Mobile viewport Setting'; $lang->about_mobile_viewport = 'The settings entered above will be output in a <meta name="viewport"> tag for mobile visitors.'; $lang->restore_default_viewport = 'Restore Default'; $lang->thumbnail_target = 'Extract Thumbnail From'; -$lang->thumbnail_target_all = 'All images'; +$lang->thumbnail_target_content = 'All embedded images, including external images'; $lang->thumbnail_target_attachment = 'Attached images only'; +$lang->about_thumbnail_target = 'Using external images to generate thumbnails may cause various issues with respect to page load speed, copyright, and security. Please be careful.'; $lang->thumbnail_type = 'Thumbnail Type'; $lang->input_header_script = 'Header Script'; $lang->detail_input_header_script = 'Content added here will be printed at the top of every page, except the admin module.'; diff --git a/modules/admin/lang/ko.php b/modules/admin/lang/ko.php index d458a4e22..6ced528eb 100644 --- a/modules/admin/lang/ko.php +++ b/modules/admin/lang/ko.php @@ -334,8 +334,9 @@ $lang->mobile_viewport = '모바일 viewport 설정'; $lang->about_mobile_viewport = '모바일 접속시 여기 입력한 내용이 <meta name="viewport"> 태그로 출력됩니다.'; $lang->restore_default_viewport = '기본값 복원'; $lang->thumbnail_target = '썸네일 생성 대상'; -$lang->thumbnail_target_all = '모든 이미지 (외부 이미지 포함)'; +$lang->thumbnail_target_content = '모든 이미지 (외부 이미지 포함)'; $lang->thumbnail_target_attachment = '첨부된 이미지'; +$lang->about_thumbnail_target = '외부 이미지를 사용하여 썸네일을 생성하는 경우 페이지 로딩 속도, 저작권, 보안 등 다양한 문제가 발생할 수 있으니 주의하시기 바랍니다.'; $lang->thumbnail_type = '썸네일 생성 방식'; $lang->input_header_script = '상단(헤더) 스크립트'; $lang->detail_input_header_script = '모든 페이지의 최상단에 코드를 삽입합니다. 관리자 화면에는 적용되지 않습니다.'; diff --git a/modules/admin/tpl/config_advanced.html b/modules/admin/tpl/config_advanced.html index 7c4540f59..c1c158476 100644 --- a/modules/admin/tpl/config_advanced.html +++ b/modules/admin/tpl/config_advanced.html @@ -126,18 +126,19 @@
- + +

{$lang->about_thumbnail_target}

diff --git a/modules/comment/comment.item.php b/modules/comment/comment.item.php index d4acad199..1af663786 100644 --- a/modules/comment/comment.item.php +++ b/modules/comment/comment.item.php @@ -924,28 +924,34 @@ class CommentItem extends BaseObject $target_src = Context::getRequestUri().$target_src; } - $tmp_file = sprintf('./files/cache/tmp/%d', md5(rand(111111, 999999) . $this->comment_srl)); - if(!is_dir('./files/cache/tmp')) + $tmp_file = sprintf('./files/cache/tmp/%s', Rhymix\Framework\Security::getRandom(32)); + if (!Rhymix\Framework\Storage::exists(\RX_BASEDIR . 'files/cache/tmp')) { - FileHandler::makeDir('./files/cache/tmp'); + Rhymix\Framework\Storage::createDirectory(\RX_BASEDIR . 'files/cache/tmp'); + } + if (!Rhymix\Framework\Storage::exists(\RX_BASEDIR . 'files/cache/tmp/.htaccess')) + { + Rhymix\Framework\Storage::protectDirectory(\RX_BASEDIR . 'files/cache/tmp'); } FileHandler::getRemoteFile($target_src, $tmp_file); - if(!file_exists($tmp_file)) + if (!Rhymix\Framework\Storage::exists($tmp_file)) { continue; } else { - if($is_img = @getimagesize($tmp_file)) + if ($is_img = @getimagesize($tmp_file)) { list($_w, $_h, $_t, $_a) = $is_img; if($_w < ($external_image_min_width) && ($height === 'auto' || $_h < ($external_image_min_height))) { + Rhymix\Framework\Storage::delete($tmp_file); continue; } } else { + Rhymix\Framework\Storage::delete($tmp_file); continue; } $source_file = $tmp_file; diff --git a/modules/document/document.item.php b/modules/document/document.item.php index 7daa75828..38718714d 100644 --- a/modules/document/document.item.php +++ b/modules/document/document.item.php @@ -1257,28 +1257,34 @@ class DocumentItem extends BaseObject $target_src = Context::getRequestUri().$target_src; } - $tmp_file = sprintf('./files/cache/tmp/%d', md5(rand(111111,999999).$this->document_srl)); - if(!is_dir('./files/cache/tmp')) + $tmp_file = sprintf('./files/cache/tmp/%s', Rhymix\Framework\Security::getRandom(32)); + if (!Rhymix\Framework\Storage::exists(\RX_BASEDIR . 'files/cache/tmp')) { - FileHandler::makeDir('./files/cache/tmp'); + Rhymix\Framework\Storage::createDirectory(\RX_BASEDIR . 'files/cache/tmp'); + } + if (!Rhymix\Framework\Storage::exists(\RX_BASEDIR . 'files/cache/tmp/.htaccess')) + { + Rhymix\Framework\Storage::protectDirectory(\RX_BASEDIR . 'files/cache/tmp'); } FileHandler::getRemoteFile($target_src, $tmp_file); - if(!file_exists($tmp_file)) + if (!Rhymix\Framework\Storage::exists($tmp_file)) { continue; } else { - if($is_img = @getimagesize($tmp_file)) + if ($is_img = @getimagesize($tmp_file)) { list($_w, $_h, $_t, $_a) = $is_img; if($_w < ($external_image_min_width) && ($height === 'auto' || $_h < ($external_image_min_height))) { + Rhymix\Framework\Storage::delete($tmp_file); continue; } } else { + Rhymix\Framework\Storage::delete($tmp_file); continue; } $source_file = $tmp_file; diff --git a/modules/document/document.model.php b/modules/document/document.model.php index 920e51083..b181b5c93 100644 --- a/modules/document/document.model.php +++ b/modules/document/document.model.php @@ -1036,9 +1036,9 @@ class DocumentModel extends Document { self::$_config = ModuleModel::getModuleConfig('document') ?: new stdClass; } - if (!isset(self::$_config->thumbnail_target)) + if (!isset(self::$_config->thumbnail_target) || self::$_config->thumbnail_target === 'all') { - self::$_config->thumbnail_target = 'all'; + self::$_config->thumbnail_target = 'attachment'; } if (!isset(self::$_config->thumbnail_type)) { diff --git a/tests/unit/framework/StorageTest.php b/tests/unit/framework/StorageTest.php index 4b17ead0c..7c9e79bc2 100644 --- a/tests/unit/framework/StorageTest.php +++ b/tests/unit/framework/StorageTest.php @@ -346,6 +346,16 @@ class StorageTest extends \Codeception\Test\Unit $this->assertFalse(Rhymix\Framework\Storage::deleteDirectory($nonexistent)); } + public function testProtectDirectory() + { + $dir = \RX_BASEDIR . 'tests/_output/protectdir'; + mkdir($dir); + $this->assertTrue(Rhymix\Framework\Storage::protectDirectory($dir)); + $this->assertTrue(file_exists($dir . '/index.html')); + $this->assertTrue(file_exists($dir . '/.htaccess')); + $this->assertStringContainsString('Require all denied', file_get_contents($dir . '/.htaccess')); + } + public function testDeleteDirectoryKeepRoot() { $sourcedir = \RX_BASEDIR . 'tests/_output/sourcedir';