☰
XSS攻击五大利用方式与防御全解:从原理到实战
2026/10/19 14:46:40 网站建设 项目流程

1. XSS到底是什么,以及为什么安全问题绕不开它

做前端开发的,或者是搞过一阵子安全测试的,对“跨站脚本攻击”这个名字应该多少都有点耳熟。它通常被简写成XSS,全称是Cross-Site Scripting。我见过不少刚入行的同事,一听到这三个字母就眉头一皱,觉得这是个很高深的黑客技术。其实说白了,XSS的本质就是“把一段恶意的JavaScript脚本,想办法送进你的网页里并让它执行”。

为什么要起“跨站”这个名字?最早研究这类攻击的时候,大家发现整个攻击过程经常要借助第三方站点来完成,比如攻击者把恶意脚本藏在某个评论框里,然后等你带着登录态去访问那个帖子时,脚本就在你的浏览器里跑起来了。它“跨越”了站点的边界,明明是A网站的页面,却执行了攻击者从B网站注入的代码。这个概念搞明白了,后面所有的利用方式都是围绕“脚本能不能在受害者的浏览器里执行”这一点展开的。

我在实际接触XSS的过程中,最大的感受是:它的破坏力完全取决于你的业务场景。同样是执行了一段脚本,放在一个纯展示型的静态页面上,可能只是弹个框烦人一下;但如果放在一个带登录态的后台系统里,脚本可以替你发请求、改配置、导数据,那就完全是另一个量级的问题。所以看XSS的威胁,不能只看漏洞本身,得看它所在的位置能接触到什么数据、什么权限。

这篇文章会把标题里提到的几种常规利用方式逐个拆开来讲,包括窃取Cookie、恶意链接、键盘记录、网页钓鱼和挂马。我不会只丢结论,而是会把每一种利用背后的脚本思路和防御逻辑都讲清楚。因为做防御的人,不理解攻击路径,配出来的策略基本都是纸上谈兵。我还会穿插一些自己在测试和防守过程中踩过的坑,希望能帮大家省点时间。

2. XSS攻击的三种形态,先分清对象再谈防御

2.1 存储型XSS:最危险的一种,脚本直接进了数据库

如果你问一个安全从业者“哪种XSS危害最大”,大概率会听到“存储型”这个答案。原因很简单:这种XSS的恶意脚本是被服务器接收并保存下来的,有可能存在数据库里,有可能是存在文件里。之后任何用户打开包含这段内容的页面,脚本就会自动执行。这种攻击不需要受害者点击什么特殊链接,只要页面加载,恶意就发生了。

最常见的落脚点就是评论功能、用户昵称、签名档、富文本编辑器。我在某论坛做过一次测试,把一段包含<img src=x onerror=alert(document.cookie)>的字符串直接贴进昵称框,结果页面每次加载都会弹框。这看起来像个小玩具,但把蓄意攻击放在这个位置上,效果就完全不同了:脚本可以把当前用户的Cookie发给攻击者,拿到Cookie之后,攻击者可以直接以管理员的身份进入后台。因为后台的权限校验,很多时候就是看Cookie里的会话标识。

这里要理解一个关键点:存储型XSS之所以危险,还在于“持续感染”的特性。它不像反射型那样“一次性”,而是像病毒感染一样驻扎在服务器端,只要你访问那个页面,就会被触发。在防御上,后端必须对输入做严格的校验和过滤,对输出做合理的编码,任何一边出现纰漏,这个链条就会断裂。我见过太多项目只在前端做了过滤,后端原样入库,最后照样被打穿的情况。

2.2 反射型XSS:攻击代码藏在一个链接里

反射型XSS和存储型的最大区别,就是恶意脚本没有被存在服务器上。它通常是藏在URL参数里,当用户访问这个链接时,服务器端把参数值直接拼进返回的HTML里,脚本就跟着响应一起执行了。因为整个过程“反射”给用户,所以叫反射型。

