首页 / 资讯中心 / 文章详情

XSS攻击五大利用方式与防御全解:从原理到实战

XSS攻击五大利用方式与防御全解:从原理到实战 ★ FEATURED ARTICLE
1. XSS到底是什么以及为什么安全问题绕不开它做前端开发的或者是搞过一阵子安全测试的对“跨站脚本攻击”这个名字应该多少都有点耳熟。它通常被简写成XSS全称是Cross-Site Scripting。我见过不少刚入行的同事一听到这三个字母就眉头一皱觉得这是个很高深的黑客技术。其实说白了XSS的本质就是“把一段恶意的JavaScript脚本想办法送进你的网页里并让它执行”。为什么要起“跨站”这个名字最早研究这类攻击的时候大家发现整个攻击过程经常要借助第三方站点来完成比如攻击者把恶意脚本藏在某个评论框里然后等你带着登录态去访问那个帖子时脚本就在你的浏览器里跑起来了。它“跨越”了站点的边界明明是A网站的页面却执行了攻击者从B网站注入的代码。这个概念搞明白了后面所有的利用方式都是围绕“脚本能不能在受害者的浏览器里执行”这一点展开的。我在实际接触XSS的过程中最大的感受是它的破坏力完全取决于你的业务场景。同样是执行了一段脚本放在一个纯展示型的静态页面上可能只是弹个框烦人一下但如果放在一个带登录态的后台系统里脚本可以替你发请求、改配置、导数据那就完全是另一个量级的问题。所以看XSS的威胁不能只看漏洞本身得看它所在的位置能接触到什么数据、什么权限。这篇文章会把标题里提到的几种常规利用方式逐个拆开来讲包括窃取Cookie、恶意链接、键盘记录、网页钓鱼和挂马。我不会只丢结论而是会把每一种利用背后的脚本思路和防御逻辑都讲清楚。因为做防御的人不理解攻击路径配出来的策略基本都是纸上谈兵。我还会穿插一些自己在测试和防守过程中踩过的坑希望能帮大家省点时间。2. XSS攻击的三种形态先分清对象再谈防御2.1 存储型XSS最危险的一种脚本直接进了数据库如果你问一个安全从业者“哪种XSS危害最大”大概率会听到“存储型”这个答案。原因很简单这种XSS的恶意脚本是被服务器接收并保存下来的有可能存在数据库里有可能是存在文件里。之后任何用户打开包含这段内容的页面脚本就会自动执行。这种攻击不需要受害者点击什么特殊链接只要页面加载恶意就发生了。最常见的落脚点就是评论功能、用户昵称、签名档、富文本编辑器。我在某论坛做过一次测试把一段包含img srcx onerroralert(document.cookie)的字符串直接贴进昵称框结果页面每次加载都会弹框。这看起来像个小玩具但把蓄意攻击放在这个位置上效果就完全不同了脚本可以把当前用户的Cookie发给攻击者拿到Cookie之后攻击者可以直接以管理员的身份进入后台。因为后台的权限校验很多时候就是看Cookie里的会话标识。这里要理解一个关键点存储型XSS之所以危险还在于“持续感染”的特性。它不像反射型那样“一次性”而是像病毒感染一样驻扎在服务器端只要你访问那个页面就会被触发。在防御上后端必须对输入做严格的校验和过滤对输出做合理的编码任何一边出现纰漏这个链条就会断裂。我见过太多项目只在前端做了过滤后端原样入库最后照样被打穿的情况。2.2 反射型XSS攻击代码藏在一个链接里反射型XSS和存储型的最大区别就是恶意脚本没有被存在服务器上。它通常是藏在URL参数里当用户访问这个链接时服务器端把参数值直接拼进返回的HTML里脚本就跟着响应一起执行了。因为整个过程“反射”给用户所以叫反射型。听起来好像链路简单危害似乎也不那么持久但实际上它才是钓鱼场景里最好用的那个。攻击者把一个精心构造的恶意链接发给目标目标看到域名是熟悉的正经网站就会降低戒备心。比如某搜索站点的搜索框把关键词直接回显在结果页里参数值里追上一段scriptalert(1)/script就是一个典型的反射点。这类利用的难点不在于构造脚本而在于“怎么让用户真的点这个链接”。所以它通常会配合短网址、跳转页面、邮件美化等手段来做包装。防御上后端对参数做输出编码是核心手段同时也可以限制URL长度、过滤危险字符组合。我在做渗透测试时经常会在URL里插入各种变体来探测回显位置的上下文比如属性值里、标签里、注释里每种上下文需要的绕过姿势完全不同。2.3 DOM型XSS纯前端问题看payload都不一定能发现DOM型XSS和前两种有个本质区别它根本不经过服务器。恶意脚本是在前端执行过程中通过操作DOM把不可信数据插入到页面里的。服务器返回的HTML是完全正常的所以你在服务器端日志里根本看不到任何异常请求这也是它最难排查的地方。典型的场景就是页面里的window.location、document.referrer、postMessage消息这些由客户端控制的数据被代码直接用于拼接HTML、设置innerHTML、写入eval或者appending到DOM节点里。我举个例子某系统的URL里带一个#langcn的参数前端拿到这个参数后动态生成一段语言切换的下拉菜单如果这个参数值没有做任何编码就直接插进DOM那#langcnscript...这种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。它不解决所有问题但能把会话窃取这条最常见的攻击路径直接封死。两个组合起来配合CSPXSS即便真的执行了攻击者也拿不到会话凭证这个杀伤力就小了很多。设置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里传递比如某些老系统会把;jsessionidxxx直接带在链接后面那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 nonceCookie各自的属性和作用域怎么划分这些都应该有明确的规范文本写进项目文档里。没有人能靠记忆管理这些细节文档和自动化检查工具才是长久的保障。我想对刚入行或者转行做安全的同学说一句不要看完各种攻击payload就觉得自己很“黑客”。真正的安全能力体现在当你看到一段攻击代码时脑子里第一反应不是“这能用在哪”而是“这段代码为什么能生效我应该在哪个环节阻断它”。不妨多花时间研究防御端的技术细节因为只有当你把防御体系想明白了你对攻击路径的理解才称得上完整。最后分享一个我踩过几次坑之后养成的习惯每次开发完一个带用户输入的新功能我都会拿一份公开的基础XSS payload列表在无痕模式里对登录前和登录后的页面各跑一遍。不是每个项目都请得起专职安全团队但至少这部分自查动作每个开发者都可以做。做业务开发的人如果普遍具备这种“输入不可信”的自觉很多安全问题其实在第一道编码的时候就已经不存在了。
阅读完成 · 觉得有帮助?
咨询建站