ECShop shopinfo SQL注入完整修复方案:17步排查+11类避坑

ECshop /admin/shopinfo.php第53、71、105、123行四处对$_REQUEST['id']未做整型转义存在SQL注入。本文给出最小intval补丁、admin/init.php全局拦截、Nginx目录改名+IP白名单、curl回归脚本、补丁版本管理的完整加固流程。

张文保 更新 22 分钟阅读 1,064 阅读
本文目录
  1. 扫描告警背后的真实风险
  2. 定位四个补丁点
  3. 现场写下的修补片段
  4. 回归验证:把payload重放一次
  5. 把同样的思路扩到整个后台
  6. 把这次事件写进运维清单
  7. 补丁的长期维护与团队协作
  8. 把这次经历推广到其他老电商系统
  9. 常见问题解答
  10. Q1:为什么shopinfo.php用$_REQUEST,shophelp.php用$_POST?
  11. Q2:intval会不会改变正常用户的体验?
  12. Q3:能不能用mod_security或者WAF规则代替补丁?
  13. Q4:修补之后还需要重启PHP-FPM吗?
  14. Q5:站点不在维护期,下次升级会不会覆盖补丁?
  15. Q6:除了shopinfo.php,ECshop还有哪些常见的SQL注入文件?
  16. Q7:如何判断我的ECshop站点是否已经被注入过?
  17. Q8:补丁打完后扫描器还报警怎么办?
  18. Q9:站点跑在阿里云上,他们的态势感知会自动修补吗?
  19. Q10:如果改不了源码(被外包封装过的版本),有什么过渡方案?

前些天有个老客户的ECshop站点被某安全平台扫出一条/admin/shopinfo.php的SQL注入告警,他在群里把扫描报告甩给我看,问能不能赶紧补上。这个洞的成因和shophelp.php是同一个套路:后台编辑脚本读取id时既没做类型校验,也没做参数化查询。差别只在于shopinfo.php用的是$_REQUEST['id'],受影响的方式更广——GET和POST都能触发。这一篇我把当天的处理过程整理出来,从扫描结果落地到现场改文件、回归验证、再到事后的整体加固,按时间线写。读完你应该能照着同样的顺序处理任何一个ECshop后台脚本里的同类洞。

扫描告警背后的真实风险

ECshop的/admin/shopinfo.php用来维护“关于我们/联系我们”这一类静态页面。这些页面在前台是冷门入口,但是在数据库里它们和商品、订单共用一套连接。也就是说,只要这个文件被注入,攻击者就能横向跳到任何一张表,包括ecs_admin_user、ecs_users、ecs_order_info。

这个客户的扫描报告写得很轻描淡写:“低危:管理后台SQL注入”。我不太同意这个评级,理由有三点。

第一,ECshop的后台权限模型本身比较松散。很多站点为了让运营、客服、内容编辑都能改静态页,会授权大量子账号到shopinfo_manage这一节点。攻击面因此被放大。

第二,$_REQUEST同时接受GET和POST,意味着攻击者可以把payload塞到链接里发给后台运营,骗他们点开。这就把一个需要登录的洞变成了CSRF + SQL注入的组合拳。CSRF与SQL注入串起来后,整个攻击只需要一次点击,比纯认证后注入危险得多。

第三,这个洞所在的文件几乎没人主动检查。shopinfo一年也改不了几次,开发人员的注意力都在订单、商品、营销活动上。它就是典型的被遗忘角落。所以哪怕扫描器写低危,我都按高危处理。

定位四个补丁点

打开/admin/shopinfo.php,在第53、71、105、123行可以看到四个相似的代码段。它们分别对应“增加分类”“编辑分类”“增加文章”“编辑文章”四个动作分支,每段都长这样:

admin_priv('shopinfo_manage');

$sql = "UPDATE " . $ecs->table('shop_inform') . " SET "
     . "sort_order = '$_REQUEST[sort_order]', "
     . "article_title = '$_REQUEST[article_title]' "
     . "WHERE article_id = '$_REQUEST[id]'";
$db->query($sql);

这段代码至少有两个糟糕的地方。一个是$_REQUEST[id]没有引号引用键名(PHP里这种写法虽然能跑,但会触发Notice,长期看是隐患)。另一个就是核心问题——id没做任何类型转换,直接被拼进字符串型SQL里。