听起来好像链路简单,危害似乎也不那么持久,但实际上它才是钓鱼场景里最好用的那个。攻击者把一个精心构造的恶意链接发给目标,目标看到域名是熟悉的正经网站,就会降低戒备心。比如某搜索站点的搜索框,把关键词直接回显在结果页里,参数值里追上一段<script>alert(1)</script>,就是一个典型的反射点。

这类利用的难点不在于构造脚本,而在于“怎么让用户真的点这个链接”。所以它通常会配合短网址、跳转页面、邮件美化等手段来做包装。防御上,后端对参数做输出编码是核心手段,同时也可以限制URL长度、过滤危险字符组合。我在做渗透测试时,经常会在URL里插入各种变体来探测回显位置的上下文,比如属性值里、标签里、注释里,每种上下文需要的绕过姿势完全不同。

2.3 DOM型XSS:纯前端问题,看payload都不一定能发现

DOM型XSS和前两种有个本质区别:它根本不经过服务器。恶意脚本是在前端执行过程中,通过操作DOM把不可信数据插入到页面里的。服务器返回的HTML是完全正常的,所以你在服务器端日志里根本看不到任何异常请求,这也是它最难排查的地方。

典型的场景就是页面里的window.location、document.referrer、postMessage消息这些由客户端控制的数据,被代码直接用于拼接HTML、设置innerHTML、写入eval或者appending到DOM节点里。我举个例子,某系统的URL里带一个#lang=cn的参数,前端拿到这个参数后,动态生成一段语言切换的下拉菜单,如果这个参数值没有做任何编码就直接插进DOM,那#lang=cn"><script>...这种payload就能直接执行。用户只要点开攻击者发来的带特定hash的链接,脚本就跑了。

这种漏洞在防御上尤其考验前端工程师的意识。后端再努力,对这种纯前端的执行链路也鞭长莫及。所以前端开发者在做任何DOM操作之前,都要问自己一个老生常谈的问题:这段数据是哪里来的,它能被我信任吗?

3. 五种常规利用方式的完整拆解,从原理到防御

3.1 窃取Cookie:最经典的会话劫持,但要先弄懂HttpOnly

了解XSS的人,几乎第一课都是“用XSS窃取Cookie”。这个观念本身没错,但我要说的是,现在的形势比十年前复杂得多。首要原因就是HttpOnly属性的普及。如果在设置Cookie时加了HttpOnly,那就意味着脚本里document.cookie是读不到这个Cookie的,XSS脚本即便能执行,也拿不到会话标识,会话劫持这条路就堵了一半。

这里有一个挺常见的误解:只要设置HttpOnly就绝对安全了。其实不然。我在某次测一个后台系统时,发现虽然管理员Cookie设了HttpOnly,但页面本身还有一层基础认证,Cookie里有另一个非HttpOnly的标记用于前端逻辑。这个标记本身虽然没有会话权限,但配合CORS的配置问题,攻击者可以借助它发起一些带凭证的跨域请求。所以断言“设了HttpOnly就安全”,在实战中属于过度自信。

对于真的能读取Cookie的场景,攻击者拿到Cookie后做了什么才决定损害程度。最常见的是直接把Cookie发到外部服务器,然后在自己的浏览器里替换Cookie,模拟登录态登录。这一整套操作下来,被攻击者的账号密码甚至都不需要知道,就能进账号后台。防御上,除了HttpOnly,还得做到敏感操作二次校验、定期滚动会话标识、对异常地域/设备的登录做风控。Cookie只是链条的第一环,链路后面每一环都值得加固。

3.2 恶意链接:让用户不知不觉访问一个危险地址

恶意链接的玩法,在XSS利用里算是一个复合型场景。攻击者通过一个反射型XSS或者一个视觉上不可识别的链接,把用户引导到某个恶意网页。这个网页可能是钓鱼站,也可能是浏览器漏洞利用页。不管哪一种,用户能看到的只是一个“有点像官方网站”的东西。

实际构造时,攻击者常用几个手段:一个是短链接服务,把一个带恶意参数的完整URL压缩成一串难辨的字符;再一个是URL跳转,先经过一个正常网站的跳转接口,到最后一跳才进入恶意页面。在用户眼里,开头那段域名确实正常,点进去之后,怪就怪在页面内容和平时不太一样。我遇到过不少用户来报障,说自己明明点开了公司内部的“工资查询”链接,页面特别奇怪,其实那根本就是个用XSS拼出来的伪造内容。

