XSS 不是弹窗,是数据流进了错误的位置
弹窗只能证明"能执行",证明不了"为什么能执行"。XSS 不是字符漏了过滤,是输入进了不该进的位置。
过滤了 script,为什么还能执行
后端过滤 script 与 javascript:,前端拼进页面——挡字符,不挡位置。同一个输入落在文本里是字符串,落进属性里就是属性,落进 <script> 的引号中间就是代码。位置有三种,过滤越细,绕开的路径越多。
过滤看字符,防御看位置。
三种上下文
| 输入落点 | 上下文 | 正确编码 |
|---|---|---|
| 标签之间的文本 | HTML 文本 | HTML 实体编码 |
| 标签属性值 | 属性 | 属性编码,引号成对 |
<script> 内的字符串 | JavaScript | JS 转义,或改用数据绑定 |
同一段昵称,进文本节点安全,进属性值可能断开属性,进脚本字符串可能闭合引号。顺序固定:先定输出点,再选编码;反过来先写过滤再找位置,一定漏。
编码规则:编错位置等于没编
HTML 实体编码只对文本节点有效。写进 <script> 里,脚本上下文不解码实体,实体成为字面量。
属性编码的前提是引号成对:值不带引号时,一个空格就能结束属性,后面被解析成新属性。
JS 转义处理引号、反斜杠、换行,不是 HTML 特殊字符;在脚本里做 HTML 编码等于没做。
// 危险:直接赋值 HTML
container.innerHTML = userInput;
// 危险:属性拼接,值带引号即断开
const html = "<a href='" + userUrl + "'>打开</a>";
// 危险:脚本字符串拼接,值带引号即闭合
const script = "<script>var name = '" + userName + "';<\/script>";
三段都出在拼接。改法是把输入放回"值":textContent、DOM API 设属性、放 dataset。
三类 XSS:反射、存储、DOM
三类的区别只在"数据在哪一步进入执行":
- 反射:服务端把请求值直接写回响应,浏览器解析时执行。
- 存储:值先进库,之后在另一个页面渲染,提交点与触发点不在一处。
- DOM:服务端响应里没有这个值,是客户端脚本自己从
location、referrer、postMessage取出后写进 HTML 解析器。
第三类最易漏,因为服务端响应是干净的。判据往前一步:前端有没有把 URL 或消息直接交给 HTML 解析器。
// hash 内容直接进 HTML
document.getElementById("panel").innerHTML = location.hash.slice(1);
三类不要混着谈。混着谈会把"服务端转义"套到 DOM 类上,而它根本不经过服务端。
CSP 能挡什么、挡不住什么
挡的是"新引入的脚本源":内联脚本(无 nonce 或 hash)、白名单外的域、eval。
挡不住四种:白名单里的 CDN 或 JSONP;注入的标记与属性照样生效;Report-Only 只报不拦;unsafe-inline 一加,内联防线就没了。
CSP 是纵深,不是输出编码的替代品。 把它当"上了就安全",等于把第二道锁当第一道门。
防御
四条,按重要性排:
- 按输出点选编码:文本、属性、脚本、URL,各一套。
- 不用
innerHTML处理用户内容,改用textContent或 DOM API。 - 富文本走白名单:标签与属性都列,不用黑名单。
- CSP 只做纵深,别随手关掉模板引擎的自动转义。
验证三动作:Elements 看实际生成的 DOM;curl 看原始响应有无转义;curl -sI 看 CSP 头。
# 看原始响应有没有被转义(换成自己提交的标记)
curl -s 127.0.0.1 | grep -n "<标记>"
# 看有没有第二道防线
curl -sI 127.0.0.1 | grep -i "content-security-policy"
三项对应渲染结果、服务端行为、第二道防线;对不上,问题在编码位置。
按形态找优于按关键字找,站内《危险函数检索为什么在真实项目里失效》拆过同一件事。站内文章 链路视角见攻击链路,组件层见组件解析。
判断只有一句:这个输入,最后落在哪个上下文里。 位置对了编码才有意义;位置错了,过滤再多也只是在猜字符。
本文仅用于安全学习与授权环境下的测试。文中所有示例均为本地或授权靶场环境,不构成对任何真实系统的测试指引。
仅限授权测试,严禁非法滥用。
读到这里,说明你在认真看。加入社群,与我们一起深挖底层。
或点此直接进入「星巡底层研习」 →
扫码加入交流社群