织梦article_add Cookie泄漏SQL注入修复
DedeCMS会员中心article_add.php的dede_fieldshash校验是单纯md5,攻击者只要在Cookie里拿到合法的dede_fields样本就能任意构造哈希绕过。保哥过去4年帮9个被打的客户做过修复,本文给出五道纵深防线:HMAC风格的md5加salt、参数化查询、输入过滤、横向扫所有会员入口、Nginx WAF兜底,附某技术站5天入侵响应实录。
DedeCMS 的会员中心 member/article_add.php 文件 2016 年首次披露过一个 SQL 注入漏洞:攻击者可以通过构造特殊的 Cookie 绕过后端的 dede_fieldshash 校验机制,向数据库执行任意查询。这个漏洞核心问题是 Cookie 信息泄漏让校验哈希可被预测,配合不严格的参数过滤造成注入。本文不讨论攻击如何构造(攻击细节请阅 CNVD / CVE 数据库的官方披露),只讲修复方案、纵深防御、修复后的入侵审计。保哥过去 4 年帮 9 个被 SQL 注入打过的 DedeCMS 客户做过修复,本文是这些工作的整理。
覆盖版本:DedeCMS 5.7 SP2 / 5.7 UTF8 SP1 / 5.8 社区版。涉及的文件主要是 member/article_add.php、include/dedesql.class.php、include/common.inc.php。修复前必须备份完整源代码和数据库。
dede_fieldshash 校验机制的设计意图
先理解 DedeCMS 的设计才能知道为什么会出现这个漏洞。member/article_add.php 是会员发布文章的入口。用户在前台填表单提交文章时,DedeCMS 用一个隐藏字段 dede_fields 描述各个字段的类型和约束(如 title=string,4-100; body=html,200-50000);同时用 dede_fieldshash 字段存这个描述的 md5 哈希,防止前端篡改字段类型。
原版逻辑大致是:
- 表单生成时把 dede_fields 写到 hidden input,并计算 dede_fieldshash = md5(dede_fields)。
- 用户提交后,后端取 POST 里的 dede_fields,重新计算 md5,与 POST 的 dede_fieldshash 比较。
- 如果一致,相信 dede_fields 没被篡改,按它描述的字段约束处理表单数据。
这个设计的问题是:md5 是单向哈希,但只要 dede_fields 的内容被攻击者知道,就能自己算 md5 构造合法 hash。攻击者只需要在 Cookie 里或者别的地方拿到一份合法的 dede_fields 样本,就能任意构造新的字段描述,让后端按攻击者的约束处理数据——攻击者可以让 body 字段不做 SQL 转义,直接传入恶意 SQL 片段实现注入。
修复的核心思路是给 md5 计算加一个攻击者无法预测的密钥(HMAC 思路),让攻击者即使知道 dede_fields 也算不出合法 hash。
掺入服务端密钥的双重 md5 改造
在 member/article_add.php 大约第 40 行(具体行号视 SP 版本和你之前的修改略有差异,用关键字 fieldshash 定位)找到 hash 校验段。原代码大致是:
$fieldshash = md5($dede_fields);
if ($fieldshash !== $_POST['dede_fieldshash']) {
ShowMsg('字段描述被篡改', '-1');
exit;
}修改为引入服务端密钥的双重 md5:
// 服务端密钥,部署时在 data/common.inc.php 里配置
// $cfg_security_salt = '你自定义的高熵字符串_至少32字符'
$securitySalt = isset($GLOBALS['cfg_security_salt']) ? $GLOBALS['cfg_security_salt'] : 'anythingelse_长度至少32位_请在部署时替换';
$expectedHash = md5(md5($dede_fields) . $securitySalt);
if ($expectedHash !== $_POST['dede_fieldshash']) {
ShowMsg('字段描述被篡改', '-1');
exit;
}核心要点:
- 用双重 md5(md5(...) . salt) 而不是简单 md5(... . salt),避免长度扩展攻击。
- 密钥从配置文件读,不要硬编码在 article_add.php 里(否则源码泄漏就废)。
- 密钥长度至少 32 字符,使用高熵随机串(用 bin2hex(random_bytes(16)) 生成一次后写入配置)。
- 每个站点的密钥应该不同,不要复制粘贴常见的示例字符串。
注意:修改这段后,原来的表单生成处也要同步修改 fieldshash 的计算逻辑——否则前端生成的 hash 和后端期望的不一致,所有合法提交都会被拦截。表单生成代码通常在 dede 系统的 include/inc_archives_functions.inc.php 或类似位置,搜 md5($dede_fields) 找到对应代码段同样改为双重 md5 加 salt。
参数化查询替代字符串拼接
第一步只是堵了表单字段类型被篡改的入口。但 DedeCMS 整个项目里有大量字符串拼接的 SQL,根本治理是改用参数化查询。从 member/article_add.php 的数据写入语句开始:
原版(脆弱)写法:
$query = "INSERT INTO #@__archives (typeid, title, body, mid)
VALUES ($typeid, '$title', '$body', $mid)";
$dsql->ExecuteNoneQuery($query);改为参数绑定写法(DedeCMS 自带的 dedesql.class.php 支持参数绑定但很多老代码没用):
$dsql->SetQuery("INSERT INTO #@__archives (typeid, title, body, mid) VALUES (?, ?, ?, ?)");
$dsql->ExecuteWithParams([$typeid, $title, $body, $mid]);如果 dedesql.class.php 没有 ExecuteWithParams 方法(老版本可能没有),用 PDO 直接写:
$pdo = new PDO("mysql:host=$cfg_dbhost;dbname=$cfg_dbname;charset=utf8mb4", $cfg_dbuser, $cfg_dbpwd);
$stmt = $pdo->prepare("INSERT INTO {$cfg_dbprefix}archives (typeid, title, body, mid) VALUES (?, ?, ?, ?)");
$stmt->execute([$typeid, $title, $body, $mid]);参数化查询的核心保护是:MySQL 服务端把 SQL 模板和参数分开解析,参数值永远不会被当作 SQL 代码执行。这是 SQL 注入的根本防御。
输入过滤与白名单标签
参数化是技术层防御,业务层还需要输入过滤。在 member/article_add.php 接收 POST 数据后立刻做一层过滤:
function sanitizeInt($val) {
return (int)$val; // 强制转 int,非数字字符全部丢弃
}
function sanitizeString($val, $maxLen = 1000) {
$val = trim($val);
$val = strip_tags($val, '<p><br><strong><em><a><img>');
if (mb_strlen($val) > $maxLen) {
$val = mb_substr($val, 0, $maxLen);
}
return $val;
}
$typeid = sanitizeInt($_POST['typeid'] ?? 0);
$title = sanitizeString($_POST['title'] ?? '', 100);
$body = sanitizeString($_POST['body'] ?? '', 50000);
$mid = (int)$cfg_ml->M_ID; // 会员 ID 从会话拿,不信任 POST几个关键细节:
- 整型字段用强制类型转换((int)),是最严的过滤。
- 字符串字段用 strip_tags 加白名单标签,只保留必要的 HTML 标签,其他全清。
- 会员 ID(mid)一定从会话或登录态拿,绝对不信 POST 里的字段——这是常见的越权漏洞起点。
- 用 mb_strlen 而不是 strlen,UTF-8 中文字符串长度计算才准确。
横向扫描所有相似入口
仅修 member/article_add.php 不够。DedeCMS 的会员中心有多个文件用相似的 dede_fieldshash 校验机制:
- member/album_add.php(图集发布)
- member/soft_add.php(软件下载发布)
- member/info_add.php(资讯发布)
- member/edit.php(文章编辑)
- member/album_edit.php、soft_edit.php、info_edit.php 等编辑入口
每个文件都需要先加 salt 改造 fieldshash 校验,再改参数化查询,再加输入过滤。漏一个文件就等于留一个口子。
用 grep 全站搜定位需要改的文件:
# 搜所有用 fieldshash 校验的文件
grep -r "fieldshash" /www/wwwroot/yoursite.com/member/
# 搜可能含字符串拼接 SQL 的文件
grep -rn "INSERT INTO.*VALUES.*\\\$" /www/wwwroot/yoursite.com/member/
# 搜直接用 $_POST 拼 SQL 的危险写法
grep -rn "\\\$_POST\\[" /www/wwwroot/yoursite.com/member/ | grep -i "select\\|insert\\|update\\|delete"这三条命令能列出绝大多数需要审计的文件。逐个看每个文件的 SQL 写法是否安全。
服务器层面的 WAF 兜底
代码层防御是主战场,但服务器层面的 WAF 能拦截大量已知 SQL 注入特征。在 Nginx 配置里增加规则:
map $args $sql_injection {
default 0;
"~*union[\s]+select" 1;
"~*select[\s]+.*[\s]+from" 1;
"~*[\'\"].*[\\\\]+x[0-9a-f]{2}" 1;
"~*concat[\s]*\(" 1;
}
server {
if ($sql_injection) {
return 403;
}
# ...其他配置
}这套规则会拦下 SQL 注入的典型特征(UNION SELECT、CONCAT、十六进制编码等)。注意 WAF 规则是字符匹配,对绕过手法(如 /*!union*/select、空格替换为 %20+%20 等)覆盖不全。WAF 只是兜底,不能替代代码层加固。
商业 WAF(云厂商的 Web 应用防火墙、Cloudflare、阿里云盾)对 SQL 注入的覆盖比上面的简单规则更全面。对预算允许的站点强烈推荐。
验证修复是否生效
修完做几组测试:
- 测试 1:用合法会员账号发布一篇正常文章。预期:成功,按预期写入数据库。
- 测试 2:手动篡改前端 dede_fieldshash 字段(用浏览器开发者工具)后提交。预期:服务端拦下,提示"字段描述被篡改"。
- 测试 3:在 title 字段输入 ' or 1=1--。预期:被参数化查询当作字面值处理,title 实际存储这串字符,不触发 SQL 异常。
- 测试 4:在 typeid 字段输入 1; DROP TABLE users。预期:被强制类型转换为整数 1,DROP 部分丢弃。
每个测试都要看后端日志确认行为符合预期。如果有任何一个测试触发了意外异常,需要回溯检查修改是否完整。
修复后的全站入侵审计
如果你的站点已经在生产环境运行了一段时间才修复,必须假设漏洞已经被利用过,做一次全面审计:
- 检查 dede_admin 表:看是否有不熟悉的管理员账号、最近一次登录 IP 是否可疑。SELECT id, userid, email, logintime, loginip FROM dede_admin ORDER BY logintime DESC LIMIT 20。
- 检查 dede_member 表:搜近期注册的会员,看是否有批量注册的可疑账号(同 IP、同邮箱域名、同注册时间集中)。
- 检查 dede_archives 表:搜 title 或 body 含 link / a href / iframe 的文章,可能是黑链注入。SELECT id, title, mid, sortrank FROM dede_archives WHERE body LIKE '%<iframe%' OR body LIKE '%<a href=%casino%'。
- 检查 dede_purview 表:管理员权限是否被异常修改。
- 查 webshell:本系列上一篇(cid 725)的方法搜可疑 PHP 文件。
- 看 Web 服务器日志:grep 一下 access.log 里 member/article_add.php 的可疑请求(含 union、select、concat 等关键字)。
- 看 MySQL 慢查询日志和 binlog:如果开启了 binlog,能精确回放某段时间内的所有 DDL/DML 操作。
发现任何被篡改的数据先备份再清理。被植入的黑链文章不要直接 DELETE——先看 mid(发布会员 ID),看是不是被攻击者通过批量注册的虚假会员发的。如果是,整批会员账号需要清理。
DedeCMS 历史 SQL 注入漏洞回顾
DedeCMS 的 SQL 注入漏洞历史很长。下面是社区披露过的主要变体:
| 年份 | 漏洞点 | 触发条件 | 修复方式 |
|---|---|---|---|
| 2014 | plus/search.php 关键词参数 | 未过滤 $keyword 直接拼接 SQL | 参数化查询 + 关键词白名单 |
| 2015 | plus/diy.php diyid 参数 | 整数字段未强制转 int | 强制 (int) 类型转换 |
| 2016 | member/article_add.php fieldshash 绕过 | 本文讨论的漏洞 | HMAC 风格的 hash 加 salt |
| 2018 | plus/feedback.php msg 参数 | 未过滤评论文本 | htmlspecialchars + 参数化 |
| 2020 | include/dialog/select_soft.php f 参数 | 未授权后台接口任意调用 | 授权检查 + 输入过滤 |
| 2022 | plus/recommend.php aid 参数 | aid 字段被构造为子查询 | 整数类型严格校验 |
从这张表能看出几个规律:
- SQL 注入漏洞每年都有新变体,整个项目里到处都是潜在风险点。
- 单点修复(每年补一个)跟不上漏洞曝光速度,必须做系统性的参数化查询改造。
- DedeCMS 官方 2018 年后不再发安全补丁,所有修复靠站长和社区。
长期建议:迁出 DedeCMS
本文虽然给出了详细的修复方案,但保哥的真实建议是:如果你的 DedeCMS 站点还能撑 2 年以上的业务运营,建议规划迁出 DedeCMS。原因:
- 新漏洞不会停止:DedeCMS 的代码量大、设计陈旧、官方维护停摆,每年都会有新漏洞被披露。修一个补一个的策略最终会被新漏洞拖垮。
- 修复成本累积:每次修复都要做完整的回归测试、入侵审计、密码轮换。修 5 次的总成本可能等于迁站一次。
- 合规风险:等保 2.0、数据安全法对老旧 CMS 的安全要求越来越严,长期持有 DedeCMS 站点的合规审计成本高。
- 团队招聘:还熟悉 DedeCMS 的开发者越来越少,遇到问题求助困难。
迁出目标推荐 WordPress(生态成熟、安全机制完善、迁移脚本现成)。迁移周期视站点规模 1 到 3 个月。如果客户预算和时间不允许迁站,按本文方案做加固后定期审计也是可行的次优解。
常见问题解答
修改 fieldshash 校验后老的草稿提交会失败吗
会。原来用旧 hash 机制生成的草稿(前端表单已经提交但被保存为草稿)在新机制下校验不通过。修复部署后第一时间通知用户重新打开草稿点保存,让前端用新 hash 重新生成。或者在过渡期保留双重校验逻辑:先用新 salt 算 hash,如果不通过再用老 hash 算一次,老 hash 通过的标记为"老格式草稿"在日志里记录。一周后下线老 hash 兼容逻辑。
cfg_security_salt 这个密钥保存在哪里最安全
最佳实践是放在 data/common.inc.php 之类已经被 Web 服务器禁止直接访问的目录。data 目录通常有 .htaccess 或 nginx location 禁止外部访问。salt 字符串通过 $GLOBALS['cfg_security_salt'] 在 PHP 脚本里读取。绝对不要把 salt 写在 member 或 plus 等可能被反向遍历的目录。同时建议把 salt 文件的文件权限设为 600(仅 www 用户可读),并在备份脚本里把这个文件单独加密。
每个会员用不同的 salt 会不会更安全
会更安全但实现成本高。要给每个会员存独立 salt(如 dede_member 表加 hash_salt 字段),表单生成和验证时都要按当前会员 ID 取对应 salt。这种方案对中等以下站点过度设计,对超大型站点或者高安全场景值得考虑。中等站点用全站统一 salt 加上定期轮换(每 6 个月换一次)已经能覆盖绝大多数威胁模型。
参数化查询会影响性能吗
几乎无影响。参数化查询在 MySQL 服务端会被预编译并缓存查询计划,对重复执行的相同结构查询反而比字符串拼接更快。性能差异在毫秒级别,对中小站点完全可以忽略。担心性能的常见原因是误把 prepare/execute 用错——每次循环都重新 prepare 而不是复用同一个 statement,这种用法才会慢。正确的写法是一次 prepare 多次 execute。
如何检测自己的 DedeCMS 站是否还有未修的 SQL 注入
分两种手段。第一是被动审计:用上面给出的 grep 命令搜全站字符串拼接 SQL 的代码模式,逐个文件人工 review。第二是主动测试:用 sqlmap、burpsuite 之类的安全测试工具对自家站点的所有表单和参数做扫描。注意主动测试只能在自己持有的站点上做,对他人站点是违法的。商业级安全审计可以委托 360、知道创宇等专业公司。
修复后还需要重置所有会员密码吗
取决于是否确认有数据泄漏。如果站点被入侵期间攻击者可能拖走了 dede_member 表(含会员密码哈希),强烈建议强制所有会员重置密码——即使密码哈希是 md5 加盐,攻击者也可能通过彩虹表或字典攻击破解弱密码。重置方案:站点公告说明安全升级、用户下次登录强制重置、邮件通知活跃会员。这是事故响应的必要动作。
DedeCMS 升级会覆盖我的修复吗
会。和 cid 725 文章里讲的一样,所有手工改的核心文件升级时都会被覆盖。处理方式:把所有修改用 git diff 记录成补丁文件,每次升级前先看官方代码是否已经修了同样问题(如果修了就用官方的),没修的话升级后重新应用补丁。理想情况是把修改提交到 DedeCMS 社区版本(如 DedeBIZ),让维护者合并进官方。
不会 PHP 怎么办,能找人代修吗
可以。安全加固的代码改动量不大但需要懂 PHP 的开发者执行。市场上有大量做老 CMS 维护的服务商,单站点加固费用通常 500 到 3000 元。如果是商业站点建议找有安全资质的服务商(不只是 PHP 开发者)——他们除了改代码还能做入侵审计、密码轮换、长期监控等完整服务。自己不懂 PHP 也不要按网上随机找的代码片段瞎改——错改可能引入新漏洞。
真实修复案例:某技术站 SQL 注入入侵响应记录
2025 年 9 月接到一个技术站求助:DedeCMS 5.7 SP2 站,10 万篇技术文章,运营 8 年。站方突然发现 GSC 报告"Pages with manual actions"——Google 把站点标记为"含有大量垃圾内容"。打开几篇文章看,正文末尾被植入了博彩外链。
保哥的入侵响应流程:
- 0-2 小时:快速确认入侵范围。SQL 查 archives 表,发现近 30 天有 4732 篇文章 body 含 'href="https://gambling-xxx' 这类外链。判断攻击者通过批量注入篡改了文章。
- 2-6 小时:审计入侵路径。grep access.log 找到 article_add.php 在凌晨 3 点有约 5000 次成功请求,对应攻击高峰。判断漏洞是 fieldshash 绕过。
- 6-12 小时:紧急止血。立刻关闭会员注册和文章发布功能(Nginx 把 member 目录全部 deny)。备份数据库。
- 12-36 小时:应用本文修复方案。改 fieldshash 校验加 salt、改 SQL 参数化、改输入过滤、加 Nginx WAF。
- 36-72 小时:清理被注入内容。SQL 批量 UPDATE 把 body 里的 gambling-xxx 链接全部替换为空。同时清理掉攻击者批量注册的 1200 多个虚假会员账号。
- 72-120 小时:验证修复。开启会员注册,做四组测试用例确认拦截到位。提交 GSC 重新审核请求。
- 120 小时后:长期监控。开启文件变更告警、binlog 实时审计、每小时跑一次 webshell 扫描。
整个事件响应耗时约 5 天,客户损失估算(Google 流量下降 + 用户信任)约 8 万元。修复后 3 周 GSC 解除人工操作,流量逐步恢复。
这个案例的最大教训是:被入侵和发现入侵之间通常有几周到几个月的时间窗口。这段窗口越长损失越大。日常监控(文件变更告警、异常 SQL 查询、可疑会员注册)能把窗口压缩到 24-48 小时内。
预防性监控的具体配置
除了修复,长期防御还要有监控。给一份保哥的实战配置清单:
- 文件变更告警:用 inotify 监听 PHP 文件目录的变更事件,任何新增或修改写日志并发邮件。配置示例:inotifywait -m -r -e modify,create,delete /www/wwwroot/yoursite.com --include '\.php$'。
- 异常 SQL 监控:MySQL 开启 general log,每分钟扫一遍最新日志,匹配 UNION SELECT、information_schema 等关键字。命中告警。
- 可疑会员监控:定时任务每小时查最近 1 小时新增会员,按 IP 聚合,单 IP 超过 5 次注册告警。
- 失败登录监控:dede_admin 表 last_failed_login_count 字段定时检查,单账号 1 小时内失败 10 次以上告警。
- 外链监控:每周跑一次 SQL 统计文章正文里的外部域名分布,新出现的可疑域名(含 casino、porn、warez 等关键字)告警。
监控的核心目的是把"被入侵到发现入侵"的窗口从几周压缩到小时级。窗口越短损失越小,恢复越容易。
本文标题:《织梦article_add Cookie泄漏SQL注入修复》
版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0