Web安全实战:XSS与CSRF攻击原理及防御方案详解

发布时间:2026/10/19 9:53:03
Web安全实战:XSS与CSRF攻击原理及防御方案详解 1. 为什么前端安全总是“看起来简单出事却很大”Web安全这个话题很多做前端或者全栈的朋友一开始都不太当回事。我刚入行那会儿也一样觉得页面能跑、接口能通、样式没崩任务就算完成了。直到有一次我们内部做安全自查发现一个用户评论模块可以直接把一段脚本塞进数据库然后在所有访问者的浏览器里执行——那一刻我才真正意识到XSS和CSRF这类问题不是“理论上的风险”而是随时可能把整个系统拖下水的定时炸弹。这篇文章想聊的就是Web安全里最经典、也最容易被忽视的两个话题XSS跨站脚本攻击和CSRF跨站请求伪造。我不会只给你背概念而是从攻击原理、实际场景、代码层面的复现到防御方案的取舍一步步拆开讲。适合谁看前端开发、后端开发、测试人员以及任何需要对自己项目安全负责的人。哪怕你之前完全没接触过安全看完也能在自己的项目里落地一套基础防护。先说一个我自己的判断大部分Web安全漏洞不是因为技术太难而是因为开发者默认“用户不会那么坏”。这个默认假设就是所有问题的起点。2. XSS到底是怎么被塞进页面的2.1 从一次“无害”的评论功能说起假设你做了一个博客系统用户可以在文章下面留言。前端拿到留言内容后直接把它渲染到页面上。代码大概长这样// 危险写法直接把用户输入插入innerHTML const comment document.getElementById(comment-input).value; document.getElementById(comment-list).innerHTML div${comment}/div;如果用户输入的是正常文字比如“写得不错”那没问题。但如果用户输入的是img srcx onerroralert(你的Cookie是 document.cookie)这段代码会怎样浏览器解析到img标签时发现srcx加载失败于是触发onerror事件执行里面的JavaScript。结果就是任何访问这条评论的人都会弹出自己的Cookie信息。如果攻击者把alert换成一段发送网络请求的代码那所有访问者的Cookie就会被悄悄送到攻击者的服务器上。这就是最典型的存储型XSS。恶意脚本被存进了数据库每次有人访问页面都会执行。还有一种叫反射型XSS恶意脚本通过URL参数传入服务器直接把它拼接到返回的HTML里用户点开链接就中招。第三种是DOM型XSS整个攻击过程完全发生在浏览器端服务器甚至不知道发生了什么。2.2 三种XSS的差异与危害等级类型触发方式是否存储典型场景危害等级存储型恶意脚本存入数据库是评论、昵称、文章内容极高反射型恶意脚本通过URL参数传入否搜索框、错误提示页中高DOM型前端JS直接操作不可信数据否innerHTML、eval中高存储型XSS之所以最危险是因为它不需要诱导用户点击特定链接只要用户访问正常页面就会中招。一个浏览量大的网站如果存在存储型XSS攻击者可以轻松拿到大量用户的登录凭证。2.3 为什么“过滤特殊字符”不是万能药很多人第一反应是那我直接把、、script这些关键词过滤掉不就行了我试过这条路基本走不通。原因有几个第一过滤规则永远赶不上绕过手法。比如你过滤了script攻击者可以用img onerror你过滤了onerror还有onload、onclick、onmouseover等一大堆事件属性。HTML标签和属性太多了黑名单根本列不完。第二不同上下文需要不同的处理方式。用户输入出现在HTML标签之间、出现在属性值里、出现在JavaScript代码里、出现在CSS里需要的转义规则完全不同。用同一套过滤逻辑处理所有场景必然有漏网之鱼。第三过滤会破坏正常内容。用户想讨论一段代码输入了div你直接过滤掉内容就变得不完整。用户体验和安全之间的平衡需要更精细的方案。我个人的经验是永远不要试图“过滤”恶意内容而是要对所有输出进行“转义”。把用户输入当成纯文本处理而不是当成HTML的一部分。3. CSRF你登录了但操作不是你做的3.1 一个让人后背发凉的场景假设你登录了某银行网站Cookie里保存着你的身份凭证。然后你打开另一个标签页访问了一个看起来人畜无害的论坛。论坛里有一张图片代码是这样的img srchttps://bank.example.com/transfer?toattackeramount10000 width0 height0浏览器加载这张图片时会自动向银行网站发送一个转账请求。因为你的Cookie还在有效期内银行服务器认为这是你本人发起的操作于是转账成功。你什么都没做钱就没了。这就是CSRF跨站请求伪造。攻击者不需要知道你的密码也不需要窃取你的Cookie只需要诱导你访问一个页面就能以你的身份执行操作。3.2 CSRF和XSS的本质区别很多人会把XSS和CSRF搞混其实它们的攻击路径完全不同XSS攻击者把恶意代码注入到你的页面里代码在你的浏览器里执行。核心是“代码注入”。CSRF攻击者诱导你的浏览器向目标网站发送请求请求本身是合法的但发起者不是你。核心是“身份冒用”。用一个生活类比XSS相当于有人偷偷在你家里装了一个窃听器你说什么他都能听到CSRF相当于有人拿着你的门禁卡趁你不注意刷开了你家的门。3.3 为什么Cookie机制天然容易受CSRF影响浏览器在发送请求时会自动带上目标域名下的Cookie。这个设计本身是为了方便——你登录一次后续请求自动携带凭证。但问题在于浏览器不会区分这个请求是你主动发起的还是被其他页面诱导发起的。只要请求的目标域名和Cookie的域名一致Cookie就会被带上。攻击者正是利用这一点在自己的页面上构造一个指向目标网站的请求借助你的浏览器完成身份认证。注意CSRF攻击通常针对的是“状态改变”的操作比如转账、改密码、发帖、删除数据。单纯的读取操作虽然也可能被利用但危害相对小一些。4. 防御XSS从输出转义到内容安全策略4.1 输出转义最基础也最有效的一层防御XSS的第一原则是所有不可信数据在输出到HTML页面时必须根据上下文进行转义。什么叫“不可信数据”简单说就是任何来自用户输入、第三方接口、URL参数的内容。在HTML标签之间输出时需要转义以下字符原字符转义后说明避免被解析为实体起始避免被解析为标签开始避免被解析为标签结束避免突破属性值边界避免突破属性值边界//辅助防止标签闭合如果你用的是现代前端框架比如React、Vue、Angular它们默认会对插值内容进行转义。比如React里的{comment}会自动转义但dangerouslySetInnerHTML不会。Vue里的{{ comment }}会自动转义但v-html不会。框架给了你安全默认值但一旦你主动绕过它责任就回到你身上了。4.2 富文本场景下的白名单策略有些场景确实需要允许用户输入HTML比如富文本编辑器、Markdown渲染。这时候不能简单转义否则所有格式都没了。正确的做法是使用白名单过滤只允许特定的标签和属性通过其他一律清除。我常用的方案是DOMPurify这个库它专门做HTML净化支持自定义白名单。比如import DOMPurify from dompurify; const dirty img srcx onerroralert(1)p正常内容/p; const clean DOMPurify.sanitize(dirty, { ALLOWED_TAGS: [p, br, strong, em, a], ALLOWED_ATTR: [href, title] }); // 结果p正常内容/pDOMPurify会解析HTML移除所有不在白名单里的标签和属性。onerror这类事件属性默认就不在白名单里所以会被直接删掉。实操心得白名单策略的关键是“默认拒绝”。不要想着“禁止哪些危险标签”而是“只允许哪些安全标签”。前者永远列不全后者边界清晰。4.3 Content Security Policy最后一道防线即使转义做得再好也难免有遗漏。这时候CSP内容安全策略可以作为兜底。CSP通过HTTP响应头告诉浏览器哪些来源的脚本可以执行哪些不可以。一个典型的CSP头Content-Security-Policy: default-src self; script-src self https://trusted.cdn.com; style-src self unsafe-inline; img-src self data: https:;这条策略的意思是默认只允许加载同源资源脚本只能从同源和指定CDN加载样式允许同源和内联图片允许同源、data URI和HTTPS来源CSP最强大的地方在于即使攻击者成功注入了脚本只要脚本来源不在白名单里浏览器就不会执行。这相当于给XSS加了一道物理隔离。不过CSP也有代价。配置太严格可能导致正常功能失效配置太宽松又起不到防护作用。我建议先从Content-Security-Policy-Report-Only模式开始只上报违规但不拦截观察一段时间后再正式启用。4.4 HttpOnly Cookie让脚本拿不到凭证对于会话Cookie一定要设置HttpOnly属性。这样JavaScript就无法通过document.cookie读取它。即使XSS攻击成功执行了脚本也拿不到用户的登录凭证。Set-Cookie: sessionIdabc123; HttpOnly; Secure; SameSiteLaxHttpOnly禁止JS读取Secure仅通过HTTPS传输SameSite限制跨站请求携带Cookie这三个属性组合起来能大幅降低XSS和CSRF的影响范围。5. 防御CSRFToken、SameSite与二次验证5.1 CSRF Token最经典的防御手段CSRF攻击之所以能成功是因为浏览器自动携带了Cookie而服务器无法区分请求来源。CSRF Token的思路是在页面中嵌入一个随机生成的Token提交请求时必须带上这个Token服务器验证Token是否匹配。攻击者的页面无法读取目标网站的Token因为同源策略限制所以无法构造出合法请求。实现流程用户访问表单页面时服务器生成一个随机Token存入Session同时嵌入表单隐藏字段。用户提交表单时Token随请求一起发送。服务器比对请求中的Token和Session中的Token一致则通过否则拒绝。form action/transfer methodPOST input typehidden namecsrf_token value随机生成的Token input typetext nameto placeholder收款方 input typenumber nameamount placeholder金额 button typesubmit转账/button /form注意Token必须足够随机且每次会话或每次请求都重新生成。如果Token可预测防御就形同虚设。5.2 SameSite Cookie浏览器层面的天然屏障SameSite是Cookie的一个属性用来控制跨站请求是否携带Cookie。它有三个值值行为适用场景Strict完全禁止跨站携带安全性要求极高的操作Lax允许顶级导航的GET请求携带大多数网站的默认选择None允许跨站携带但必须配合Secure需要跨站嵌入的场景设置SameSiteLax后从其他网站发起的POST请求不会携带CookieCSRF攻击自然失效。这是目前浏览器默认推荐的配置成本低、效果好。Set-Cookie: sessionIdabc123; SameSiteLax; Secure; HttpOnly5.3 二次验证敏感操作的额外保险对于转账、改密码、删除账号这类高危操作光靠Token还不够。我建议加上二次验证比如要求输入当前密码发送短信验证码使用邮箱确认链接这样即使CSRF Token被绕过攻击者也无法完成最终操作。多一层验证多一层安全。5.4 防御方案对比与选型建议防御手段防御目标实现成本兼容性推荐程度输出转义XSS低好必做白名单过滤XSS中好富文本场景必做CSPXSS中高较好强烈推荐HttpOnlyXSS低好必做CSRF TokenCSRF中好必做SameSiteCSRF低较好强烈推荐二次验证CSRF中好高危操作必做实际项目中我通常会组合使用输出转义 HttpOnly CSP防XSSCSRF Token SameSite 二次验证防CSRF。没有哪一层是绝对可靠的但多层叠加后攻击成本会大幅提高。6. 实战排查我踩过的坑和修复过程6.1 一个被忽略的DOM型XSS之前接手过一个老项目后端接口返回的数据都做了转义但前端有一段代码是这样的// 从URL hash中读取参数并渲染 const hash window.location.hash.substring(1); const params new URLSearchParams(hash); document.getElementById(welcome).innerHTML 欢迎 params.get(name);这段代码的问题在于params.get(name)来自URL属于不可信数据但直接赋给了innerHTML。攻击者可以构造一个链接https://example.com/page#nameimg srcx onerroralert(1)用户点开后脚本就会执行。修复方式很简单把innerHTML换成textContentdocument.getElementById(welcome).textContent 欢迎 params.get(name);textContent会把内容当成纯文本处理不会解析HTML标签。这个改动只花了一分钟但堵住了一个真实的漏洞。6.2 CSRF Token失效的排查过程有一次测试反馈说某个表单提交总是报“Token无效”。我排查了半天发现原因是页面被缓存了。用户第一次打开页面时拿到了Token A但页面被浏览器缓存。等Token过期后用户再次提交用的还是缓存的Token A而服务器已经生成了新的Token B。解决方案有两个在表单页面设置Cache-Control: no-store禁止缓存。Token不存Session而是用无状态的方式生成比如JWT并设置合理的过期时间。最后我们选了方案一因为改动最小而且表单页面本来就不应该被缓存。6.3 常见问题速查表问题现象可能原因排查方向解决方案页面弹出莫名弹窗XSS注入检查所有用户输入输出点输出转义、CSPCookie被发送到外部XSS窃取检查Cookie是否HttpOnly设置HttpOnly、Secure用户未操作却执行了动作CSRF检查敏感接口是否有Token验证加CSRF Token、SameSiteToken验证总是失败缓存或过期检查页面缓存策略和Token有效期禁用缓存、刷新TokenCSP配置后功能异常白名单不完整查看浏览器控制台CSP报告调整白名单、先用Report-Only实操心得排查XSS时重点看所有“用户可控数据”最终去了哪里。是插入HTML了是拼接到JS里了还是放进了URL每个出口都需要对应的转义处理。排查CSRF时重点看所有“状态改变”的接口是否只依赖Cookie做身份认证。7. 把安全变成开发习惯而不是事后补救聊了这么多原理和方案最后我想分享几个让安全落地的实际做法。第一把安全检查和代码审查绑定。每次提交代码时如果涉及用户输入输出审查者必须确认是否做了转义或过滤。我们团队后来加了一条规则任何使用innerHTML、v-html、dangerouslySetInnerHTML的地方都必须有注释说明为什么安全否则不予合并。第二用自动化工具做兜底。比如ESLint插件可以检测不安全的DOM操作OWASP ZAP可以扫描常见的XSS和CSRF漏洞。工具不能替代人工但能帮你发现明显的问题。第三安全配置要写进项目模板。新建项目时默认带上CSP头、HttpOnly Cookie、CSRF Token中间件。让安全成为默认选项而不是每次都要额外记得加。第四定期做安全演练。我们每季度会做一次内部的安全自查模拟XSS和CSRF攻击看看现有防护是否有效。有一次演练中我们发现一个内部管理后台的搜索框存在反射型XSS虽然外部访问不到但内部人员可能被诱导点击。修复后整个团队对XSS的敏感度都提高了。我个人在实际操作中的体会是Web安全不是一次性的任务而是一种持续的习惯。你不需要成为安全专家但你需要知道最常见的攻击方式长什么样以及如何在日常开发中堵住这些口子。XSS和CSRF只是冰山一角但把这两个搞明白你的项目就已经比大多数网站安全了。最后再分享一个小技巧如果你不确定某段代码是否安全可以问自己一个问题——“如果用户输入的是最恶意的内容这段代码会怎样”想清楚这个问题的答案你就知道该怎么改了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询