document.getElementById 操作 DOM 节点,区分反射型、存储型和 DOM 型三种 XSS 的触发条件,理解 innerHTML 与 textContent 的安全差异,并用 CSP 和 Trusted Types 构建前端 XSS 防御层——这是模块 2 Web 渗透之前,从前端视角理解漏洞成因的最后一环。上一篇你在 PHP 后端代码中追踪污点流向——从 $_GET 到 eval()。本篇视角切换到浏览器端。JavaScript 是唯一运行在用户浏览器中的编程语言——它赋予网页动态交互能力,但同一套 DOM API 也让攻击者能够注入并执行恶意脚本。这就是 XSS(Cross-Site Scripting,跨站脚本攻击),OWASP Top 10 中长期排名前三的 Web 安全威胁。
浏览器是一把双刃剑。JavaScript 能读取页面内容、发送网络请求、操作 Cookie——这些能力让 Web 应用强大,但也让攻击者一旦注入脚本就能窃取用户数据。XSS 的本质是:攻击者的输入被浏览器当作代码执行。与 SQL 注入发生在后端数据库不同,XSS 发生在前端浏览器——后端代码再安全,如果前端把用户输入直接写进 HTML,XSS 就成立了。
| 维度 | SQL注入(后端) | XSS(前端) |
|---|---|---|
| 发生位置 | 数据库引擎 | 浏览器渲染引擎 |
| 注入语言 | SQL | JavaScript / HTML |
| 攻击者获取 | 数据库内容 | 用户Cookie、会话令牌 |
| 根本防御 | PDO预处理 | 输出编码 + CSP |
JavaScript 当前 ECMAScript 2025(ES2025)规范已于 2026 年 6 月正式发布,所有现代浏览器均已支持 ES2024 全部特性。本篇代码基于浏览器原生 JavaScript,不依赖任何框架——理解原生 DOM API 是审计前端漏洞的前提,React/Vue 等框架的 XSS 问题最终都归结到 DOM 操作的底层行为。
你已经熟悉 Python 和 PHP 的变量概念。JavaScript 变量声明有三个关键字:var(旧版,已不推荐)、let(可变)、const(不可变)。但对安全审计而言,语法不是重点——DOM(Document Object Model)才是。DOM 是浏览器把 HTML 解析成的树形对象结构,JavaScript 通过 DOM API 操作这棵树的节点,改变页面内容、样式和行为。
在 Kali 的 Firefox 浏览器中按 F12 打开开发者工具,切到 Console(控制台)标签页,可以直接输入 JavaScript 代码执行。先看最基本的 DOM 操作——获取元素并修改内容:
第 5 行 textContent 把内容当作纯文本——即使内容包含 <script> 标签也会原样显示,不会执行。第 8 行 innerHTML 把内容当作 HTML 解析—— <b> 会被解析为加粗标签。如果内容来自用户输入,innerHTML 就是 XSS 漏洞的入口。
| 属性 | 解析方式 | XSS风险 | 安全等级 |
|---|---|---|---|
textContent |
纯文本,不解析HTML | 无 | 安全 |
innerHTML |
解析为HTML标签 | 高(注入点) | 危险 |
outerHTML |
替换整个元素HTML | 高(注入点) | 危险 |
.innerHTML、.outerHTML、document.write()、eval()、setTimeout()(传字符串时)——这五个是 DOM XSS 的"危险汇聚点"(sink)。如果用户可控数据流到了这些 sink 且未经编码,XSS 就成立了。你已经理解了 innerHTML 为什么危险。现在把这个认知扩展到三种 XSS 类型。XSS 按恶意脚本的来源和持久性分为三类,每一类的攻击路径和检测难度不同。安全工程师必须能快速判断目标存在的是哪种类型——这决定了后续的利用方式和防御方案。
| 类型 | 数据来源 | 持久性 | 触发方式 |
|---|---|---|---|
| 反射型 | URL参数 | 非持久 | 用户点击恶意链接 |
| 存储型 | 数据库 | 持久 | 用户打开含恶意内容的页面 |
| DOM型 | 前端JavaScript | 非持久 | 前端数据处理不经过服务器 |
上一篇 PHP 的 input.php?name=<script>alert(1)</script> 就是反射型 XSS——恶意 payload 在 URL 中,服务器把它拼到 HTML 返回给浏览器。存储型 XSS 更危险:攻击者把恶意脚本提交到评论区、个人资料等存储功能中,每个访问该页面的用户都会中招——不需要点击链接,打开页面就触发。
DOM 型 XSS 是三者中最隐蔽的。它的特点是:恶意数据完全不经过服务器——从 URL 读取到渲染全在前端 JavaScript 中完成。服务器日志中看不到 payload,传统 WAF(Web 应用防火墙)检测不到。看一个最简单的 DOM 型 XSS 代码:
访问 dom_xss.html#<img src=x onerror=alert(document.cookie)> 时,location.hash 读取 # 后面的内容,直接写入 innerHTML。浏览器解析 <img> 标签,src=x 加载失败触发 onerror 事件执行 alert(document.cookie)——整个攻击过程没有一行请求到达服务器。
上一章你看到了 DOM 型 XSS 的基本原理——location.hash 到 innerHTML。本章用上一篇 PHP 中"污点追踪"的思路,分析一个更真实的前端漏洞场景。前端 JavaScript 的污点追踪链是:用户输入(source)→ 变量传递 → DOM sink。
先写一个搜索页面——用户在搜索框输入关键词,页面显示搜索结果。这是 Web 应用中最常见的功能,也是反射型 XSS 和 DOM 型 XSS 的高发场景:
这段代码的污点追踪链:input.value(source)→ keyword 变量 → 字符串拼接 → innerHTML(sink)。如果用户在搜索框输入 <img src=x onerror=alert('XSS')>,这段 HTML 会被 innerHTML 解析并执行 onerror 中的 JavaScript。
innerHTML 是 XSS 的经典组合。用户输入中的 <script> 标签虽然不会被 innerHTML 直接执行(HTML5 规范限制),但 <img onerror>、<svg onload> 等事件处理器能绕过此限制textContent 或 createTextNode 替代 innerHTML,或在写入前用 encodeURIComponent() 编码修复方案有两种。最简单的是把 innerHTML 换成 textContent——内容会被当作纯文本,不解析 HTML。如果确实需要渲染 HTML 结构(比如搜索关键词需要高亮),则用 DOM API 安全构建节点:
修复版用 createElement 创建 <p> 元素,用 textContent 设置内容——用户输入中的 <script> 会被当作纯文本显示,不执行。appendChild 把节点安全插入 DOM 树。这种"先创建节点再设置文本"的模式是前端安全操作 DOM 的标准写法。
location.hash、location.search、document.referrer、input.value、localStorage。再找 sink(危险汇聚点),常见的有 innerHTML、eval()、document.write()、setTimeout()。如果 source 和 sink 之间没有编码或过滤,漏洞成立。你已经掌握了用 textContent 替代 innerHTML 的修复方式。但实际项目中不可能逐一替换所有 DOM 操作——大型前端项目有数千处 DOM 写入。业界建立了三层 XSS 防御体系:输出编码(应用层)、CSP 内容安全策略(浏览器层)、Trusted Types(DOM API 层)。
第一层:输出编码。在用户输入写入 HTML 前把特殊字符转义——< 变成 <、> 变成 >,浏览器就不再把它们解析为标签:
第二层:CSP(Content Security Policy,内容安全策略)。CSP 通过 HTTP 响应头 Content-Security-Policy 告诉浏览器哪些来源的脚本可以执行——即使攻击者注入了 <script> 标签,CSP 也会阻止其执行。在 PHP 中设置 CSP 头:
这个 CSP 策略做了三件事:default-src 'self' 只允许加载同源资源、script-src 'self' 'nonce-r4nd0m' 只允许同源脚本和带特定 nonce 的内联脚本、style-src 'self' 只允许同源样式。攻击者注入的 <script>alert(1)</script> 没有 nonce 值,浏览器拒绝执行。
第三层:Trusted Types。这是 2026 年 2 月正式达到 Baseline 状态的浏览器原生 API——Firefox 在 2026 年 2 月完成支持后,所有主流浏览器均已实现。Trusted Types 从 DOM API 层面根治 DOM 型 XSS:它强制所有 DOM sink(innerHTML、outerHTML 等)只接受"受信任类型"对象,不再接受原始字符串:
Trusted Types 把"开发者需要自觉编码"变成了"浏览器强制编码"——不经过 createPolicy 定义的策略函数,任何字符串都无法写入 innerHTML。这是从"依赖开发者自觉"到"平台强制安全"的范式转变——与 PHP 的 PDO 预处理从根本上防止 SQL 注入是同一种思路。
本篇所有 XSS 漏洞复现必须在本地环境进行——自己电脑上的浏览器、本机启动的 PHP 服务器、DVWA 靶场。XSS 的 payload <script>alert(document.cookie)</script> 在真实网站上意味着窃取用户 Cookie——如果在他人网站上注入这段代码,即使只是弹窗测试,也已触犯《刑法》第 285 条。合法练习方式:本机 127.0.0.1、DVWA 靶场、PortSwigger Web Academy 免费实验室。
textContent 把输入内容显示在页面上。然后在输入框中输入 <script>alert(1)</script>,验证是否会执行。再把 textContent 换成 innerHTML,输入相同内容观察差异。search_vuln.html,让它从 location.search(URL 参数 ?q=)读取搜索关键词而非输入框。然后构造一个包含 onerror 事件的 XSS payload URL,在浏览器中验证触发。最后用 encodeURIComponent() 编码修复。这是模块 2 Burp Suite 拦截 URL 参数修改的预热练习。textContent 和 createTextNode 修复。提示:搜索"DOMPurify"了解业界标准的 HTML 消毒库。