为什么必须分上下文
同一段用户数据可能落进不同位置:标签之间的文本、属性值、链接地址、脚本片段、样式规则。每个位置的解析规则不同,需要转义的字符也不同:文本内容只需处理 & 与尖括号;属性值还需要处理引号(未加引号的属性限制更严);URL 位置要防协议与百分号问题;脚本上下文则要用相应语言的转义规则。用一套规则包打天下,就会出现「已经转义了却仍能注入」的情况。
基本信息
| HTML 文本内容 | 转义 & 与尖括号 |
|---|---|
| 属性值(带引号) | 额外转义引号,属性值必须用引号包裹 |
| URL 位置 | 百分号编码,并校验协议白名单 |
| 脚本上下文 | 使用对应语言的转义,优先改为传递数据而非拼接代码 |
| 样式上下文 | 避免拼接用户数据,必要时做严格数值校验 |
| 通用原则 | 在输出边界做一次、按上下文选择规则的转义 |
典型使用场景
- 评论与昵称展示:文本位置按文本规则转义
- 链接与图片地址:校验协议、必要时编码,禁止直接拼接脚本协议
- 模板渲染:了解模板引擎是否自动转义,避免手动重复转义或漏转
- 富文本编辑:先白名单过滤标签与属性,再按位置输出
- 前端动态插入:使用设置文本内容的接口而不是直接写入标记
正确与错误示例
正确与错误示例
# 错误:只做宽松过滤,标签仍可注入
# <div>欢迎 {{ name }}</div> 而 name 未转义 → 可插入 <script> 或属性事件
# 正确:文本位置转义 & 与尖括号
# <div>欢迎 <b>张三</b></div>
# 正确:属性值使用引号包裹并转义引号
# <a title="" onmouseover="x"">链接</a> # 用户内容里的引号被转义
# 正确:URL 位置校验协议白名单,拒绝脚本协议
python -c "import html; print(html.escape('" onmouseover="alert(1)'))" # 转义属性注入
python -c "import urllib.parse as u; print(u.quote('a b&c'))" # URL 位置使用百分号编码上下文与转义要求
| 输出位置 | 需处理 | 常见错误 |
|---|---|---|
| 标签之间的文本 | & 与尖括号 | 漏转 & 导致实体与内容混淆 |
| 属性值(键值对) | 引号与尖括号 | 属性未加引号,空格即可逃逸 |
| URL 参数与路径 | 百分号编码与协议白名单 | 未校验协议,允许脚本协议执行 |
| 脚本片段 | 按语言转义或改传数据 | 直接拼接字符串造成代码注入 |
| 样式值 | 严格数值或枚举校验 | 拼接用户数据造成样式注入 |
黑名单过滤不可靠:只删除某个标签名或事件名,攻击者可用其他等价写法绕过。正确顺序是「按上下文转义输出」加上「必要的白名单过滤」,而不是在输入端做字符替换。
模板引擎与前端框架通常默认转义,「关闭自动转义」这一操作要审慎对待:每次显式关闭都必须确认数据来源可信,或已经过白名单净化,否则就等于把注入风险交还给调用方。
转义与净化是两件事:转义让内容「原样显示」,净化决定「允许显示哪些格式」。富文本场景两者都要有——先用白名单净化允许的标签与属性,再在输出位置按上下文转义剩余内容。