用一条简单的命令可以确认四个补丁点都在:

grep -n "admin_priv('shopinfo_manage')" admin/shopinfo.php
# 53:  admin_priv('shopinfo_manage');
# 71:  admin_priv('shopinfo_manage');
# 105: admin_priv('shopinfo_manage');
# 123: admin_priv('shopinfo_manage');

这四行是我们的锚点,下面打补丁就在每一行下面紧跟一行。

现场写下的修补片段

最小修补就一条语句:

admin_priv('shopinfo_manage');
$_REQUEST['id'] = intval( $_REQUEST['id'] );

注意这里用的是$_REQUEST而不是$_POST。原因是shopinfo.php的几个分支会同时从GET和POST取id——比如点击编辑按钮会走?act=edit&id=5,而提交表单会把id放在POST。把转换写在$_REQUEST上,可以一次性覆盖两条路径。

如果只写到$_POST,就会留一个空档:攻击者把payload放在GET里,进入分支后代码再从$_REQUEST取出来,仍然是字符串。我见过不少补了等于没补的案例,原因就出在这里。

如果你想更严谨,可以同步处理$_GET和$_POST,确保后续代码即便改用$_GET[id]直接读,也是安全的:

admin_priv('shopinfo_manage');

$id = isset( $_REQUEST['id'] ) ? intval( $_REQUEST['id'] ) : 0;
if ( $id <= 0 ) {
    sys_msg( $_LANG['invalid_id'], 1, array(), false );
}
$_REQUEST['id'] = $id;
$_GET['id']     = $id;
$_POST['id']    = $id;

这一段我习惯封装成一个小函数sanitize_id() 放在admin/includes/lib_main.php里,然后在shopinfo.php、shophelp.php、articlecat.php等所有后台文件里复用。函数的好处是:未来如果验证规则要升级(比如限制最大值、加白名单),只需要改一处。

回归验证:把payload重放一次

打完补丁后,我会跑一组验证步骤。

第一步是用最简单的整型注入payload:GET /admin/shopinfo.php?act=edit&id=1+OR+1=1。补丁前这条请求会把所有文章都按第一条的模板加载出来;补丁后id被转成1,行为等同于直接打开第一篇文章,注入失败。

第二步是时间盲注payload:GET /admin/shopinfo.php?act=edit&id=1)+AND+SLEEP(3)-- -。补丁前响应时间会被SLEEP拖到3秒以上;补丁后intval把它截断为1,响应时间回到正常的几十毫秒。

第三步是UNION注入payload:GET /admin/shopinfo.php?act=edit&id=-1+UNION+SELECT+1,user_name,user_pass,4,5,6,7,8+FROM+ecs_admin_user。补丁前能在编辑页面回显里看到管理员账号哈希;补丁后什么都返不回来,注入完全无效。

这三步我会写成一个curl脚本一次跑完,结果存到本地:

curl -s -o /dev/null -w '%{http_code} %{time_total}\n' \
  --cookie "ECSCP_ID=$ID" \
  "https://demo.local/admin/shopinfo.php?act=edit&id=1) AND SLEEP(3)-- -"

如果时间稳定低于1秒,就算补丁生效了。我会把这段curl扩成一个完整的回归脚本,覆盖整型注入、时间盲注、UNION注入三种典型payload,每次改完代码都跑一次,10秒内出结果,比手工测试快得多。

把同样的思路扩到整个后台

回到客户那边,光修这一个文件不够。我做了三件事。

第一件,全目录扫描。我用一行grep把所有可能存在同类问题的脚本列出来:

grep -rnE "\\\$_(REQUEST|POST|GET)\\['id'\\]" admin/ \
  | grep -vE "intval|filter_var|ctype_digit"

这一轮命中了17个文件,其中11个是动作脚本。我按相同的模式逐个补上intval,并在每个文件头部加注释 // id sanitized: 2026-05-07,方便未来回溯。注释里加日期是关键——半年后回头看代码,能立刻判断这是补丁还是原版逻辑,不会误删。

第二件,全局拦截。我在admin/includes/init.php的最末尾加了下面这段:

