From e930328e996515246315c78a4ff3dcd45720e8a6 Mon Sep 17 00:00:00 2001
From: nilm <1147659453@qq.com>
Date: Sun, 28 Jun 2026 12:34:19 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=89=E5=85=A8=E5=8A=A0=E5=9B=BA:=20?=
=?UTF-8?q?=E4=B8=BA=20UEditor=20=E4=B8=8A=E4=BC=A0=E5=A2=9E=E5=8A=A0=20MI?=
=?UTF-8?q?ME=20=E6=A0=A1=E9=AA=8C=E3=80=81=E5=86=85=E5=AE=B9=E6=89=AB?=
=?UTF-8?q?=E6=8F=8F=E5=92=8C=20.htaccess=20=E9=98=B2=E6=8A=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Uploader.class.php:
- 新增 checkMimeType() 使用 exif_imagetype() 和 finfo_file() 验证文件真实 MIME 类型
- 新增 checkFileContent() 扫描上传文件中的 PHP 标签和 webshell 模式
- 新增 createHtaccess() 生成 .htaccess 禁止上传目录执行 PHP
- 对 upFile()、upBase64()、saveRemote() 三个入口均应用安全检查
action_upload.php:
- 上传后二次验证扩展名白名单
- 验证保存路径必须在允许的 upload 目录下
- 发现恶意文件立即删除
修复 CWE-434(任意文件上传)和 CWE-22(路径遍历)漏洞
基于 PbootCMS 3.2.15 安全性评估报告
---
core/extend/ueditor/php/Uploader.class.php | 187 ++++++++++++++++++++-
core/extend/ueditor/php/action_upload.php | 57 ++++++-
2 files changed, 237 insertions(+), 7 deletions(-)
diff --git a/core/extend/ueditor/php/Uploader.class.php b/core/extend/ueditor/php/Uploader.class.php
index 5b76d60..406a4db 100644
--- a/core/extend/ueditor/php/Uploader.class.php
+++ b/core/extend/ueditor/php/Uploader.class.php
@@ -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");
@@ -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];
}
/**
@@ -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];
}
/**
@@ -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];
}
/**
@@ -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 = "\n";
+ $htaccessContent .= " Order allow,deny\n";
+ $htaccessContent .= " Deny from all\n";
+ $htaccessContent .= "\n";
+ $htaccessContent .= "\n";
+ $htaccessContent .= " Order allow,deny\n";
+ $htaccessContent .= " Deny from all\n";
+ $htaccessContent .= "\n";
+ $htaccessContent .= "php_flag engine off\n";
+ @file_put_contents($htaccessPath, $htaccessContent);
+ }
+
/**
* 文件大小检测
* @return bool
diff --git a/core/extend/ueditor/php/action_upload.php b/core/extend/ueditor/php/action_upload.php
index a1a5917..395d5cf 100644
--- a/core/extend/ueditor/php/action_upload.php
+++ b/core/extend/ueditor/php/action_upload.php
@@ -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',