news 2026/10/19 9:52:58

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 src="x" onerror="alert('你的Cookie是:' + document.cookie)">

这段代码会怎样?浏览器解析到<img>标签时,发现src="x"加载失败,于是触发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 src="https://bank.example.com/transfer?to=attacker&amount=10000" width="0" height="0">

浏览器加载这张图片时,会自动向银行网站发送一个转账请求。因为你的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 src="x" onerror="alert(1)"><p>正常内容</p>'; const clean = DOMPurify.sanitize(dirty, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'], ALLOWED_ATTR: ['href', 'title'] }); // 结果:<p>正常内容</p>

DOMPurify会解析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: sessionId=abc123; HttpOnly; Secure; SameSite=Lax
  • HttpOnly:禁止JS读取
  • Secure:仅通过HTTPS传输
  • SameSite:限制跨站请求携带Cookie

这三个属性组合起来,能大幅降低XSS和CSRF的影响范围。

5. 防御CSRF:Token、SameSite与二次验证

5.1 CSRF Token:最经典的防御手段

CSRF攻击之所以能成功,是因为浏览器自动携带了Cookie,而服务器无法区分请求来源。CSRF Token的思路是:在页面中嵌入一个随机生成的Token,提交请求时必须带上这个Token,服务器验证Token是否匹配。

攻击者的页面无法读取目标网站的Token(因为同源策略限制),所以无法构造出合法请求。

实现流程:

  1. 用户访问表单页面时,服务器生成一个随机Token,存入Session,同时嵌入表单隐藏字段。
  2. 用户提交表单时,Token随请求一起发送。
  3. 服务器比对请求中的Token和Session中的Token,一致则通过,否则拒绝。
<form action="/transfer" method="POST"> <input type="hidden" name="csrf_token" value="随机生成的Token"> <input type="text" name="to" placeholder="收款方"> <input type="number" name="amount" placeholder="金额"> <button type="submit">转账</button> </form>

注意:Token必须足够随机,且每次会话或每次请求都重新生成。如果Token可预测,防御就形同虚设。

5.2 SameSite Cookie:浏览器层面的天然屏障

SameSite是Cookie的一个属性,用来控制跨站请求是否携带Cookie。它有三个值:

值行为适用场景
Strict完全禁止跨站携带安全性要求极高的操作
Lax允许顶级导航的GET请求携带大多数网站的默认选择
None允许跨站携带,但必须配合Secure需要跨站嵌入的场景

设置SameSite=Lax后,从其他网站发起的POST请求不会携带Cookie,CSRF攻击自然失效。这是目前浏览器默认推荐的配置,成本低、效果好。

Set-Cookie: sessionId=abc123; SameSite=Lax; Secure; HttpOnly

5.3 二次验证:敏感操作的额外保险

对于转账、改密码、删除账号这类高危操作,光靠Token还不够。我建议加上二次验证,比如:

  • 要求输入当前密码
  • 发送短信验证码
  • 使用邮箱确认链接

这样即使CSRF Token被绕过,攻击者也无法完成最终操作。多一层验证,多一层安全。

5.4 防御方案对比与选型建议

防御手段防御目标实现成本兼容性推荐程度
输出转义XSS低好必做
白名单过滤XSS中好富文本场景必做
CSPXSS中高较好强烈推荐
HttpOnlyXSS低好必做
CSRF TokenCSRF中好必做
SameSiteCSRF低较好强烈推荐
二次验证CSRF中好高危操作必做

实际项目中,我通常会组合使用:输出转义 + HttpOnly + CSP防XSS,CSRF 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#name=<img src=x onerror=alert(1)>

用户点开后,脚本就会执行。修复方式很简单,把innerHTML换成textContent:

document.getElementById('welcome').textContent = '欢迎,' + params.get('name');

textContent会把内容当成纯文本处理,不会解析HTML标签。这个改动只花了一分钟,但堵住了一个真实的漏洞。

6.2 CSRF Token失效的排查过程

有一次测试反馈说,某个表单提交总是报“Token无效”。我排查了半天,发现原因是:页面被缓存了。用户第一次打开页面时拿到了Token A,但页面被浏览器缓存。等Token过期后,用户再次提交,用的还是缓存的Token A,而服务器已经生成了新的Token B。