防御这个场景,单纯靠技术拦截不够,还得培养用户习惯。企业这边建议在内部系统部署统一的安全浏览器或Web网关,对外部域名做信誉检测和跳转拦截。个人这边,我的习惯是:任何网页要求输入密码,第一件事去看域名是否为官方域名,尤其是通过聊天窗口、短信发的链接。这个习惯能挡掉至少八成的恶意链接攻击。

3.3 获取键盘记录:脚本监听你的每一次敲击

键盘记录的核心思路非常简单,就是在前台脚本里注册keydown、keypress、keyup事件,把每次按键对应的键值存进数组或变量。当用户把焦点放在输入框里,输入的每一下都会被捕获。如果页面本身就是一张登录表单,那等于用户当面把账号密码输给攻击者看。

我拆过一个真实场景里的样本,代码并不复杂,就几十行。它用document.addEventListener('keydown', function(e){...})监听全页面的键盘事件,然后做了一层很鸡贼的过滤:只有当事件源的标签是input或textarea时才记录,这样不会记录用户在别的地方打的快捷键之类的噪音。记录下来的字符串每5秒就通过请求发送到外部收集服务器一次,即使中途断网或关闭页面,数据也已经回传了。

这里有一个对防守方来说比较扎心的事实:键盘记录脚本本身很难被常规安全软件检测出来,因为它没有触发任何系统级API,只是用了浏览器早就暴露给网页的前端接口。防护思路应该放在CSP上,通过内容安全策略严格限制脚本的来源,让外界脚本根本没有执行的资格。在配合上,对于那些可能有第三方SDK的页面,要额外审计SDK的行为,确认它没有向未知域名发送数据。我见过不少第三方统计SDK也做类似的键盘事件采集,至于数据的去向,扒一扒它的代码就能看到端倪。

3.4 网页钓鱼:比仿冒官网更隐蔽的是篡改真实页面

钓鱼这个词大家不陌生,但通过XSS做钓鱼和传统的“建一个假网站”最大的不同在于:受害者访问的是真实网站,只是页面内容被脚本临时篡改了。你打开某支付网站的首页,域名看上去完全正确,HTTPS锁也是绿的,但页面上的“请输入账号密码”弹窗,实际上是XSS注入脚本渲染出来的。真实页面可能只是被你正常访问,脚本在DOM上又画了一层一模一样的界面盖在上面。

这种钓术在“登录前页面”上效果尤其好,因为用户本身就处于还没认证的状态,输入账号密码是正常操作,面对一个突然弹出来的登录框,很多人不会多想。更高级的做法是结合键盘记录:不去伪造登录框,而是直接监听用户在用真实登录框输入时的按键序列,连伪造这一步都省了,拿到手的数据一样能用。

做防御的人不能只盯着“页面是否被注入”,还得做好整站防篡改的感知。比如监控DOM结构中是否出现非预期的iframe、是否多出了事件监听器。现在有一些前端安全监控方案会自动采集副作用信息,一旦发现异常注入,就上报并阻断渲染。我的建议是:关键业务页面尽量做完整域名级的安全校验,不允许任何非同源脚本执行,这样才能把钓鱼脚本生存的土壤全部铲掉。你自己开着某个真实的支付页面,页面里突然出现一个不是这个产品团队写的按钮,背后一定有问题。

3.5 挂马:通过浏览器漏洞把恶意代码放进你的设备

挂马这个词是早年间流氓软件时代留下来的老话,但它的思路到今天依然有效。核心逻辑是:XSS脚本先进入页面,然后尝试利用浏览器的某个漏洞,或者下载一个带病毒的附件,让用户主动打开。整个链条的终点不是浏览器里的弹窗,而是设备失陷。

