织梦uploadsafe.inc.php上传漏洞5层加固方案
DedeCMS的uploadsafe.inc.php是负责文件上传安全校验的核心文件。2010年首次披露漏洞之后官方修过几次,但2015、2018、2022年又陆续被研究者发现新的绕过方式。如果你还在维护DedeCMS 5.7 SP2或更早版本的站点,本文给出一套防御性加固方案——基于2026年公开披露过的所有变体漏洞做综合防护,把单一图片MIME校验扩展成多层防御。保哥过去4年处理过6个被这类漏洞利用过的DedeCMS站点,本文记录的是真实修复路径,不涉及攻击细节。同一批织梦后台上传漏洞还有 media_add.php任意上传漏洞的白名单加固方案 可以一起部署,覆盖面更广。
覆盖范围:DedeCMS 5.7 SP2 / 5.7 UTF8 SP1 / 5.8社区维护版。前缀按官方默认dede_,目录路径以 /www/wwwroot/yoursite.com 为示例。修复前必须备份完整源代码和数据库。
uploadsafe.inc.php在文件上传链中的位置
先理清楚DedeCMS文件上传的完整流程,才能知道为什么修这个文件能起到加固作用:
- 用户提交文件上传请求(前端表单或者编辑器调用)。
- 请求进入DedeCMS的入口PHP文件(如album_add.php、image_add.php、media_main.php等多个上传入口)。
- 这些入口都会require_once include/uploadsafe.inc.php做文件名和类型的安全过滤。
- uploadsafe.inc.php通过后才走真正的文件保存逻辑(move_uploaded_file)到uploads目录。
uploadsafe.inc.php是上传链的安全闸口。它做得好,所有上传入口都受益;它做得不严,攻击者可以从任一上传入口绕进来。所以加固这个文件是覆盖面最广的防御措施。
原版逻辑的三个明显缺陷
DedeCMS 5.7 SP2原版的include/uploadsafe.inc.php大致逻辑(已脱敏简化):
- 读取上传文件的 $_FILES 数组。
- 检查文件扩展名是否在白名单(jpg/png/gif/bmp/zip/rar/doc等)。
- 检查文件大小是否超限。
- 检查文件名是否含恶意字符。
- 通过则放行。
这套逻辑的核心问题:
- 仅看扩展名不看MIME:攻击者可以把PHP文件改名为evil.jpg.php或evil.php.jpg绕过简单扩展名检测。客户端声明的 $_FILES['type'] 完全可以伪造,不能作为可信源。
- 没有内容验证:即使扩展名是.jpg,文件内容可能是PHP代码——服务器解析时如果.jpg后缀触发了PHP执行(如Apache的AddHandler配置错误),漏洞被利用。GIF89a开头后追加PHP代码的伪图是经典构造。
- 对multipart/form-data边界处理不严:早期PHP版本对畸形multipart数据的解析有缝隙,攻击者构造特殊边界让PHP把恶意段当作文件名段处理。
MIME类型与扩展名一致性校验
第一层防御是检查PHP探测到的MIME类型与扩展名是否一致。在uploadsafe.inc.php大约第42行(不同SP版本行号略有差异,用关键词搜索定位)找到扩展名检查的代码段,在其后追加:
// MIME 与扩展名一致性校验
$allowedMimeMap = [
'jpg' => ['image/jpeg', 'image/pjpeg'],
'jpeg' => ['image/jpeg', 'image/pjpeg'],
'png' => ['image/png', 'image/x-png'],
'gif' => ['image/gif'],
'bmp' => ['image/bmp', 'image/x-ms-bmp'],
'webp' => ['image/webp'],
];
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (isset($allowedMimeMap[$ext])) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$actualMime = finfo_file($finfo, $tmpFile);
finfo_close($finfo);
if (!in_array($actualMime, $allowedMimeMap[$ext], true)) {
ShowMsg('文件类型与扩展名不匹配,禁止上传', '-1');
exit;
}
}
关键细节:
- 用finfo扩展检测文件真实MIME,不要用 $_FILES['file']['type']——后者是客户端声明的可以伪造。finfo读取的是文件头部的magic number,攻击者无法在不破坏文件可读性的前提下篡改。
- 用in_array的严格模式(第三个参数true)避免类型转换攻击。PHP的弱比较会让 '0' 和 0 相等,严格模式才安全。
- 白名单包含的别名(image/pjpeg是Internet Explorer历史版本上传JPEG时的MIME,必须接受否则误伤)。
getimagesize二次验证图片真伪
MIME类型可以通过文件头几个字节伪造(攻击者把PHP代码放在伪造的图片头之后)。第二层防御是用PHP的getimagesize函数验证文件确实是合法图片。在大约第53行追加:
// getimagesize 返回值验证
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'], true)) {
$imgInfo = @getimagesize($tmpFile);
if ($imgInfo === false || !isset($imgInfo[2])) {
ShowMsg('图片格式损坏或文件非图片,禁止上传', '-1');
exit;
}
$expectedType = [
'jpg' => IMAGETYPE_JPEG,
'jpeg' => IMAGETYPE_JPEG,
'png' => IMAGETYPE_PNG,
'gif' => IMAGETYPE_GIF,
'bmp' => IMAGETYPE_BMP,
'webp' => IMAGETYPE_WEBP,
];
if ($imgInfo[2] !== $expectedType[$ext]) {
ShowMsg('图片真实格式与扩展名不符,禁止上传', '-1');
exit;
}
}
getimagesize会真的去解析图片的尺寸和格式头部。如果文件不是合法图片或者头部被恶意拼接破坏,函数返回false。这是PHP内置的图片验证函数,对绝大多数攻击载荷都能识别。它的判断基于图片的二进制结构而非扩展名或客户端声明,攻击者要绕过必须构造一个既能让getimagesize返回真值又能携带PHP代码的复合文件,难度大幅提高。
文件名规范化与强制重命名
第三层防御针对的是文件名层面的绕过。比如 .php.jpg、.jpg.php、.shtml、.phtml 这类容易被服务器误解析的扩展名。在前两层校验之后追加:
// 文件名规范化
$dangerousExts = ['php', 'php3', 'php4', 'php5', 'php7', 'phtml',
'phar', 'pl', 'py', 'jsp', 'asp', 'aspx',
'sh', 'cgi', 'shtml', 'htaccess'];
$nameParts = explode('.', strtolower($filename));
foreach ($nameParts as $part) {
if (in_array($part, $dangerousExts, true)) {
ShowMsg('文件名包含危险扩展,禁止上传', '-1');
exit;
}
}
// 强制重命名为时间戳 + 随机字符串
$newName = date('YmdHis') . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
$filename = $newName;
核心思路:
- 把文件名按 . 拆分,检查每一段都不是危险扩展。这能拦下evil.php.jpg、evil.jpg.php等多扩展构造。
- 强制重命名为时间戳加随机串。这样即使有上传文件,攻击者也不知道最终URL是什么——降低了利用难度。这是OWASP A05配置错误条目下专门提到的对抗手法。
- 用random_bytes而不是mt_rand之类弱随机源,避免攻击者通过种子推测文件名。random_bytes是PHP 7.0+引入的密码学安全随机源,跟 /dev/urandom 等价。
Nginx/Apache层禁止uploads目录PHP解析
前三层是PHP代码层面的防御。第四层是服务器配置层面的兜底——即使有恶意文件被上传到uploads目录,也不让服务器作为PHP执行它。
在Nginx配置文件里server块内增加:
location ^~ /uploads/ {
# uploads 目录禁止任何动态脚本执行
location ~ \.(php|php3|php4|php5|php7|phtml|phar|pl|py|jsp|asp|aspx|sh|cgi|shtml)$ {
deny all;
return 403;
}
}
Apache用户在uploads目录下放一个.htaccess文件:
<FilesMatch "\.(php|php3|php4|php5|php7|phtml|phar|pl|py|jsp|asp|aspx|sh|cgi|shtml)$">
Require all denied
</FilesMatch>
这层防御的逻辑:
- 即使前三层防御被绕过,恶意文件落到uploads目录。
- 攻击者访问/uploads/xxx.php时,Web服务器直接返回403而不是调用PHP解析器执行。
- 恶意代码无法运行,攻击就失败了。
这是纵深防御理念的体现——单层防御不够稳,多层叠加才能在某层被绕过时依然保护住业务。配置后必须 nginx -t 验证语法、nginx -s reload 平滑生效,不要直接 service nginx restart 影响线上请求。
可疑内容特征扫描(按需启用)
对安全敏感的站点(电商、政企、金融),还可以在前面四层之上加第五层——上传后用内容扫描工具检测文件中是否含可疑PHP函数特征。在uploadsafe.inc.php通过所有校验后、move_uploaded_file之前插入:
// 可疑内容扫描
$content = file_get_contents($tmpFile);
$suspiciousPatterns = [
'eval(', 'assert(', 'base64_decode(',
'gzinflate(', 'str_rot13(', 'passthru(',
'system(', 'shell_exec(', 'proc_open(',
'$_POST[', '$_GET[', '$_REQUEST['
];
foreach ($suspiciousPatterns as $pat) {
if (stripos($content, $pat) !== false) {
ShowMsg('文件内容含可疑代码特征,禁止上传', '-1');
exit;
}
}
注意这层会有误伤——某些合法PSD文件、Word文档的二进制内容偶尔会包含 $_POST 这种字节序列。如果你的站点上传需求多样,这层用stripos简单匹配会拦住正常文件。可以改成检查文件类型是图片时才扫描,或者用更精细的yara规则。商业级方案直接接ClamAV或类似引擎,扫描精度比简单字符串匹配高一个量级。
验证修复是否生效的四组测试
修完上传一些测试样本验证:
- 正常图片样本:上传一张正常的.jpg图片。预期:成功,文件被重命名为时间戳格式,落到uploads目录。
- 双扩展名样本:上传一个改了扩展名的PHP文件(test.php改成test.jpg)。预期:被第一层MIME校验拦下,提示文件类型与扩展名不匹配。
- EXIF注入样本:上传一个尾部嵌入PHP代码的伪图片(合法图片头部 + 末尾PHP代码)。预期:被第五层内容扫描拦下(如果开启了第五层);或者文件被强制改名,攻击者无法通过URL触发执行。
- uploads直访样本:直接访问/uploads/xxx.php测试服务器层禁解析。预期:返回403。
每个测试都要在生产环境模拟做一遍,不要只在开发环境验证——服务器配置经常不一致。生产环境Nginx可能有CDN回源、可能开启了不同的fastcgi_pass,开发环境的"通过"不代表生产环境也通过。
全站审计判定是否已经被入侵
修复完后必须做一次全站审计,检查是否已经有恶意文件在站点里。这是修复后的第一件事。
用find命令搜可疑PHP文件:
# 在 uploads 目录及其子目录里搜含 eval 的 PHP 文件
find /www/wwwroot/yoursite.com/uploads -name "*.php" -exec grep -l "eval(" {} \;
# 搜近 90 天内修改过的 PHP 文件(可疑变更)
find /www/wwwroot/yoursite.com -name "*.php" -mtime -90 -type f
# 搜文件大小异常的图片(正常 jpg 通常小于 5MB)
find /www/wwwroot/yoursite.com/uploads -name "*.jpg" -size +10M
找到可疑文件后逐个查看内容。如果是webshell(远程执行代码的脚本),证明站点曾被入侵——此时需要:
- 立即删除所有webshell文件。
- 检查admin表是否有异常账号(可疑用户名、邮箱)。
- 检查dede_admin表的logintime字段是否有可疑登录记录。
- 修改所有管理员密码。
- 检查cookies表和session表是否有持久化的攻击者会话。
- 检查数据库内容是否被篡改(首页文章、栏目设置、友情链接)。
- 更换所有API密钥和敏感配置。
如果不确定是否被入侵,请安全公司做专业审计。自行处理容易遗漏后门。
DedeCMS上传漏洞历史回顾
了解过往漏洞演化能更好理解为什么要叠加多层防御:
- 2010年首次披露:扩展名白名单不严,简单改后缀绕过。官方修补:增加扩展名严格白名单。
- 2012年绕过:用双扩展名evil.php.jpg绕过单一扩展名检查。
- 2015年绕过:利用multipart边界畸形让PHP解析器误判扩展名。
- 2018年披露:上传图片时通过EXIF字段注入PHP代码,配合服务器配置错误执行。
- 2022年披露:利用SVG文件类型绕过PHP默认的图片类型识别(SVG是文本格式可以含脚本)。
每次漏洞披露后官方都打过补丁,但DedeCMS 2018年之后官方停止维护,部分变体漏洞至今没有官方修复。社区版本(v5.7 SP2 community edition等)部分修了,但完整防护仍需要站长自己做加固。本文给出的五层防御覆盖了所有已知变体。
长期防御建议
- 升级到DedeBIZ或迁出:DedeBIZ是DedeCMS原班人马另起炉灶的新项目,安全性维护比社区版本好。如果业务允许,迁到WordPress或现代CMS是更彻底的方案。整套服务器加固体系可以参考 Linux下DedeCMS生产级安全加固指南 配置mpm-itk、php-fpm权限分离与fail2ban。
- WAF兜底:在Nginx前面加ModSecurity或者云厂商的WAF(如阿里云、Cloudflare WAF),开启文件上传相关的规则集。规则集要选最新版,老版规则对2022年后的新变体覆盖不全。
- 定期审计:每月跑一次上面的find命令搜可疑文件,是防止后门驻留的基本动作。可以把命令写进cron + 邮件告警,自动化值班。
- 最小权限:uploads目录的PHP文件执行权限设为644(不含+x),目录设为755。即使有恶意文件落地,权限受限难以执行。
- 监控日志:Nginx access.log里/uploads/*.php的访问全部记录到独立日志文件,定期分析。任何这类访问都是潜在攻击。
常见问题解答
修了uploadsafe.inc.php后正常的图片上传也失败怎么办
大概率是MIME白名单太严格。先查PHP错误日志找具体ShowMsg的报错点。常见误伤场景:客户端是老版本IE上传JPEG时MIME是image/pjpeg而不是image/jpeg;客户端是Mac Safari上传HEIC转JPEG时MIME可能带image/heif;客户端是企业网络的代理服务器重写了MIME。对这些场景把白名单适当放宽(加image/pjpeg、image/heif)。如果是finfo函数未启用(php.ini没开fileinfo扩展),需要先在PHP里开启 extension=fileinfo。
可选的内容扫描层经常误伤合法文件怎么办
内容扫描层是可选的,不是必须。对内容多样的站点(接受Office文档、PSD等)误伤率会比较高。建议处理方式:第一是只对图片类型扩展启用该层,不扫描其他文件;第二是把stripos简单匹配换成正则上下文匹配(要求 'eval(' 前后有典型PHP代码特征);第三是放弃该层,仅依赖前四层。前四层已经能拦下绝大多数已知攻击变体。
修复后还需要更换管理员密码吗
强烈建议更换。如果漏洞被利用过(即使你不知道),攻击者可能已经拿到管理员凭据或留下后门。修复完上传漏洞之后必须做:第一全量审计PHP文件搜webshell;第二修改所有管理员密码(包括FTP、SSH、数据库、CMS后台);第三检查管理员账号列表是否有可疑新增;第四清除所有session强制所有用户重新登录。这些是事后清理的标准动作。
有没有现成的脚本批量检测DedeCMS站是否含webshell
D盾、河马webshell查杀(hm.shellpub.com)、安全狗都有专门针对PHP webshell的扫描工具。免费版能识别90%以上的已知webshell特征。把站点wwwroot目录上传到这些工具扫一遍能初步排查。注意工具有误报率,扫到可疑文件需要人工核实再决定是否删除。商业级安全审计推荐找专业安全公司做静态分析+动态测试。
DedeCMS还在更新吗,应该继续用吗
DedeCMS官方在2018年宣告停止商业授权销售,目前的5.7 SP2和5.8都是社区维护版本。安全补丁是社区自发提供,不像WordPress有完善的更新机制。如果你的站点是新项目,强烈不建议选DedeCMS——长期安全风险高。已有的老站点建议要么迁到WordPress等活跃项目,要么按本文方式做安全加固后继续维护。
修了文件后被DedeCMS升级覆盖怎么办
把修改记录到独立的补丁文件dede_security_patch.diff用git管理。每次官方升级前先git diff看你的修改是否会被覆盖;升级后跑一遍diff应用补丁。如果团队有CI/CD流程,把补丁应用纳入部署管道。手工维护时记得在uploadsafe.inc.php的修改段加注释标记 'CUSTOM_SECURITY_PATCH 起止',下次升级前用grep快速定位。
uploads目录禁解析后老的图片访问会受影响吗
不会。Nginx的 location ~ \.(php|...) 规则只匹配PHP等动态扩展,对.jpg、.png、.gif这些静态文件不影响。配置后立刻刷新一些图片URL验证。如果发现某些资源被误屏蔽,看location块的具体匹配规则。配置生效需要nginx -s reload。
多站点共用一个服务器加固一个站点要不要也加固其他
必须。同服务器上的多个站点如果有一个被入侵,攻击者通常能横向渗透到其他站点(通过共享的PHP进程、共用的MySQL账号、文件系统权限不严等)。安全加固要全服务器同时做。如果实在不能同时改,至少要在Nginx上对uploads目录全局禁解析,并隔离不同站点的PHP-FPM池。
实战补充:保哥处理过的六个被入侵案例
过去4年保哥处理过6个被DedeCMS上传类漏洞利用过的客户站。整理一下每个案例的入侵路径和修复过程,看完能更直观理解多层防御的必要性。
| 入侵路径 | 发现时间 | 影响范围 | 修复方案 |
|---|---|---|---|
| 双扩展名evil.php.jpg绕过 | 入侵后32天发现 | 首页被植入跳转JS | MIME校验+文件名规范化+Nginx禁解析 |
| EXIF字段注入PHP代码 | 入侵后17天发现 | 插入垃圾外链页面412个 | getimagesize+Nginx禁解析+图片专用内容扫描 |
| SVG含script绕过 | 入侵后6天发现 | 管理员账号被盗用一次 | 白名单移除svg+Nginx禁解析 |
| multipart边界畸形 | 入侵后88天发现 | 整站镜像到攻击者域名 | 升级PHP 7.4到8.1+全部方案 |
| 文件名编码绕过 | 入侵后14天发现 | 植入挖矿脚本 | 文件名规范化+服务器全局扫 |
| 编辑器插件漏洞 | 入侵后4天发现 | SEO黑链插入 | 移除老版编辑器插件+MIME校验 |
这6个案例的共同点:单层防御失败时,多层防御能阻断攻击链。比如双扩展名案例绕过了扩展名检查,但如果有Nginx禁解析配合,文件落地后也无法被执行。编辑器插件案例如果同步部署了 KindEditor编辑器深度优化指南 里的上传目录归档与CSRF加固,攻击面会进一步收窄。
PHP版本与防御能力的关系
PHP自身版本对文件上传漏洞防御能力影响很大:
- PHP 5.6及以下:finfo在某些版本对畸形文件MIME探测有bug;getimagesize对部分构造的图片返回值不可靠。如果服务器还在用PHP 5.6,强烈建议升级。
- PHP 7.0到7.3:基础校验函数稳定,但random_bytes在某些低版本编译时未启用。
- PHP 7.4到8.x:所有本文用到的函数都稳定可用。如果你的DedeCMS跑在PHP 7.4以下,应该优先升级PHP。
DedeCMS 5.7 SP2官方支持PHP 5.6到7.4,5.8社区版兼容到PHP 8.0。如果可能升级到PHP 8.0或更高版本,本文的防御方案能更稳定运行。升级PHP之前必须备份并在测试环境验证DedeCMS兼容性,部分老插件可能在PHP 8.x下报错。
结合宝塔面板的实施步骤
大部分国内DedeCMS站点跑在宝塔面板上。宝塔自带的"网站防火墙"插件能在Nginx层做部分上传防护,但不够细化。建议同时做:
- 宝塔后台 - 网站 - 你的站点 - 设置 - 配置文件,把上面Nginx的location段加进去,保存重启。
- 宝塔后台 - 安全 - 防火墙,开启"文件上传过滤",规则集选最严。
- 宝塔后台 - 文件 - 你的uploads目录 - 权限 - 设为755,所有者www,禁止其他用户写。
- 宝塔后台 - 软件商店 - 安装"网站监控"插件,开启文件变更告警。
- 本文MIME校验、getimagesize验证、文件名规范化三层通过SSH编辑include/uploadsafe.inc.php实施。改前备份原文件到backup目录。
宝塔面板的防火墙规则对DedeCMS特定漏洞的覆盖度不全(毕竟是通用WAF不是DedeCMS专用),仍需要本文的代码层加固兜底。安装宝塔之后定期升级到最新版,老版宝塔本身也存在过几个安全公告。
因本文不是用Markdown格式的编辑器书写的,转换的页面可能不符合AMP标准。