主题
JavaScript:DOM、同源策略与 XSS 的土壤
这是第二阶段最重要的一课。HTML 是骨架、CSS 是皮肤,JavaScript 才是让页面"动起来"的东西——它也是 XSS 的执行者、CSRF 的帮凶。其中同源策略你必须吃透:后面所有浏览器安全的话题都建立在它之上。
一、看懂 payload 需要的最小语法
你不需要成为前端工程师,只要能看懂"一段 payload 在干什么":
js
const site = 'https://www.myxbw.cn' // 常量:定义后不能再赋值
function greet(who) { return '你好,' + who } // 函数;字符串用 + 拼接
greet('同学') // 调用它再看一段真实的 XSS payload,你现在能读懂多少?
html
<img src="x" onerror="alert(document.cookie)">它的意思是:加载一张不存在的图片(src="x" 必然失败)→ 触发 onerror → 执行 alert(document.cookie),把当前页面的 Cookie 弹出来。JavaScript 就是 XSS 的实际执行者,所以防 XSS 先得看得懂 JS。
二、DOM:JavaScript 手中的那棵树
上一课我们区分了 HTML(文本)和 DOM(内存里的树)。JS 正是操作那棵树的唯一工具:
| 常用操作 | 写法 | 作用 |
|---|---|---|
| 用选择器找 | document.querySelector('.vp-doc a') | 按 CSS 选择器语法找第一个 |
| 改文字 | el.textContent = '你好' | 安全,纯文本 |
| 改 HTML | el.innerHTML = '<b>你好</b>' | 危险,会解析成标签 |
| 读取当前地址 | location.href、location.hash | 参数常从这里进 |
记住最后一行:location.hash(网址里 # 后面的部分)是 DOM 型 XSS 最常见的输入来源,因为它根本不会发给服务器,服务器端的过滤完全看不到它。
三、innerHTML 为什么危险
这是你以后最常看到的危险写法:
js
const out = document.getElementById('out')
out.innerHTML = '欢迎你,' + location.hash.slice(1)innerHTML 的工作流程是"把这段字符串当成 HTML 重新解析一遍"。既然当成 HTML 解析,字符串里的 <img onerror=...> 自然就被解析成真标签、真的执行起来。对照两种写法的差别,就是理解 XSS 的一半:
| 写法 | 处理方式 | 遇到 <img onerror=alert(1)> |
|---|---|---|
el.textContent = s | 当纯文本 | 原样显示成文字,安全 |
el.innerHTML = s | 当 HTML 解析 | 真的执行弹窗,危险 |
四、事件:是什么在触发你的代码
事件就是"什么时候执行"。两种写法:
- 写法一(内联属性):
<button onclick="alert(1)">点我</button>—— 写在 HTML 里,注入时最容易被塞进去 - 写法二(JS 绑定,推荐):
el.addEventListener('click', fn)—— 结构与行为分离,更干净
最常被 payload 利用的事件:
| 事件 | 触发时机 |
|---|---|
onerror | 图片等资源加载失败——最常用的免 <script> 载荷 |
onclick / onmouseover | 用户点一下 / 鼠标划过 |
onfocus | 元素获得焦点,常配 autofocus 自动触发 |
onerror 之所以受欢迎,是因为它不需要 <script> 标签就能执行代码,很多只过滤 <script> 的"防护"在它面前形同虚设。
五、AJAX 与 fetch:页面不刷新也能发请求
AJAX 的意思是"用 JavaScript 在后台发 HTTP 请求,拿到数据后局部更新页面"。现代写法是 fetch:
js
fetch('/api/user?id=1', { credentials: 'include' })
.then(res => res.json()).then(data => console.log(data))credentials: 'include' 表示"把 Cookie 一起带上";后面的 .then 负责把响应解析成 JSON 再交给下一段代码。老写法叫 XMLHttpRequest,你在 Network 面板里看到的 XHR / Fetch 类型请求就是它们发的。安全含义:接口是暴露给脚本的——凡页面能调的接口,攻击者写三行 JS 也能调,所以每个接口都必须在服务端做权限校验,不能指望"前端藏起来用户就看不到"。
💡 动手实验 1:打开
https://www.myxbw.cn,按F12→ Console,逐行输入并回车:jsdocument.querySelectorAll('a').length document.cookie第二条很关键:如果返回空字符串,说明这个站的 Cookie 带了
HttpOnly(或者根本没设 Cookie);如果返回了内容,说明 JS 能读到它——那么一旦发生 XSS,攻击者也能读到。把结果记进笔记,这就是一次最小的信息收集。
六、同源策略:浏览器安全的基石
定义:协议、域名、端口三者完全相同才算"同源"。以 https://www.myxbw.cn/page 为基准,判断下表每一行:
| 对比 URL | 同源? | 原因 |
|---|---|---|
https://www.myxbw.cn/other | ✅ | 三者全同 |
https://learn.myxbw.cn/ | ❌ | 域名不同(www vs learn) |
http://www.myxbw.cn/ | ❌ | 协议不同(http vs https) |
https://www.myxbw.cn:8443/ | ❌ | 端口不同 |
它的作用用一句话说清:没有同源策略,你随便打开一个网页,页面里的 JS 就能去读你在网银站的页面内容、读其他标签页的数据。 注意第二行:www.myxbw.cn 和 learn.myxbw.cn 看起来是一家人,在浏览器眼里却是彻头彻尾的两个源。
它限制什么、不限制什么,同样重要:
| 行为 | 是否受限制 |
|---|---|
用 fetch 读另一个源的响应内容 | ✅ 限制 |
| 用 JS 读另一个 iframe 里的 DOM | ✅ 限制 |
用 <img>、<script> 加载另一个源的资源 | ❌ 不限制(所以能外带数据) |
跨源提交一个 <form> | ❌ 不限制(所以 CSRF 成立) |
这张表解释了太多东西:图片能做"数据外带信道"、CSRF 能成功、JSONP 时代能跨域取数据,全都是因为"发请求"和"读响应"不是同一件事。
七、CORS:同源策略的"授权例外"
有些跨源读取是合法的,比如你的前端在 learn.myxbw.cn,接口在 api.myxbw.cn,这时需要服务器主动授权:
text
Access-Control-Allow-Origin: https://learn.myxbw.cn
Access-Control-Allow-Credentials: true- 浏览器会检查响应里有没有这条头,没有就拦截并报 CORS 错误;对于
PUT、DELETE这类方法和自定义头,还会先发一个OPTIONS预检请求问一句"我能发吗" Access-Control-Allow-Origin: *表示"谁都行",但它不能和Allow-Credentials: true同时用,否则等于把门拆了还要求带身份证
安全视角:CORS 配置过宽,就等于主动拆掉同源策略。看到 * 加携带凭证的组合,就是你该警惕的信号。
💡 动手实验 2:打开
https://www.myxbw.cn,在 Console 里依次执行下面两行:jsfetch('/').then(r => console.log('同源请求:', r.status)) fetch('https://learn.myxbw.cn/').then(r => console.log('跨源成功')).catch(e => console.log('跨源被拦截:', e.message))第一条必然成功(同一个源)。第二条看结果:如果 Console 出现红色 CORS 报错,说明同源策略正在工作;如果居然成功,说明
learn.myxbw.cn返回了宽松的 CORS 头——去 Network 面板点开那条请求,看看access-control-allow-origin写了什么。
八、Cookie 的 HttpOnly 与 SameSite
Cookie 是身份的载体,它的两个属性几乎是为你的漏洞学习量身定做的:
| 属性 | 作用 | 防的是什么 |
|---|---|---|
HttpOnly | JS 读不到(document.cookie 里没有它) | XSS偷 Cookie |
Secure | 只在 HTTPS 上发送 | 明文网络中被窃听 |
SameSite=Strict | 完全不允许跨站携带 | CSRF,最严 |
SameSite=Lax | 只允许顶级导航的 GET 带上 | CSRF,浏览器默认值 |
要点:HttpOnly 挡住的是"把 Cookie 偷走",挡不住 XSS 本身——攻击者虽然拿不到 Cookie,但可以直接用你的身份在页面上发请求。所以它是减损措施,不是解药;而 SameSite 是 CSRF 最省力的一道防线。
九、一个 DOM 型 XSS 的一步演示
假设页面里有这么一段:
js
// 页面加载时,把地址栏 # 后面的内容显示到页面上
const out = document.getElementById('out')
out.innerHTML = decodeURIComponent(location.hash.slice(1))你只要访问 https://example.com/page#<img src=x onerror=alert(1)> 这样的地址,location.hash 就会取到那段载荷,赋给 innerHTML 后浏览器把它当 HTML 解析,弹窗随之执行。整个过程中请求没有打到服务器,服务器日志里干干净净——这正是 DOM 型 XSS 与反射型最大的区别:载荷只在浏览器里,服务器与防护设备完全看不见。
💡 动手实验 3:仍然只在自己的站点上做。打开
https://www.myxbw.cn,在 Console 里执行:jsconst d = document.createElement('div') d.innerHTML = '<img src=x onerror="alert(1)">' document.body.appendChild(d)你会看到弹窗——这就是一次成功的 XSS,只是没有经过服务器。接着刷新页面,把同一段代码里的
innerHTML换成textContent再执行一次:这次页面上只会显示一行字面文字。亲手对比这两次结果,比读十篇文章都有用。
十、防御:输出编码 + CSP
1. 输出编码(最根本)——把数据写进页面前,按它所在的上下文转义:HTML 正文里转义 <、>、&;属性里还要转义引号;URL 里做 URL 编码。核心原则是永远不让用户数据被当成代码。React、Vue 这类现代框架默认帮你做了这件事,所以"用框架"本身就是一种防护,而手写字符串拼接正是风险源头。
2. CSP(内容安全策略)——一道响应头形式的白名单:
text
Content-Security-Policy: default-src 'self'; script-src 'self'意思是"页面只许加载本站资源,脚本只许从本站来"。即使攻击者成功注入代码,浏览器也会拒绝执行。CSP 是纵深防御的第二道墙,不能替代输出编码。
另一个习惯:能不用 innerHTML 就不用,必须插入 HTML 时先用净化库(如 DOMPurify)过滤。
小结
- JS 语法看懂即可;payload 的执行者永远是 JavaScript
- DOM 是 JS 操作的树;
innerHTML会解析 HTML 因而危险,textContent安全 - 事件是代码的执行入口,
onerror是最常见的"免 script"载荷 fetch让页面不刷新也能发请求,接口对脚本完全开放,权限必须在服务端把关- 同源 = 协议 + 域名 + 端口三者全同;
www与learn子域也不同源;它限制"读",不限制"发"和"加载资源"——CSRF 与外带数据因此成立 - CORS 是服务器主动授权的例外,配成
*加凭证等于自拆防线 HttpOnly防偷 Cookie,SameSite防 CSRF,两者都不能替代根本修复- 防 XSS = 输出编码(根本)+ CSP(纵深防御)
下一课把这三课的工具串起来:F12 开发者工具精要。
📎 本课对应知识库阅读:《知识库里的 XSS 与同源策略主题文章》(把你今天在 Console 里跑出的三次结果对照着看一遍)。