现代浏览器厂商的安全机制已经做了很多,比如沙箱隔离、自动更新、内存破坏防护。理论上纯粹靠浏览器漏洞打穿一台打好补丁的设备已经很难。但挂马的实际攻击窗口往往不在浏览器本身,而在浏览器配套的插件,或者用户在网页里主动下载并打开的文档文件。举个例子,某XSS脚本在检测到用户访问页面时,如果发现HTTP响应头没有X-Content-Type-Options: nosniff,就会动态创建一个下载任务,假装以“发票明细.xls”为名,实为可执行文件。用户一旦下载并双击运行,后面就没浏览器的安全模型什么事了。

防御上,企业侧可以做下载文件的强制策略,比如不允许从浏览器直接下载可执行文件,统一走IT审批流程。个人侧,保持浏览器自动更新、插件能少则少、一眼识别高频出现的“发票/表格/压缩包”文件名,都算比较实用的动作。每次看到有人因为好奇双击一个来路不明的附件而中招,我都觉得这根本不是技术问题,是安全意识问题。

4. 前端过滤策略与浏览器关键防御机制,一步步该怎么配

4.1 输入过滤和输出编码,两件事不能混为一谈

很多人一说到XSS防御,张口就是“过滤”。但我一直强调,输入过滤和输出编码是两件完全不同的事,不能混为一谈。输入过滤是指对用户提交上来的数据做检查,把<script>、javascript:这类危险模式拦在门外。输出编码是指把动态内容渲染到页面时,对特殊字符进行转义,让它们不参与HTML解析。

只做输入过滤是远远不够的。因为过滤总有被绕过的时候,比如用HTML实体编码、用事件属性拼字符、用Unicode变形,都有机会躲过关键词黑名单。输出编码则是在每一个渲染点上做数学意义上的“安全兜底”,无论输入长什么样,渲染出来的都是纯文本,解析不出任何标签和事件。我把这两者比作铠甲和盾牌,过滤是挡一下,编码是让你自带免伤。

实操里,输出编码要分上下文。同样的数据,放进HTML标签内容里和放进属性值里、放进JavaScript字符串里、放进URL里,需要的编码规则完全不同。比如放进<div>${data}</div>,要转义的是<、>、&;放进<img src="${data}">,还得额外阻止"逃逸;放进onclick="..."里,等于是在构造JavaScript代码,必须对它做JavaScript层面的转义。开发框架一般自带编码器,但如果你用的是字符串拼接的老项目,务必注意这些边界细节。

4.2 内容安全策略CSP,把脚本来源画在一个圈里

CSP是我在防御XSS时推荐的第一顺位大招。它的思路很简单粗暴,就是通过响应头或者meta标签,告诉浏览器“页面里允许执行哪些来源的脚本”。比如配置Content-Security-Policy: script-src 'self',意思是这个页面只能执行自身域名下的脚本,任何内联脚本、外部脚本、eval都直接拒绝。

这个策略对XSS是釜底抽薪式的:无论payload写得多么天衣无缝,只要它不是一个来自当前域名下的合法脚本,浏览器压根不会执行。在实际配置时要注意粒度,生产环境尽量避开'unsafe-inline'和'unsafe-eval'这两个豁免词,它们基本等于把CSP打了个半开。如果业务上有内联脚本需求,可以用'nonce-xxx'的方式给每次请求生成一次性随机数,只放行带正确nonce的脚本块。

我见过一些团队在CSP配置完成后,把第三方统计、客服聊天、广告SDK的域名统统加进了白名单。这个做法可以理解,但白名单越宽,被攻破的风险就越大。建议对第三方脚本做分类评估:必用的SDK单独通过script-src白名单引入,其余不常用、不透明的代码宁可砍掉功能。这里有一个实操细节:上线CSP之前,先开着Content-Security-Policy-Report-Only模式跑一段时间,把报错收集下来再做调整,别直接强制执行,否则首页秒挂不是玩笑话。

4.3 HttpOnly本质上是一个值钱的小标记

HttpOnly本身是个Cookie属性,加上之后浏览器就禁止JavaScript读取这个Cookie。它不解决所有问题,但能把会话窃取这条最常见的攻击路径直接封死。两个组合起来,配合CSP,XSS即便真的执行了,攻击者也拿不到会话凭证,这个杀伤力就小了很多。

