|
上周三凌晨两点,网站运维群里弹出一条告警:某电商后台的漏洞订单导出接口响应时间从200ms飙到8秒。我爬起来连上VPN,修复顺手打开Nginx日志,报告发现同一个IP在十分钟内请求了四千多次导出操作。网站事情不复杂——有人拿脚本在刷数据,漏洞但根源不在脚本,修复而在那个接口压根没做频率限制。报告 这就是网站典型的“功能上线时忘了安全”的案例。开发当时只想快速支持运营导出,漏洞加了个GET参数就能跑,修复连用户角色都没校验。报告我翻了下代码,网站更离谱的漏洞是,那个接口用字符串拼接SQL,修复把用户传入的日期直接塞进查询里。等于说,只要有人构造一个带'; DROP TABLE orders; --的日期参数,整个订单表就没了。好在MySQL用户权限没给DROP,但注入点真实存在。 修复其实不费劲。第一件事,把接口的权限从“登录即可”改成“仅管理员+IP白名单”。第二,用预编译语句重写查询,日期参数强制走DateTime::createFromFormat校验,格式不对直接返回400。第三,加个简单的Redis计数器,同一个用户每分钟最多导出5次,超过就锁10分钟。前后不到两小时,代码改完推上预发环境,用sqlmap跑了一遍,注入点消失,压测脚本再刷也被限流挡在外面。 但真正让我头疼的是另一个漏洞,藏在文件上传功能里。用户上传头像时,后端只检查了Content-Type,没验文件内容。攻击者把.php文件改成.jpg,再伪造MIME类型为image/jpeg,就能传上去。更巧的是,上传目录恰好有执行权限。我试了一下,传了个shell.jpg,里面写<?php system($_GET['c']); ?>,然后访问/uploads/shell.jpg?c=id,服务器直接返回了uid=33(www-data)。那一刻后背有点凉。 这个修复要麻烦些。不能只靠前端校验,后端得用finfo_file读真实MIME,再配合getimagesize确认是合法图片。上传后重命名成随机哈希,去掉扩展名,存到Web根目录之外。最后在Nginx里加一条规则:location ^~ /uploads/ { deny all; },所有文件必须通过PHP脚本读取输出。改完再测,传PHP文件直接报“不是有效图片”,传图片马也被重命名成无扩展名文件,访问返回403。 还有个小问题差点漏掉。后台的密码重置功能,token生成用的是md5(time())。我写了个脚本,在同一个秒级时间窗口内生成一百个token,跟目标token比对,居然撞上了。这玩意儿叫“时间竞争漏洞”,利用成本极低。改成random_bytes(32)之后,token长度翻倍,熵值足够,暴力枚举基本没戏。 修完这些,我顺手扫了一遍全站。发现三个老接口还在用eval()处理模板,两个API返回了堆栈信息,还有一处SSRF——图片抓取功能没限制内网IP,能读169.254.169.254的元数据。这些不是新漏洞,是历史遗留。每次加新功能,旧代码就像地基里的裂缝,平时看不见,一震就露出来。 最后说个观察。很多团队修漏洞喜欢“打补丁”——哪漏补哪,不查同类。比如修了SQL注入,但没检查所有DAO层;修了文件上传,但没管其他上传点。我习惯修完一个,用grep搜一遍类似模式,把相关代码全过一遍。这次修完导出接口,顺手把另外六个导出功能全加了限流和预编译,省得下次半夜再被叫醒。 网站安全没有一劳永逸。今天修完,明天新代码可能又带进来。能做的就是让修复速度比攻击速度快一点,再快一点。 |