Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 181 additions & 6 deletions core/extend/ueditor/php/Uploader.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ private function upFile()
return;
}

//安全加固:MIME类型校验
if (!$this->checkMimeType()) {
$this->stateInfo = "文件内容与扩展名不匹配";
return;
}

//创建目录失败
if (!file_exists($dirname) && !mkdir($dirname, 0777, true)) {
$this->stateInfo = $this->getStateInfo("ERROR_CREATE_DIR");
Expand All @@ -120,9 +126,20 @@ private function upFile()
//移动文件
if (!(move_uploaded_file($file["tmp_name"], $this->filePath) && file_exists($this->filePath))) { //移动失败
$this->stateInfo = $this->getStateInfo("ERROR_FILE_MOVE");
} else { //移动成功
$this->stateInfo = $this->stateMap[0];
return;
}

//安全加固:上传后检查文件内容是否包含恶意代码
if (!$this->checkFileContent()) {
@unlink($this->filePath);
$this->stateInfo = "文件内容包含非法代码";
return;
}

//安全加固:创建.htaccess防止上传目录执行PHP
$this->createHtaccess();

$this->stateInfo = $this->stateMap[0];
}

/**
Expand Down Expand Up @@ -160,10 +177,20 @@ private function upBase64()
//移动文件
if (!(file_put_contents($this->filePath, $img) && file_exists($this->filePath))) { //移动失败
$this->stateInfo = $this->getStateInfo("ERROR_WRITE_CONTENT");
} else { //移动成功
$this->stateInfo = $this->stateMap[0];
return;
}

//安全加固:检查文件内容
if (!$this->checkFileContent()) {
@unlink($this->filePath);
$this->stateInfo = "文件内容包含非法代码";
return;
}

//安全加固:创建.htaccess
$this->createHtaccess();

$this->stateInfo = $this->stateMap[0];
}

/**
Expand Down Expand Up @@ -252,10 +279,20 @@ private function saveRemote()
//移动文件
if (!(file_put_contents($this->filePath, $img) && file_exists($this->filePath))) { //移动失败
$this->stateInfo = $this->getStateInfo("ERROR_WRITE_CONTENT");
} else { //移动成功
$this->stateInfo = $this->stateMap[0];
return;
}

//安全加固:检查文件内容
if (!$this->checkFileContent()) {
@unlink($this->filePath);
$this->stateInfo = "文件内容包含非法代码";
return;
}

//安全加固:创建.htaccess
$this->createHtaccess();

$this->stateInfo = $this->stateMap[0];
}

/**
Expand Down Expand Up @@ -344,6 +381,144 @@ private function checkType()
return in_array($this->getFileExt(), $this->config["allowFiles"]);
}

/**
* MIME类型校验(安全加固)
* 验证文件的真实MIME类型与声明的扩展名是否匹配
* @return bool
*/
private function checkMimeType()
{
$ext = $this->getFileExt();
// 扩展名到MIME类型的映射
$mimeMap = array(
'.jpg' => array('image/jpeg', 'image/jpg', 'image/pjpeg'),
'.jpeg' => array('image/jpeg', 'image/jpg', 'image/pjpeg'),
'.png' => array('image/png', 'image/x-png'),
'.gif' => array('image/gif'),
'.bmp' => array('image/bmp', 'image/x-ms-bmp'),
'.ico' => array('image/x-icon', 'image/vnd.microsoft.icon'),
'.svg' => array('image/svg+xml'),
'.mp4' => array('video/mp4'),
'.mp3' => array('audio/mpeg'),
'.webm' => array('video/webm'),
'.pdf' => array('application/pdf'),
'.doc' => array('application/msword'),
'.docx' => array('application/vnd.openxmlformats-officedocument.wordprocessingml.document'),
'.xls' => array('application/vnd.ms-excel'),
'.xlsx' => array('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'),
'.ppt' => array('application/vnd.ms-powerpoint'),
'.pptx' => array('application/vnd.openxmlformats-officedocument.presentationml.presentation'),
'.zip' => array('application/zip', 'application/x-zip-compressed'),
'.rar' => array('application/x-rar-compressed', 'application/vnd.rar'),
'.txt' => array('text/plain'),
'.xml' => array('text/xml', 'application/xml'),
'.flv' => array('video/x-flv'),
'.avi' => array('video/x-msvideo'),
);

// 对于图片类型,使用更严格的图像类型检测
if (in_array($ext, array('.jpg', '.jpeg', '.png', '.gif', '.bmp'))) {
if (function_exists('exif_imagetype')) {
$imgType = @exif_imagetype($this->file['tmp_name']);
$expectedTypes = array(
'.jpg' => array(IMAGETYPE_JPEG),
'.jpeg' => array(IMAGETYPE_JPEG),
'.png' => array(IMAGETYPE_PNG),
'.gif' => array(IMAGETYPE_GIF),
'.bmp' => array(IMAGETYPE_BMP),
);
if (!isset($expectedTypes[$ext]) || !in_array($imgType, $expectedTypes[$ext])) {
return false;
}
}
}

// 通用MIME类型检查
if (isset($mimeMap[$ext]) && function_exists('finfo_file')) {
$finfo = @finfo_open(FILEINFO_MIME_TYPE);
if ($finfo) {
$realMime = @finfo_file($finfo, $this->file['tmp_name']);
finfo_close($finfo);
if ($realMime && !in_array($realMime, $mimeMap[$ext])) {
return false;
}
}
}

return true;
}