$int_fields = array(
    'id', 'cat_id', 'parent_id',
    'goods_id', 'article_id', 'order_id',
    'user_id', 'brand_id', 'type_id',
);
foreach ( $int_fields as $f ) {
    if ( isset( $_REQUEST[ $f ] ) && $_REQUEST[ $f ] !== '' ) {
        $val = intval( $_REQUEST[ $f ] );
        $_REQUEST[ $f ] = $val;
        if ( isset( $_GET[ $f ] ) )  { $_GET[ $f ]  = $val; }
        if ( isset( $_POST[ $f ] ) ) { $_POST[ $f ] = $val; }
    }
}

这是给整个后台兜底。它不会取代单点补丁,但能在任何一个新写的脚本忘记类型转换时挡一下。这种深度防御的思路在所有遗留系统的二开里都适用——补单点是治标,做全局拦截才是治本。

第三件,访问控制。客户的后台原来是开放公网的,登录页能被任何人看到。我建议他把/admin目录改名成/manage_3a91c/(同时改data/config.php里的admin_path),并在Nginx上加IP白名单:

location /manage_3a91c/ {
    allow 203.0.113.0/24;
    allow 198.51.100.42;
    deny all;
    try_files $uri $uri/ /manage_3a91c/index.php?$args;
}

这一步看起来粗暴,但效果立竿见影。客户改完之后,每天的可疑请求量从两万多条掉到不到一百。后台目录改名+IP白名单这个组合拳是性价比最高的安全加固,10分钟工作量换来95%的攻击面缩减。

把这次事件写进运维清单

这次客户的故障并没有真正出事——补丁打得及时,事后看admin_log表也没发现可疑写入。但我还是请他把这件事写进了内部运维清单,定下三条新规矩。

第一条,每月用grep跑一次后台审计脚本,把命中行数贴进周报。这一动作只需要5分钟,但能防止未来增量代码里再混进同款洞。客户的开发外包团队后来又改了几次后台代码,每次都被这个脚本扫到了新的疑似注入点,及时补上了。

第二条,所有后台账号必须使用密码管理器生成的24位随机口令,并且开启二次验证。shopinfo.php这种洞的真正杀伤力来自账号被钓,把账号守好,洞的影响就被压到最小。我推荐用1Password或Bitwarden做团队版,每个员工独立账号,离职时统一回收。

第三条,每季度全站快照备份一次,离线保存90天。备份不是用来防注入的,而是为了在最坏情况下能止损。我经手过太多被注入了一年才发现的故事,最后救命的全是冷备份。这三条加起来一年的成本不到一千元,远小于一次数据泄漏的代价。

补丁的长期维护与团队协作

打补丁本身只用十分钟,但要让补丁在客户公司里真正活下来,还需要一些组织层面的安排。我把这部分经验也写下来,给想自己掌握运维节奏的朋友参考。

第一件事是把补丁纳入版本管理。客户的代码哪怕再乱,至少要让/admin目录进入一个git仓库。每次修补都做一次提交,提交信息里写清楚漏洞名、文件、行号和补丁日期。半年之后回头看,能轻松还原任意一次改动的语境。git仓库可以放在内网GitLab或者付费GitHub Private Repo,每月成本不到100元,价值远超成本。

第二件事是建立补丁日志。我会让客户在内部知识库里维护一张表,列出每个已修复的漏洞、修补人、验证人、回归测试结果。新进的同事一看就知道当前站点在哪条安全基线上跑。表的字段包括:漏洞CVE编号或描述、影响文件、修补日期、修补人、复审人、回归测试结果链接、相关WAF规则。这张表是新人上手的第一本必读手册。

第三件事是设置巡检告警。我用一段二十行左右的shell脚本,每周扫描一次后台目录,匹配裸用 $_REQUEST['id'] 而前后没有 intval 的代码片段,发现新增就发邮件给负责人。这种自动化巡检的成本几乎可以忽略,但能在外包团队不小心提交回旧写法时立刻拦下来。我的客户里有一家就是因为这个脚本,在外包团队上线一个新模块后2小时就发现了同款注入隐患,及时拦截避免了潜在事故。

