PHP安全编码三大致命坑:缓存工程师20年避坑血泪总结
|
去年十一月二十七号,凌晨三点二十,我正蹲在IDC机房角落啃冷泡面,汤里浮着半片没化开的脱水葱——监控屏右下角贴了块泛黄胶布,盖住去年夏天烧糊的LED像素点,现在还漏着幽幽蓝光。空调轰鸣声突然低了半拍,我抬头看了眼墙上挂钟,秒针卡顿半秒,又咔哒跳过去。这声音我熟,比自己心跳还熟。 三点半,告警微信弹出来:缓存层雪崩。不是红字,是灰的——Redis连接池耗尽,PHP-FPM子进程全挂成僵尸。我手指刚碰上键盘,客服电话就打进来:“王工,您那边是不是把CDN回源规则删了?”我没接话,她后半句自己吞了回去。隔壁工位老张正往嘴里塞第四包干脆面,嘎吱嘎吱嚼得震耳,他忽然说:“你家缓存KEY里带$_GET参数那块,是不是还没改?” 我原以为只是配置漏刷。结果切到生产库看slowlog,第一行就钉死了:file_get_contents("https://api.xxx.com/data?token=" . $_GET['t'] . "&id=" . $_GET['id'])。那个$_GET['t'],压根没过滤,没urlencode,没isset判断,就那么赤条条喂进curl——而下游服务恰好返回了302跳转,重定向地址里带了个未转义的#号,PHP把整个Location头当字符串塞进了Redis KEY。KEY长2871字节,超长截断,尾部全变成\\x00\\x00\\x00… 但缓存驱动居然没报错,只默默写入。等第二天上午十点流量上来,三台从库全卡死在scan命令里,因为SCAN匹配时遇上\\x00开头的乱码KEY,直接坠入底层哈希表遍历深渊。 窗外雨下得没停过,雨刮器节奏乱了,像我昨天下午在工位上重装composer时手抖敲错的那行vendor/autoload.php路径。值班室泡面桶堆成小塔,最顶上那桶印着“红烧牛肉味”,塑料盖拧得死紧,我试了三次才抠开——可这跟缓存有啥关系?没关系。但人总得攥点东西才敢喘气。
文章配图,仅供参考 老板凌晨四点零七分在运维群甩了张图:一张截图,显示某次Git commit里写着“临时注释掉XSS过滤,等前端联调完再开”,时间戳是上个月十八号。底下没人接话。五点十五分,他又补一句:“@我,这个‘临时’,临了几天?”我盯着手机屏,指尖冰凉,回了个“马上”,却点错了,发成“马—上—”,三个字拆成三行,像三截断骨头。 第二个坑,是缓存穿透的防御逻辑写反了。我们用布隆过滤器挡空KEY,可上线前压测时发现误判率高,小李随手把bf_add()和bf_might_contain()的调用顺序对调了——不是“先查再加”,是“先加再查”。所以所有非法KEY第一次访问时都被强行塞进布隆过滤器,之后永不过期。那天晚上十二万次恶意探测,全部被布隆标记为“可能存在”,接着冲向DB。DB没崩,MySQL主从延迟干到八百秒,而我们的告警脚本还在检查Seconds_Behind_Master < 60…… 第三个坑?缓存更新。代码里写着“先删Redis,再更新DB”,看着多干净。可那天晚上十一点四十三分,DB主库执行了一次alter table加字段,事务卡了四十来分钟。而上游PHP进程在删完缓存后,等不到DB响应,直接timeout重试——删缓存、删缓存、删缓存……中间还穿插着两个同事手动跑shell清缓存,没人同步操作记录。等到DB恢复,缓存早空了两轮,旧KEY全失效,新KEY根本没机会写进去。用户刷出来的全是NULL,不是404,是真正的null——PHP echo null,页面一片空白,白得瘆人。 说到白,想起上回搬家。房东非说阳台瓷砖缝里霉斑是我留下的,拿棉签蘸84擦给我看。结果擦掉一层,底下又是黑的,再擦,水泥粉簌簌掉,最后露出钢筋。这跟我们查问题一样,挖三层,底下还是脏的。 谁的孩子打来电话?是老张的闺女,半夜两点喊爸爸,说梦见机房地板塌了,掉进黑洞里全是缓存KEY。老张挂了电话,摸出烟盒,发现最后一支被压扁了,烟丝从锡纸缝里漏出来,像数据流溢出缓冲区。我看着那摊灰白烟丝,忽然想到——其实最要命的从来不是KEY设计,不是布隆误判,也不是删缓存时机。是那个用file_get_contents拼URL的人,在写这行代码时,窗外也下着雨,电脑右下角时间也是凌晨三点二十。他没点进PHP手册看filter_var()的文档,没搜CVE-2019-15052,也没管curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false)到底有没有生效。他就想快点下班。 我到现在也没想明白,为啥那个302跳转的Location头里,会混进一个没encode的#号。下游团队说是Nginx配置问题,Nginx说上游HTTP Client没遵循RFC,RFC说PHP的http_build_query()默认不处理fragment……这链子能扯到太平洋海底光缆。但有一点我笃定:二十七号凌晨三点二十二分,监控屏蓝光一闪,就是那个\\x00\\x00\\x00……第一次被写进磁盘的时刻。 今天早上七点,运维群里又飘过一条消息:新来的实习生问,“缓存KEY能不能用base64_encode(serialize($_REQUEST))?”没人回。我盯着这行字看了三分钟,面汤早凉透了。窗外雨停了,但空调没修好,机柜风扇呼呼吼着,像一整排濒死的蜂巢。 明天先把告警阈值调了。不调CPU,不调内存,就调Redis slowlog里那个大于500ms的scan命令触发频率——设成每分钟最多一次,超过就打铃。至于base64那行?我删了聊天记录。 (编辑:站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |


全平台缓存优化:多端适配网站资源加速方案