保哥笔记

织梦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文件上传的完整流程,才能知道为什么修这个文件能起到加固作用:

  1. 用户提交文件上传请求(前端表单或者编辑器调用)。
  2. 请求进入DedeCMS的入口PHP文件(如album_add.php、image_add.php、media_main.php等多个上传入口)。
  3. 这些入口都会require_once include/uploadsafe.inc.php做文件名和类型的安全过滤。
  4. uploadsafe.inc.php通过后才走真正的文件保存逻辑(move_uploaded_file)到uploads目录。

uploadsafe.inc.php是上传链的安全闸口。它做得好,所有上传入口都受益;它做得不严,攻击者可以从任一上传入口绕进来。所以加固这个文件是覆盖面最广的防御措施。

原版逻辑的三个明显缺陷

DedeCMS 5.7 SP2原版的include/uploadsafe.inc.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; } }

关键细节:

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;

核心思路:

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>

这层防御的逻辑:

这是纵深防御理念的体现——单层防御不够稳,多层叠加才能在某层被绕过时依然保护住业务。配置后必须 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或类似引擎,扫描精度比简单字符串匹配高一个量级。

验证修复是否生效的四组测试

修完上传一些测试样本验证:

每个测试都要在生产环境模拟做一遍,不要只在开发环境验证——服务器配置经常不一致。生产环境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(远程执行代码的脚本),证明站点曾被入侵——此时需要:

  1. 立即删除所有webshell文件。
  2. 检查admin表是否有异常账号(可疑用户名、邮箱)。
  3. 检查dede_admin表的logintime字段是否有可疑登录记录。
  4. 修改所有管理员密码。
  5. 检查cookies表和session表是否有持久化的攻击者会话。
  6. 检查数据库内容是否被篡改(首页文章、栏目设置、友情链接)。
  7. 更换所有API密钥和敏感配置。

如果不确定是否被入侵,请安全公司做专业审计。自行处理容易遗漏后门。

DedeCMS上传漏洞历史回顾

了解过往漏洞演化能更好理解为什么要叠加多层防御:

每次漏洞披露后官方都打过补丁,但DedeCMS 2018年之后官方停止维护,部分变体漏洞至今没有官方修复。社区版本(v5.7 SP2 community edition等)部分修了,但完整防护仍需要站长自己做加固。本文给出的五层防御覆盖了所有已知变体。

长期防御建议

常见问题解答

修了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天发现首页被植入跳转JSMIME校验+文件名规范化+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自身版本对文件上传漏洞防御能力影响很大:

DedeCMS 5.7 SP2官方支持PHP 5.6到7.4,5.8社区版兼容到PHP 8.0。如果可能升级到PHP 8.0或更高版本,本文的防御方案能更稳定运行。升级PHP之前必须备份并在测试环境验证DedeCMS兼容性,部分老插件可能在PHP 8.x下报错。

结合宝塔面板的实施步骤

大部分国内DedeCMS站点跑在宝塔面板上。宝塔自带的"网站防火墙"插件能在Nginx层做部分上传防护,但不够细化。建议同时做:

  1. 宝塔后台 - 网站 - 你的站点 - 设置 - 配置文件,把上面Nginx的location段加进去,保存重启。
  2. 宝塔后台 - 安全 - 防火墙,开启"文件上传过滤",规则集选最严。
  3. 宝塔后台 - 文件 - 你的uploads目录 - 权限 - 设为755,所有者www,禁止其他用户写。
  4. 宝塔后台 - 软件商店 - 安装"网站监控"插件,开启文件变更告警。
  5. 本文MIME校验、getimagesize验证、文件名规范化三层通过SSH编辑include/uploadsafe.inc.php实施。改前备份原文件到backup目录。

宝塔面板的防火墙规则对DedeCMS特定漏洞的覆盖度不全(毕竟是通用WAF不是DedeCMS专用),仍需要本文的代码层加固兜底。安装宝塔之后定期升级到最新版,老版宝塔本身也存在过几个安全公告。

因本文不是用Markdown格式的编辑器书写的,转换的页面可能不符合AMP标准。