Merge branch 'security/rve-2026-7'

This commit is contained in:
Kijin Sung 2026-05-11 18:17:22 +09:00
commit aa0f3f0200
9 changed files with 85 additions and 21 deletions

View file

@ -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/', '', <<<END
<IfModule authz_core_module>
Require all denied
</IfModule>
<IfModule !authz_core_module>
Order deny,allow
Deny from all
</IfModule>
END));
if (!$result)
{
return false;
}
return true;
}
/**
* Get the current umask.
*

View file

@ -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');

View file

@ -338,8 +338,9 @@ $lang->mobile_viewport = 'Mobile viewport Setting';
$lang->about_mobile_viewport = 'The settings entered above will be output in a &lt;meta name=&quot;viewport&quot;&gt; 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.';

View file

@ -334,8 +334,9 @@ $lang->mobile_viewport = '모바일 viewport 설정';
$lang->about_mobile_viewport = '모바일 접속시 여기 입력한 내용이 &lt;meta name=&quot;viewport&quot;&gt; 태그로 출력됩니다.';
$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 = '모든 페이지의 최상단에 코드를 삽입합니다. 관리자 화면에는 적용되지 않습니다.';

View file

@ -126,18 +126,19 @@
<div class="x_control-group">
<label class="x_control-label">{$lang->thumbnail_target}</label>
<div class="x_controls">
<label for="thumbnail_target_all" class="x_inline">
<input type="radio" name="thumbnail_target" id="thumbnail_target_all" value="all" checked="checked"|cond="$thumbnail_target == 'all' || !$thumbnail_target" />
{$lang->thumbnail_target_all}
</label>
<label for="thumbnail_target_attachment" class="x_inline">
<input type="radio" name="thumbnail_target" id="thumbnail_target_attachment" value="attachment" checked="checked"|cond="$thumbnail_target == 'attachment'" />
<input type="radio" name="thumbnail_target" id="thumbnail_target_attachment" value="attachment" checked="checked"|cond="$thumbnail_target == 'attachment' || !$thumbnail_target" />
{$lang->thumbnail_target_attachment}
</label>
<label for="thumbnail_target_content" class="x_inline">
<input type="radio" name="thumbnail_target" id="thumbnail_target_content" value="content" checked="checked"|cond="$thumbnail_target == 'content'" />
{$lang->thumbnail_target_content}
</label>
<label for="thumbnail_target_none" class="x_inline">
<input type="radio" name="thumbnail_target" id="thumbnail_target_none" value="none" checked="checked"|cond="$thumbnail_target == 'none'" />
{$lang->thumbnail_none}
</label>
<p class="x_help-block">{$lang->about_thumbnail_target}</p>
</div>
</div>
<div class="x_control-group">

View file

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

View file

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

View file

@ -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))
{

View file

@ -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';