解决方案有两个:

  1. 在表单页面设置Cache-Control: no-store,禁止缓存。
  2. Token不存Session,而是用无状态的方式生成(比如JWT),并设置合理的过期时间。

最后我们选了方案一,因为改动最小,而且表单页面本来就不应该被缓存。

6.3 常见问题速查表

问题现象可能原因排查方向解决方案
页面弹出莫名弹窗XSS注入检查所有用户输入输出点输出转义、CSP
Cookie被发送到外部XSS窃取检查Cookie是否HttpOnly设置HttpOnly、Secure
用户未操作却执行了动作CSRF检查敏感接口是否有Token验证加CSRF Token、SameSite
Token验证总是失败缓存或过期检查页面缓存策略和Token有效期禁用缓存、刷新Token
CSP配置后功能异常白名单不完整查看浏览器控制台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只是冰山一角,但把这两个搞明白,你的项目就已经比大多数网站安全了。

最后再分享一个小技巧:如果你不确定某段代码是否安全,可以问自己一个问题——“如果用户输入的是最恶意的内容,这段代码会怎样?”想清楚这个问题的答案,你就知道该怎么改了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 9:52:57

代码审查效率提升:开源自动化辅助审查工具落地实践

团队最近做了一次代码审查效率复盘&#xff0c;翻出一个很有意思的数据&#xff1a;一个中等规模的合并请求&#xff0c;平均要经过2.3轮人工评论才能合入&#xff0c;其中超过四成评论是格式、命名、明显逻辑遗漏这类静态问题。这让我下决心把“open-code-review”这套自动化辅…

作者头像 李华
网站建设 2026/10/19 9:51:22

Seedance 2.0多镜头叙事实战:零基础如何用AI视频生成30秒车企宣传片

上个月我接到一个车企商单&#xff0c;需求是30秒的新能源品牌宣传片&#xff0c;预算不高&#xff0c;但交付时间只有四天。按传统方式&#xff0c;这个周期连踩景和拍摄都来不及&#xff0c;更别提炼后期。但我最终靠Seedance 2.0的新功能把片子做出来了&#xff0c;从分镜脚…

作者头像 李华
网站建设 2026/10/19 9:49:55

API调用成本优化指南:从缓存命中到任务分流的省钱实战

1. 先算清这笔账&#xff1a;你的钱到底花在哪了先说结论&#xff1a;我过去每个月在 API 调用上的开销大约 2300 块上下&#xff0c;把能砍的砍掉、能省的省下来之后&#xff0c;现在稳定在 500 块左右&#xff0c;等于一个月少了 1800 块的开支。说实话&#xff0c;这个数字不…

作者头像 李华
网站建设 2026/10/19 9:49:43

open-code-review:开源自动化代码评审工具实战指南

1. 代码评审的现状与 open-code-review 的定位代码评审这件事&#xff0c;每个团队都在做&#xff0c;但做得好不好就是另一回事了。我在不少团队里见过同一个问题&#xff1a;评审流于形式&#xff0c;或者干脆变成了“点赞大会”。代码合并之前大家点个赞、留个言&#xff0c…

作者头像 李华
网站建设 2026/10/19 9:49:31

CPO优化SVR超参数回归预测:区别于SVM分类的Matlab实现

最近看到不少人在讨论“豪冠猪算法”这个名字&#xff0c;第一反应以为是谐音梗或者翻译失误。直到我去翻了原论文并亲手在Matlab里复现了一版&#xff0c;才发现它其实是2024年新提出的自然启发式优化算法——冠豪猪优化器&#xff08;Crested Porcupine Optimizer&#xff0c…

作者头像 李华
网站建设 2026/10/19 9:48:28

A2.0反爬风控系统拆解:动态挑战、设备指纹与行为模型

最近不少做业务风控的朋友来问我&#xff1a;“A2.0那个系统到底怎么破&#xff1f;”每次听到这种问题&#xff0c;我都会先反问一句&#xff1a;你是想绕过它&#xff0c;还是想弄明白它为什么能拦住你&#xff1f;这两个方向完全不同。一个是黑产关注的事&#xff0c;一个是…

作者头像 李华