设置HttpOnly没有统一标准,不同语言的后端框架配置方式不同,但核心就一件事:给会话Cookie加属性。以Java的Servlet为例,通过response.setHeader("Set-Cookie", "...; HttpOnly")或者用SessionConfig的httpOnly开关即可。Node方向,Express的cookie-session中间件默认有这个配置项。部署之后一定要实测一下,打开浏览器开发者工具,确认document.cookie里确实看不到会话标识。我遇到过好多次配置文档说开了但实际没生效的情况,代码审查又查不出来,最后都是靠这一步验证发现的。

4.4 安全响应头,能加的全加上

除了CSP,还有一些容易被忽略但非常重要的响应头值得加。X-Frame-Options: DENY或等价的frame-ancestors指令,用来阻止你的页面被嵌进别人的iframe里,配合防止点击劫持。X-Content-Type-Options: nosniff,强制浏览器严格按Content-Type处理响应,避免MIME嗅探带来的隐患。Referrer-Policy限制Referrer泄露,避免URL参数里的敏感信息被第三方网页带走。

这些头不用一次性全加上,但建议做成公司的安全基线,所有新项目上线前过一遍。我经常看到一种情况:业务迭代很快,新页面越来越多,但安全头只有老页面有,新页面的控制台一直报警。所以更好的做法是做成网关层的统一添加,而不是每个服务自己加。网关统一加会带来一个问题就是动态内容那边的缓存策略会受影响,需要测一轮缓存与安全头的配合是否正常。

5. 常见问题与排查实录,我踩过的一些坑

5.1 为什么设置了HttpOnly还是被窃取了会话

这个问题的答案往往不在HttpOnly本身,而在会话管理的其他环节。最常见的是会话标识在URL里传递,比如某些老系统会把;jsessionid=xxx直接带在链接后面,那HttpOnly再多也没用,因为攻击者从日志和Referrer里都能扒到它。

另一个比较隐蔽的坑是:Cookie没有设置Secure属性。如果整个站点存在HTTP与HTTPS混用的地方,Cookie在明文HTTP请求里就裸奔了。攻击者只要有网络层的某一个监听点,就能直接截获。这种情况HttpOnly同样形同虚设。动手排查时,建议自己完整走一遍登录流程,把每个请求的Cookie属性都翻一遍,重点看有没有Secure、SameSite、HttpOnly三个值同时缺席的情况。

同样值得看的还有SameSite属性。同站和跨站请求的Cookie发送策略,如果设置不当,你即使加了HttpOnly,攻击者依然可以利用跨站请求伪造。我建议在业务允许的情况下尽量把SameSite设为Lax或者Strict,能显著降低CSRF和部分跨站脚本窃取场景的风险。

5.2 编码已经做了,为什么还是弹出执行框

这个问题我碰到过好几次,最后定位到原因几乎都是一样的:输出编码做了,但做的位置不对。比如后端把数据编码成HTML实体之后,字段被另一个模块取出来,直接当JavaScript变量以字符串拼接的形态接回页面。这时HTML实体只能确保它不会作为标签被解析,但JavaScript的eval或者new Function依旧可以执行它。

还有一种情况是编码器用错了上下文。后端统一调了一个文本编码器,这个编码器对HTML内容有效,但当数据被查进一个属性值里,你需要的其实是属性值编码,它的转义规则跟文本编码不是一套。我曾在一个标签页属性里塞数据,编码后引号被转义了,但浏览器解析属性值时,&实体被还原,导致属性逃逸。你如果用的是框架自带的编码器,也要搞清楚它的适用上下文,不是所有编码器都万能。

链条排查的时候,建议从外部输入一路走到浏览器DOM,检查每个环节的编码是否匹配目标上下文。很多时候问题不是“没编码”,而是“编码了一次之后又被错误地拼接了一次”,相当于绕过了防护。现在的新项目实际开发中,优先选用了自动转义的模板引擎或前端框架,很多时候能直接规避一半的调试成本。

5.3 同一个XSS攻击,为什么有的终端执行有的不执行

