缓存键拼接用户输入必然导致缓存投毒,必须用哈希(如md5)或白名单清洗处理;tag名禁止动态构造,须硬编码或查表;file驱动比redis更危险,生产环境需禁用default_return。

缓存键里拼接用户输入就是高危操作
ThinkPHP 不会对 Cache::get() 或 Cache::tag() 传入的键做任何过滤,直接拼接 input('id')、$request->param('slug') 等变量,等于把攻击面完全暴露给请求参数。攻击者只需发一个 ?id=123%00_tag:admin,就可能让 File 驱动写到非法路径,或让 Redis 驱动在后续 KEYS 扫描时解析出错。
常见错误写法:
Cache::get('user_' . input('id'))Cache::tag(input('category'))->set(...)Cache::get('post_' . $request->route('id'))
这些都不是“可能出问题”,而是只要参数可控,就必然可投毒。关键不是驱动类型,而是键生成逻辑本身没设防。
必须用哈希或白名单处理原始输入
别试图删掉点号、斜杠、NUL 字符——黑名单永远漏得比补得快。真正有效的只有两种方式:
- 哈希归一化:
Cache::get('user_' . md5(input('id'))),简单、固定长度、无歧义,适合绝大多数场景 - 白名单清洗:
preg_replace('/[^a-zA-Z0-9_]/', '', input('id')),再拼接,仅用于需要人工可读键名的调试环境
注意:md5() 不是为了防碰撞,而是为了消除原始输入的语义和结构。哪怕输入是 ../../etc/passwd,哈希后也只是一个 32 位字符串,File 驱动不会再把它当路径解析。
Tag 名禁止动态构造
Cache::tag() 的底层依赖键名格式约定,比如 user:123 属于 user tag。如果 tag 名来自用户输入,Cache::tag(input('t'))->set() 就等于把整个 tag 组的清理、失效权限交给了攻击者。
正确做法只有一条:
- 硬编码或查表:
Cache::tag(['user', 'profile'])->set(...)或Cache::tag($allowed_tags[$type] ?? 'default')->set(...)
绝不能出现 input()、$request->param()、$_GET 直接进 tag() 参数的位置。
不同缓存驱动的风险差异要心里有数
cache.type=file 比 redis 更危险,因为 File 驱动会把键名直接映射为文件路径。即使 ThinkPHP 内部做了基础转义,也无法覆盖所有绕过路径穿越的技巧(如 %00、././、Unicode 归一化等)。
redis 虽不报错,但运维脚本、监控工具、甚至某些客户端 SDK 会按冒号分隔 tag 和 key,若你键里含 : 或空格,会导致 SCAN 失效、统计错乱、误删整组缓存。
检查当前配置:config('cache.type'),生产环境务必禁用 cache.default_return,否则缓存未命中时返回默认值,反而掩盖投毒已发生的事实。
真正容易被忽略的,是开发者习惯性把缓存键当成“普通字符串”来拼——它其实是一条通往存储层的指令。只要键名含用户上下文,就必须经过哈希或白名单这道关,没有例外,也没有“小数据没关系”这种侥幸。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