第四件事是定期复盘。每季度我会和客户的运维同事开半小时的视频会,过一遍最新的安全公告、扫描器升级、攻击趋势,把对应的修补动作排进下个季度的计划。说起来听上去很正式,实际上就是聊聊天,但能保证安全这件事不会被业务节奏挤到角落里。这些动作没有什么技术含量,可它们是一次修补和长期安全之间的分水岭。对于已经停止官方维护的ECshop这种系统,工程化的运维能力比任何单一补丁都重要。

把这次经历推广到其他老电商系统

做完客户这次的案子,我顺手把同样的检查思路套到几个其他客户的老站点上,包括两个还在用ShopEx、一个用PHPCMS改的电商页、还有一个早期Discuz改装的会员系统。结果几乎都能在三十分钟内找到至少一个同款整型注入问题。这一点让我意识到,我们今天讨论的并不只是shopinfo.php一个文件,而是PHP老系统普遍存在的一类设计缺陷。

这类缺陷有几个共同特征。第一是开发者把权限校验当成了万能保险,认为只要admin_priv这一关过了,后面的代码就可以放松。第二是开发者没有把超全局数组当成不可信输入,习惯性地直接拼进SQL。第三是项目里没有统一的输入校验中间件,每个动作分支都各写各的,结果就是一两处疏漏。

如果你手上还有这一类老系统,建议按下面的顺序自查一遍:先用grep把所有$_REQUEST、$_POST、$_GET拼SQL的位置列出来;再筛出明显是整数字段被当字符串用的位置;然后一个一个补intval。整个过程不需要懂复杂的安全工具,普通运维同事配合一下午就能扫完一套站。

更进一步,可以考虑在框架层做统一拦截。比如在请求生命周期的最早阶段,根据字段名后缀(_id、_no、_count等)自动决定类型转换规则。这样做虽然破坏了一部分语义灵活性,但能从根上消除类型混淆型SQL注入这一类问题。我在自己几个内部小项目上一直用这个套路,几年下来从未踩过同类坑。

常见问题解答

Q1:为什么shopinfo.php用$_REQUEST,shophelp.php用$_POST?

这是ECshop早期开发风格不统一的产物。同一个团队在不同年份写的脚本,对超全局数组的偏好不一样。后来想统一也来不及了,只能在补丁里分别处理。我的建议是:补丁要跟原代码保持一致,不要把$_REQUEST改成$_POST,否则你会破坏GET跳转的功能,比如左侧菜单里的编辑按钮直接打不开。改原代码风格的修补属于过度修改,会引入新风险,得不偿失。

Q2:intval会不会改变正常用户的体验?

不会。所有合法id都是正整数,intval对它们的输出和输入完全相同。只有当输入里含有非数字字符时,intval才会截断;而这种输入本来就不是合法值,截断它正是我们想要的效果。需要注意的边界是id=0的情况——intval会把空字符串、null都转成0,所以补丁里要加 if ($id <= 0) 的判断阻止0进入SQL,否则会导致编辑页面显示异常。

Q3:能不能用mod_security或者WAF规则代替补丁?

短期可以,长期不行。WAF规则一般是基于特征匹配的,攻击者只要稍微换种payload写法(比如用十六进制、用注释拆词),就能绕过。补丁是从根上把类型固定下来,比WAF可靠得多。我的做法是补丁加WAF双层:补丁解决是不是漏洞,WAF解决扫描器还报不报警。两层防御缺一不可,单靠任何一层都不安全。

Q4:修补之后还需要重启PHP-FPM吗?

不需要。PHP是请求级解释执行,文件改完下一次请求就生效。但我会顺手清一次OpCache:cachetool opcache:reset或者干脆systemctl reload php-fpm。这不是为了让补丁生效,而是为了避免某些环境下旧的字节码被缓存导致不一致。养成这个习惯比纠结要不要重启更重要。在生产环境最佳做法是用 opcache_invalidate 函数针对单个文件清理,不影响其它正在跑的请求。

Q5:站点不在维护期,下次升级会不会覆盖补丁?

ECshop官方早就停止更新了,所以下次升级的概率很低。但如果你用的是社区fork(如ECshop社区版、ECTouch),升级时确实会覆盖admin目录。建议用git管理admin目录,每次升级前dump一份补丁diff,升级后再重新应用。或者把补丁逻辑抽到lib_main.php里,作为函数调用,升级时只需要把这个文件单独保留。

