redis缓存未命中回源查库本身不导致sql注入,风险源于业务代码拼接sql或未校验用户输入;穿透是“合法格式但语义不存在”,注入是“非法格式且可执行”,二者需在controller或网关层统一通过类型转换、范围限制、正则白名单等输入校验拦截。

直接说结论:Redis缓存未命中时的回源查库环节,本身不产生SQL注入;真正的风险点在业务代码里拼接SQL或未校验用户输入的 key —— 防穿透和防SQL注入是两件事,但共用同一道防线:输入校验。
为什么缓存穿透和SQL注入常被混谈
很多人看到“恶意key绕过Redis直打数据库”,就下意识联想到SQL注入。其实两者触发路径不同:
- 缓存穿透:请求一个
user:id=999999999这种根本不存在的ID,Redis没命中 → 查DB → DB返回空 → 未缓存空值 → 下次还走DB - SQL注入:请求一个
user:id=1%20OR%201=1(URL编码后为1 OR 1=1),如果DAO层用String.format("SELECT * FROM user WHERE id = %s", id)拼SQL,就会出事
关键区别在于:穿透是「合法格式但语义不存在」,注入是「非法格式且可执行」。同一个参数,可能既引发穿透,又携带注入 payload —— 所以必须在最外层拦截。
回源前必须做的三道输入过滤
所有从客户端来的 key(比如 userId、articleId)在进入 getById() 或任何 DAO 方法前,就得完成校验。不是等 Redis 查完再判,而是先拦住。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 类型强转兜底:用
Long.parseLong(idStr)替代字符串直接传入 SQL;抛NumberFormatException就直接返回 400,不进 DB 层 - 范围限制:比如用户 ID 不可能是负数或超 10 位,加判断
if (id 999999999L) - 正则白名单:对带字母的复合 key(如
order:SN2026ABC),用idStr.matches("^[a-zA-Z0-9_:]{5,32}$")严格匹配,拒绝;、--、UNION等一切可疑字符
缓存空值时别忽略 value 的序列化安全
你决定缓存空结果(比如 redisTemplate.opsForValue().set("user:123456", "", 60, TimeUnit.SECONDS)),这个 "" 看似安全,但要注意:
- 如果后续用
JSON.parseObject(redisValue, User.class)反序列化,而redisValue是攻击者通过其他途径写入的恶意 JSON(如{"@type":"java.lang.Class","val":"javax.naming.InitialContext"}),可能触发反序列化漏洞 - 更稳妥的做法是统一用
null或预定义哨兵字符串(如"__NULL__"),并在业务层显式判断,避免交给泛型反序列化器处理 - 不要把原始错误信息(如
"java.sql.SQLException: Column 'xxx' not found")塞进缓存 —— 这等于向攻击者暴露表结构
布隆过滤器不能替代输入校验
有人想用布隆过滤器(Bloom Filter)提前筛掉 99% 的无效 key,这没错,但它解决不了注入问题:
- 布隆过滤器只判断“这个 key 可能 存在”,它不解析内容,也不做语法检查
-
user:id=1%20OR%201=1可能被布隆过滤器放行(因为 hash 后看起来像合法 ID),但进 DAO 前必须被正则或类型转换干掉 - Bloom Filter 本身有误判率,且初始化需要全量数据 —— 新增数据要及时更新 filter,否则会漏判,反而放大穿透
真正难缠的,从来不是“查不到”,而是“你以为查不到,其实它悄悄执行了别的东西”。输入校验必须落在 Controller 或 Gateway 层,越早越好,别给任何字符串机会碰 SQL 或反射调用。