/**
* 检查上传文件内容是否包含恶意代码(安全加固)
* @return bool
*/
private function checkFileContent()
{
$ext = $this->getFileExt();
// 只检查可能被伪装的可执行文件类型(图片、文本等)
if (!in_array($ext, array('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.txt', '.xml'))) {
return true; // 其他类型不检查内容
}

$content = @file_get_contents($this->filePath);
if ($content === false) {
return true;
}

// 检测PHP标签
$phpPatterns = array(
'/<\?php/i',
'/<\?\s+/i',
'/<\?=\s*/i',
'/<\?$/m',
);
foreach ($phpPatterns as $pattern) {
if (preg_match($pattern, $content)) {
return false;
}
}

// 检测常见的 webshell 函数调用
$dangerousPatterns = array(
'/eval\s*\(/i',
'/assert\s*\(/i',
'/system\s*\(/i',
'/exec\s*\(/i',
'/passthru\s*\(/i',
'/shell_exec\s*\(/i',
'/base64_decode\s*\(/i',
'/file_put_contents\s*\(/i',
);
foreach ($dangerousPatterns as $pattern) {
if (preg_match($pattern, $content)) {
return false;
}
}

return true;
}

/**
* 创建 .htaccess 文件防止上传目录执行 PHP(安全加固)
*/
private function createHtaccess()
{
$htaccessPath = dirname($this->filePath) . '/.htaccess';
if (file_exists($htaccessPath)) {
return;
}
$htaccessContent = "<FilesMatch \"\\.(?i:php)$\">\n";
$htaccessContent .= " Order allow,deny\n";
$htaccessContent .= " Deny from all\n";
$htaccessContent .= "</FilesMatch>\n";
$htaccessContent .= "<FilesMatch \"\\.(?i:php\\d*)$\">\n";
$htaccessContent .= " Order allow,deny\n";
$htaccessContent .= " Deny from all\n";
$htaccessContent .= "</FilesMatch>\n";
$htaccessContent .= "php_flag engine off\n";
@file_put_contents($htaccessPath, $htaccessContent);
}

/**
* 文件大小检测
* @return bool
Expand Down
57 changes: 56 additions & 1 deletion core/extend/ueditor/php/action_upload.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,63 @@
/* 生成上传实例对象并完成上传 */
$up = new Uploader($fieldName, $config, $base64);

// 图片打水印
// 获取上传结果
$rs = $up->getFileInfo();

// 安全加固:如果上传失败,直接返回错误
if ($rs['state'] !== 'SUCCESS') {
return json_encode($rs);
}

// 安全加固:验证最终文件的扩展名是否合法(防止大小写绕过、双扩展名等)
$uploadedExt = strtolower(pathinfo($rs['url'], PATHINFO_EXTENSION));
$allowedExts = array('jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'svg',
'mp4', 'mp3', 'webm', 'avi', 'flv', 'swf', 'mkv', 'rm', 'rmvb', 'mpeg', 'mpg', 'ogg', 'ogv', 'mov', 'wmv',
'rar', 'zip', 'tar', 'gz', '7z', 'bz2', 'cab', 'iso',
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf', 'txt', 'md', 'xml',
'wav', 'mid');
if (!in_array($uploadedExt, $allowedExts)) {
@unlink(ROOT_PATH . $rs['url']);
return json_encode(array(
'state' => '上传失败:文件扩展名不合法',
'url' => '',
'title' => '',
'original' => '',
'type' => '',
'size' => 0
));
}

// 安全加固:检查上传路径是否在允许的目录下
$uploadDir = realpath(dirname(ROOT_PATH . $rs['url']));
$allowedDirs = array(
realpath(ROOT_PATH . '/upload/image'),
realpath(ROOT_PATH . '/upload/video'),
realpath(ROOT_PATH . '/upload/file'),
realpath(ROOT_PATH . STATIC_DIR . '/upload/image'),
realpath(ROOT_PATH . STATIC_DIR . '/upload/video'),
realpath(ROOT_PATH . STATIC_DIR . '/upload/file'),
);
$pathValid = false;
foreach ($allowedDirs as $allowedDir) {
if ($allowedDir !== false && strpos($uploadDir, $allowedDir) === 0) {
$pathValid = true;
break;
}
}
if (!$pathValid) {
@unlink(ROOT_PATH . $rs['url']);
return json_encode(array(
'state' => '上传失败:非法的保存路径',
'url' => '',
'title' => '',
'original' => '',
'type' => '',
'size' => 0
));
}

// 图片打水印
$ext = array(
'.jpg',
'.png',
Expand Down