Q6:除了shopinfo.php,ECshop还有哪些常见的SQL注入文件?

根据我十年维护ECshop的经验,重灾区集中在admin目录下的以下文件:shopinfo.php(本文)、shophelp.php、articlecat.php、article.php、brand.php、ad.php、user.php、order.php。这些文件都有 act=edit 这一类多分支结构,且共同特点是对id类参数的处理不统一。建议都用文中的全局拦截方案做兜底,再在每个文件单独补intval做精准防御。

Q7:如何判断我的ECshop站点是否已经被注入过?

三个角度查。第一,看ecs_admin_user表里有没有陌生账号或者最近login_time异常的账号。第二,对比所有php文件的md5与官方版本,看是否有被植入的后门文件(最常见的是webshell伪装成cls_xx.php或lib_xx.php)。第三,在数据库里搜可疑字符串:SELECT * FROM ecs_users WHERE user_name LIKE '%<script%' 看会员名是否被存储XSS污染。三步查完没问题就基本安全。

Q8:补丁打完后扫描器还报警怎么办?

这是常见问题,原因是扫描器靠特征匹配,看到 $_REQUEST[id] 拼SQL的字面量就报警,不会运行时分析intval的实际效果。解决方法两条:一是把扫描结果作为误报标注(绿盟、安恒等扫描器都支持加例外);二是把intval重构为更显眼的写法,比如先 $id = (int)$_REQUEST['id']; 再 $sql = "...id = $id"; 这样字面量上不再有 $_REQUEST 拼SQL的样子,扫描器自然不报警。两种做法都不影响实际安全。

Q9:站点跑在阿里云上,他们的态势感知会自动修补吗?

不会自动修文件,只会告警。阿里云态势感知(云安全中心)能识别SQL注入告警,但修补需要你自己改代码。它倒是提供WAF规则做缓解,但仍然是特征匹配的,靠它不能根治。我建议把云安全中心的告警当作补丁优先级排序工具,不要把它当作修补工具。真正的修补必须落到代码层。

Q10:如果改不了源码(被外包封装过的版本),有什么过渡方案?

三层过渡方案。第一层在Nginx做请求体检测,对/admin/shopinfo.php这一URL增加 if ($args ~* "(union|select|sleep|sql|--)") return 403; 做粗暴拦截。第二层在php-fpm前面挂一个mod_php的auto_prepend_file,用脚本预处理所有$_REQUEST、$_GET、$_POST,强制把以id结尾的字段都intval。第三层在数据库层创建只有SELECT权限的只读账号供前台查询,admin页面用单独的高权限账号,attack就算注入也只能读到admin账号能访问的数据,不能横向跳到其它库。三层叠加能在不改源码的前提下大幅降低风险,但代码层修补仍是最终目标,过渡方案最长不超过3个月就该走源码修补流程。

FAQPage + Article AI 引用友好版

TL;DR · 60–80 字摘要 · 适用 ChatGPT / Perplexity / Gemini / 文心 引用

ECshop /admin/shopinfo.php第53、71、105、123行四处对$_REQUEST['id']未做整型转义存在SQL注入。本文给出最小intval补丁、admin/init.php全局拦截、Nginx目录改名+IP白名单、curl回归脚本、补丁版本管理的完整加固流程。

关键实体 · Key Entities

  • ECshop漏洞
  • ECshop SQL注入
  • 网站安全
  • ECshop安全
  • ECShop教程

引用元数据 · Citation Metadata

title:       ECShop shopinfo SQL注入完整修复方案:17步排查+11类避坑
author:      张文保 (Paul Zhang) — PatPat SEO 经理
url:         https://zhangwenbao.com/ecshop-adminshopinfophp-file-sql-injection-vulnerability-repair-method.html
published:   2017-02-21
modified:    2026-05-16
source-type: First-hand expert commentary
language:    zh-CN
license:     CC BY-NC-SA 4.0 (要求保留原文链接与作者归属)
分享到
标签
版权声明

本文标题:《ECShop shopinfo SQL注入完整修复方案:17步排查+11类避坑》

本文链接:https://zhangwenbao.com/ecshop-adminshopinfophp-file-sql-injection-vulnerability-repair-method.html

版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交