遇到过真实场景,同一条XSS脚本,在测试的Chrome里弹框,在某个同事的旧版浏览器里完全不执行。原因是浏览器对某些DOM事件、字符编码、解析器特性的支持不一样。比如老版本浏览器对一些畸形HTML标签比较宽容,payload写得更随意,新版本直接把它当成文本处理,根本触发不了。

这种情况在攻击者眼里是个挑战,在防御者眼里其实也是个机会。如果你的XSS攻击只在特定浏览器版本下生效,说明防御方向之一是尽量把用户迁移到现代浏览器,并推进自动更新。另外测试时最好覆盖主流浏览器的当前版本,别只用自己顺手的那一个。我做基线扫描的时候都会要求在Chrome、Firefox、Safari、Edge四件套各跑一遍,少一个可能就漏一个分支。

同一个页面在不同浏览器下的解析差异,本质上是浏览器内核实现差异导致的。对于受害者来说,这个不确定性意味着攻击成功率可能并不稳定,攻击者通常会选择性价比较高的环境来持续投递。防御侧不能指望“反正攻击兼容性差”来苟,还是得老老实实把刚才讲的过滤、编码、CSP全套做完整。

5.4 发现了漏洞不会修,或者害怕改了影响业务

业务团队遇到这个情况特别普遍。安全团队报了个XSS漏洞,研发说“这个字段要保留HTML格式,没法做纯文本编码”,双方僵住了。这种时候我的建议是不要一刀切禁掉HTML,而是改用白名单式的富文本过滤库,比如把允许的标签精确控制在常用、无害的集合内,所有非白名单的标签一律剥掉。这样既保住了富文本能力,又能挡住99%的常规注入。

另一个常见的“不会修”,其实是不知道影响面。如果同一个数据接口被多个页面复用,改了编码规则可能其他页面样式全乱。排查的时候建议先做数据流梳理,弄清楚这个字段在哪些页面以什么方式渲染,然后再决定修改点是后端统一改,还是前端分散改。确实有那种“牵一发动全身”的老系统,我的建议是可以先给高风险页面单独加CSP和HttpOnly顶住,再排期做彻底修复。

修复过程中最重要的一件事是建立回归用例。每一次修复都应该写一个包含常规payload变体的测试用例进CI,让这个漏洞不会在下次重构中不知不觉又冒出来。我见过太多“修好了过一个月又回来”的漏洞,根因就是修复缺少自动化验证闭环。

6. 我最想分享给实战派的一些体会

做了几年安全之后,我最大的感受是:XSS没有死,也不会死。你去看所有最新的前端框架、开发语言、浏览器安全机制,它们都在跟XSS的变体形态缠斗。每一次出现新的渲染API、新的数据传递通道,就会有一段新的绕过招式冒出来。所以真正落地的安全建设,不能是一锤子买卖,必须有机制持续运行。

我自己在日常工作里,习惯把安全策略拆得很细。一个页面在什么场景下允许内联脚本,什么场景下必须走CSP nonce,Cookie各自的属性和作用域怎么划分,这些都应该有明确的规范文本,写进项目文档里。没有人能靠记忆管理这些细节,文档和自动化检查工具才是长久的保障。

我想对刚入行或者转行做安全的同学说一句:不要看完各种攻击payload就觉得自己很“黑客”。真正的安全能力,体现在当你看到一段攻击代码时,脑子里第一反应不是“这能用在哪”,而是“这段代码为什么能生效,我应该在哪个环节阻断它”。不妨多花时间研究防御端的技术细节,因为只有当你把防御体系想明白了,你对攻击路径的理解才称得上完整。

最后分享一个我踩过几次坑之后养成的习惯:每次开发完一个带用户输入的新功能,我都会拿一份公开的基础XSS payload列表,在无痕模式里对登录前和登录后的页面各跑一遍。不是每个项目都请得起专职安全团队,但至少这部分自查动作,每个开发者都可以做。做业务开发的人如果普遍具备这种“输入不可信”的自觉,很多安全问题其实在第一道编码的时候就已经不存在了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询