CRLF 注入的原理
HTTP 响应头以回车换行(CRLF)分隔,把用户输入直接放进响应头值(如 Location、Set-Cookie 或自定义头)时,输入中的换行符可以提前结束当前头部并注入新头部甚至响应体。后果包括响应拆分、缓存投毒与会话固定。现代框架与运行时多数会直接拒绝含 CR/LF 的头值,但拼接型代码和老旧运行时仍需自行加固。
基本信息
| 涉及字符 | CR(\r,%0d)与 LF(\n,%0a) |
|---|---|
| 触发位置 | 响应头赋值处,如 Location、Set-Cookie、自定义头 |
| 可能后果 | 响应拆分、缓存投毒、会话固定 |
| 缓解方向 | 剔除换行、改用白名单、升级运行时 |
| 自检方式 | 检索代码中拼接响应头的位置并逐处复核 |
典型使用场景
- 重定向:Location 头由用户参数拼出(与开放重定向常同时出现)
- 设置 Cookie:名称或值来自用户输入时
- 自定义头回显:把请求信息回写到响应头用于调试
- 下载文件名:Content-Disposition 中拼入用户提供的文件名
- 网关与中间件:自研的反向代理头部处理逻辑
风险与修复示例
风险与修复示例
风险:response['Location'] = user_input
修复:剔除 \r 与 \n,或改为仅允许站内路径白名单
框架层:主流现代框架直接抛错拒绝含 CR/LF 的头值
自研网关:对头部值做字符白名单校验,而不是只做长度限制
下载文件名:去除换行并按规范做编码,避免头部被截断自检要点
自检要点
检索代码中拼接响应头的位置(Location、Set-Cookie、Content-Disposition)
覆盖自定义头与错误页头信息
检查框架与运行时版本:老版本可能不默认拦截
结合缓存层评估:共享缓存被污染的影响面远大于单次响应
复核网关与中间件:自研头部处理逻辑最容易漏检常见入口与检查点
| 入口 | 检查点 | 加固方式 |
|---|---|---|
| 重定向 | 跳转参数是否进入 Location | 相对路径校验 + 剔除换行 |
| Cookie | 名称与值是否来自输入 | 固定名称,值做白名单 |
| 下载文件名 | 是否拼入 Content-Disposition | 文件名净化并按规范编码 |
| 调试头 | 是否回写请求信息 | 改为记录日志,不回写头部 |
只处理 %0d%0a 这类字面量不足以保证安全:二次编码或字符归一化后仍可能还原为 CR/LF,校验应在解码与规范化之后,并以字符白名单为准。
老版本语言运行时可能不默认拦截换行符,升级运行时本身也是缓解手段之一;但升级不能替代代码层的输入校验,两者要一起做。