ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

XSS跨站脚本攻击原理与防御:从基础到SpringBoot实战

XSS跨站脚本攻击原理与防御:从基础到SpringBoot实战 XSS跨站脚本攻击是前端安全领域最容易被忽视、但实际破坏力极强的威胁之一。很多开发者把XSS简单理解成“弹个alert”直到用户Cookie被窃取、后台会话被劫持、整站页面被挂马才意识到它真正能造成的影响。这篇文章我会从攻击原理、三种常见类型、真实利用链、靶场实操到SpringBoot全局过滤器落地防御完整梳理一套前端安全防御思路。适合前端工程师、后端安全负责人、安全测试入门者也适合准备面试或CTF方向的同学。文章里不会堆砌空理论而是尽量把“为什么这么做”讲清楚再给出可以直接参考的防御方案。1. XSS攻击的本质与影响范围1.1 从一个真实案例说起之前帮一个做企业服务的朋友做安全评审他们的后台管理系统中有一个“客户反馈搜索”功能。开发同学觉得这个功能太简单了就是把输入框的关键词通过GET请求传到后端后端拼SQL查数据库再把结果渲染回HTML页面。结果我用一个很普通的探测字符串类似scriptalert(1)/script放在搜索框里页面立刻就弹窗了。随后我尝试把弹窗脚本替换成一个读取Cookie并外发的逻辑时对方才意识到问题的严重性只要把构造好的URL发给管理员管理员点击后他的登录会话就可能被攻击者直接拿走。这个案例很典型它说明XSS并不只是“页面乱掉”或者“弹个窗”而是可以变成一条成熟的攻击链。真实业务里搜索框、评论区、用户昵称、富文本编辑器、文件上传后的文件名展示几乎每一个能展示用户输入的地方都可能成为入口。最让人头疼的是XSS往往不是独立存在的它会和其他漏洞组合比如CSRF攻击、钓鱼页面、键盘记录最终演变成账号被盗、核心数据泄露甚至内网渗透的跳板。1.2 XSS的分类与判定标准XSS通常被分成三大类反射型、存储型、DOM型。这个分类主要看“恶意脚本在哪里被解析”以及“是否持久化”。反射型XSS恶意脚本存在于URL参数中后端获取参数后未做编码直接输出到页面。脚本只在当前请求中生效刷新或换参数后就消失所以叫“反射”。存储型XSS恶意脚本被保存到数据库、文件系统等存储介质中之后每次有人访问包含该数据的页面脚本都会执行。评论、留言、个人简介都是典型场景。DOM型XSS脚本不经过后端而是由前端JS直接获取URL片段、URL参数或本地存储中的内容再通过innerHTML、document.write、eval等接口写入页面并触发执行。现场排查时可以先看数据流打开浏览器开发者工具刷新页面看Network请求输入内容是否出现在请求参数里再看响应HTML中是否直接出现输入值最后看前端JS里有没有把某个可控变量塞进HTML片段。判断逻辑其实就是一个三选一请求参数到响应HTML是“后端拼接”就是反射型输入被持久化到服务端再渲染就是存储型从请求参数或URL片段到前端DOM操作全程不涉及服务端就是DOM型。1.3 XSS攻击的影响面分析很多人以为XSS只影响浏览器前端实际上XSS可以把手伸到整个应用和后端基础设施。主要影响面包括会话劫持通过读取Cookie中的Session ID攻击者可以冒充用户登录系统执行发帖、改密码、转账等操作。钓鱼与诱导攻击者可以在页面上插入伪装登录框、伪造公告诱导用户提交敏感信息。页面篡改与挂马存储型XSS可以让页面长期被植入恶意内容影响所有访问者。内网探测如果用户在内网环境访问了被攻击的页面脚本可以向内网地址发起请求探测内部系统。借刀杀人XSS请求携带用户Cookie并且受同源策略约束相对宽松时比如JSONP接口攻击者可以利用用户身份调用后端敏感接口。也就是说XSS破坏的不只是前端UI而是整个信任体系。这也是为什么现在不管是等保测评还是企业安全评审都会把XSS列为高危漏洞项。2. 三种XSS类型深度拆解2.1 反射型XSS请求参数里的“回马枪”反射型XSS最常见的出现位置是搜索页、错误提示页、页面跳转接口。它的特点是“一次性”脚本只在当前请求的响应中执行。攻击者需要诱导用户点击构造好的恶意URL所以经常配合钓鱼邮件、短链接、二维码一起使用。举一个典型的场景一个小型网站的搜索接口后端代码类似print(p您搜索的是 request.getParameter(keyword) /p)。如果用户输入的 keyword 是scriptalert(1)/script后端没有对、做HTML实体编码浏览器就会把它当作标签解析并执行。攻击者把这个参数编码进URL再发给目标用户用户一旦点击脚本就在用户自己的浏览器环境中执行。为什么反射型XSS容易漏掉因为开发自测时经常只测“页面显示是否正常”不测“特殊字符是否被转义”。而且反射型XSS受上下文影响很大同样的输入出现在HTML标签内、属性内、JS字符串内需要的输出编码策略完全不同。比如在HTML标签内需要对尖括号做编码在属性内还要处理引号在JS字符串里需要处理反斜杠和引号。防御反射型XSS最有效的手段不是过滤用户输入而是在输出侧做上下文感知的编码。后端模板引擎如Thymeleaf、JSP的fn:escapeXml、Vue的{{ }}自带转义这些是默认安全选项但要小心v-html、innerHTML这类跳过转义的接口。2.2 存储型XSS长期潜伏在服务器端的威胁存储型XSS比反射型更危险因为脚本一旦写入服务器所有访问受影响页面的用户都会中招。攻击者不需要单独给每个受害者发送链接只要在评论区发布一条包含恶意脚本的内容后续每个打开页面的用户都会执行脚本。常见的存储位置包括数据库字段、文件系统、缓存系统。评论区、个人资料、订单备注、富文本内容、文件上传后的文件名或描述信息都是重灾区。富文本编辑器尤其值得注意很多富文本编辑器允许用户提交包含HTML标签的内容如果后端只是简单过滤script攻击者通过img、svg、iframe、video等标签的onerror、onload事件属性一样可以触发脚本。我在测试一个项目时见过这样的案例用户头像上传后系统把头像文件的原始文件名直接拼到HTML里展示文件名是abc onerroralert(1)这样的形式拼接后变成了img src/upload/abc onerroralert(1)前端执行了恶意脚本。这种问题不是出现在大段文本里而是出现在一个不起眼的“文件名展示”环节。防御存储型XSS需要做两层写数据之前对输入做白名单校验读数据显示时做输出编码。对于富文本场景建议使用成熟的富文本过滤库比如OWASP Java HTML Sanitizer、Python的bleach、前端DOMPurify。一定不要自己写正则过滤HTML因为绕过正则的花样太多了实体编码、大小写混淆、Unicode编码都可以让黑名单失效。2.3 DOM型XSS纯前端逻辑的“意外后背”DOM型XSS严格来说和服务器端无关问题完全出在前端JS代码里。常见的数据来源是location.href、location.hash、location.search、document.referrer、window.name、postMessage消息等。如果这些数据被直接传递给innerHTML、outerHTML、document.write、eval、setTimeout等接口就会产生XSS。举个例子一个单页应用通过URL hash实现路由比如http://example.com/#/profile?tababout前端代码用location.hash取到tab参数后直接执行document.getElementById(content).innerHTML div tab /div。攻击者把URL里的tab改成img srcx onerroralert(document.cookie)页面加载后就会执行。DOM型XSS比较隐蔽即使后端把所有请求参数都做了过滤如果前端某个地方用错误的方式处理了URL参数漏洞依然存在。根据我接触过的项目最常见的前端坑是使用innerHTML拼接用户可控内容使用v-html渲染后端返回的富文本把服务端返回的JSON直接塞进script标签用eval执行动态拼接的代码对postMessage消息来源和格式检查不严防御DOM型XSS的核心是杜绝危险的DOM操作方法优先使用textContent、innerText、createTextNode这类不解析HTML的接口。如果确实需要渲染富文本一定要通过DOMPurify等工具做净化处理。Vue和React项目里也要严格限制v-html、dangerouslySetInnerHTML的使用碰到业务上必须用的情况必须经过过滤器。2.4 三种XSS类型的对比表格维度反射型XSS存储型XSSDOM型XSS恶意脚本存放位置URL参数服务器存储没有固定位置运行时在前端内存中是否需要后端参与需要需要不需要纯前端数据来源请求参数、表单提交上次请求写入的持久化数据URL、hash、referrer、postMessage等持久性一次性持久化危害周期长每次页面加载时触发取决于前端代码典型场景搜索框、错误提示评论、昵称、富文本单页应用路由、hash参数危害等级中高高防御侧重点输出编码输入过滤输出编码前端安全编码实践这张表对我来说最大的意义是做安全测试时先判断输入数据流走到哪一层就能快速定位应该看后端代码还是前端代码避免在错误的方向上浪费时间。3. XSS攻击链与实战场景还原3.1 Cookie窃取与会话劫持的完整链路讲XSS就绕不开Cookie窃取。Cookie里一般会保存会话标识如果没有设置HttpOnly属性那么浏览器中的JS代码可以通过document.cookie读取到它。攻击者一旦通过XSS在页面中执行了JS就可以拿到当前用户的会话凭证。完整链路通常是这样的攻击者找到某个存在存储型XSS的评论区发布一条看似正常但包含外部脚本引用的内容。后续用户访问该页面时脚本在本机执行读取document.cookie的值然后向攻击者控制的接收端发起请求。攻击者获得合法Cookie后把Cookie放入自己的浏览器就能以该用户身份登录系统。这里必须强调HttpOnly是防御Cookie窃取的关键属性。只要Cookie设置了HttpOnlydocument.cookie就无法读取到该Cookie值JS脚本拿不到会话凭证也就无法直接完成会话劫持。但HttpOnly不是万能的如果攻击者利用XSS执行的是“发请求”而不是“读Cookie”比如直接调用后端接口修改用户资料那么HttpOnly就起不了作用了。所以在安全设计里一般会组合使用多种手段关键Cookie加HttpOnly、SameSite属性限制跨站携带、前端接口做CSRF Token校验再配合CSP限制脚本来源。即使某层被绕过其他层还能兜底。3.2 富文本与上传PDF文件时的XSS攻击面文件上传场景里的XSS容易被忽略尤其是上传PDF这类文档。常规思路是“PDF文件是二进制内容不会执行脚本”但如果系统提供在线预览功能并且直接以附件类型打开PDF有些场景下PDF内部可以嵌入JavaScript浏览器解析时就会执行。我在接触SpringBoot项目时发现一个很典型的隐患系统允许用户上传PDF文件上传后会把PDF以预览方式打开。后端对上传请求只校验了扩展名是不是.pdf没有校验文件头也没有限制响应头中的Content-Disposition。攻击者可以构造一个包含恶意脚本的PDF上传后通过在线预览功能在用户浏览器中执行脚本。针对这类问题我建议从以下几个方面加固文件上传时先校验MIME类型再校验文件头魔数PDF文件头一般为%PDF-不能只信任前端传来的Content-Type。上传目录和访问路径分离文件存储路径不要暴露真实名字使用随机UUID重命名文件文件名不参与页面渲染。在线预览PDF时通过响应头设置Content-Disposition: attachment或者强制X-Content-Type-Options: nosniff让浏览器不要把文件当作页面内嵌资源执行。在SpringBoot项目中可以用全局过滤器统一设置安全响应头但需要注意过滤器的处理顺序。这里要特别提醒很多团队喜欢在全局Filter里对所有请求参数做XSS过滤这种做法在普通表单场景下可行但面对文件上传时要非常小心。如果过滤器粗暴地读取并改写请求体很容易把PDF文件的二进制内容改坏导致上传后的文件无法打开。正确的做法是过滤器的参数清理只针对普通请求参数文件上传接口单独定义处理逻辑。3.3 常见靶场实操记录DVWA、Pikachu、PortSwigger、CTFshow纸上谈兵再多不如在靶场里亲自动手。我练习XSS时常用的平台有DVWA、Pikachu、PortSwigger Web Security Academy和CTFshow各有用处。DVWA是经典的PHP靶场XSS模块分了低级、中级、高级、不可能四个安全级别。低级环境基本没有任何过滤适合初学者理解XSS“为什么会执行”中级会过滤部分标签拼路径时可以看到过滤不完整导致的绕过高级开始有明显防御但仍然存在逻辑缺陷。在这些环境里我最建议养成的习惯是每次提交输入后先看响应HTML里输入值被放到了哪个位置是标签内容、属性值还是JS代码块。不同的位置决定了后续利用方式和防御策略完全不同。Pikachu是中文靶场里面不仅有XSS还有SQL注入、CSRF等配合场景。它的XSS题目更偏向中国开发者日常写代码时容易出现的错误比如安全隐患出现在JSON接口返回、URL转发跳转这些地方。做过一遍能明显提升对“看似正常代码中暗藏风险”的敏感度。PortSwigger的XSS labs更贴近真实业务题目背景包括购物网站、博客系统、客服系统。它的特点是需要考虑同源策略、编码层级、DOM渲染流程不是无脑打一个payload就结束。做这些lab最有价值的地方在于练习“分析数据流”的思路同一个输入什么时候会被URL编码什么时候被HTML实体编码什么时候被JS解码一层层捋清楚后才能真正理解XSS的触发条件。CTFshow的Web方向XSS题目则更偏向CTF赛题思维有些题目需要结合前端代码审计、滤绕过技巧、甚至是浏览器特性。这类题目做多了对“为什么过滤会被绕过”会有更深刻的认识。无论哪个靶场我都不建议直接背诵网上现成的payload。最有效的方法是先尝试一个最基础的探测字符串比如img srcx onerroralert(1)观察浏览器渲染结果和网络请求差异然后逐步删除或替换部分内容确认哪段字符触发了执行。这个过程能帮你在脑子里建立“输入输出映射关系”比刷一百道题都有用。4. 前端安全防御体系从输入过滤到动态防御4.1 输入过滤与输出编码第一道防线XSS防御不能只靠一个操作而是要分多层构建。第一道防线就是输入过滤和输出编码。输入过滤的核心是“只允许合法数据进入”输出编码的核心是“防止数据被当成代码解析”。输入过滤推荐白名单而不是黑名单。比如手机号字段只允许数字和少数几个符号用户名只允许中文、英文、数字和有限下划线长度这样的限制不仅安全还能提升数据质量。黑名单方式最大的问题是永远跟不上绕过思路今天过滤了script明天又有人用svg/onload...后天换个编码方式又能绕过。输出编码则要看具体上下文。HTML标签内容里页面会把解析为标签开始所以需要把、、、、转义成HTML实体。JS字符串里需要处理的是引号、反斜杠、换行符。URL参数里则要对内容做URL编码。很多模板框架默认会做编码比如Vue模板插值、Thymeleaf的th:text但如果用v-html、th:utext就相当于放弃了编码保护必须非常谨慎。4.2 使用SpringBoot全局过滤器统一处理XSS含上传PDF场景对于很多后端项目来说最省力的XSS治理方案是用一个全局过滤器统一拦截请求参数把危险字符替换为安全字符。下面这个SpringBoot过滤器示例是我在一个中小型项目里实际用过的简化版。Component public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; // 对普通请求参数做清理 XssHttpServletRequestWrapper wrapper new XssHttpServletRequestWrapper(req); chain.doFilter(wrapper, response); } static class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { public XssHttpServletRequestWrapper(HttpServletRequest request) { super(request); } Override public String getParameter(String name) { String value super.getParameter(name); return cleanXss(value); } Override public String[] getParameterValues(String name) { String[] values super.getParameterValues(name); if (values null) { return null; } String[] cleaned new String[values.length]; for (int i 0; i values.length; i) { cleaned[i] cleanXss(values[i]); } return cleaned; } private String cleanXss(String value) { if (value null) { return null; } // 实际生产环境建议使用owasp-java-html-sanitizer避免简单替换 value value.replaceAll(, lt;).replaceAll(, gt;); value value.replaceAll(\, quot;).replaceAll(, #x27;); return value; } } }然后在启动类或配置类里注册过滤器Configuration public class FilterConfig { Bean public FilterRegistrationBeanXssFilter xssFilterRegistration() { FilterRegistrationBeanXssFilter registration new FilterRegistrationBean(); registration.setFilter(new XssFilter()); registration.addUrlPatterns(/*); registration.setOrder(1); return registration; } }这段代码有几点需要说明过滤器顺序很重要我习惯把XSS过滤器放在最前面因为它要先于业务处理把脏数据清掉。对参数值做简单替换只适合快速止血不适合做长期方案。被替换成lt;可以防止大多数标签注入但如果业务需要把富文本内容存储到数据库这样替换会直接把用户提交的HTML标签改坏。文件上传场景必须单独考虑。multipart/form-data请求体在Spring中不走getParameter那套机制所以上面的过滤器不会污染文件内容。但如果你额外实现了一个“读取body再清理”的过滤器很可能把PDF文件截断。处理上传PDF时的XSS重点应放在文件类型校验、安全响应头、文件名展示编码而不是清理文件二进制内容。如果想在过滤器里给所有响应统一加安全头可以另外实现一个响应包装器或者更简单地在Nginx/网关层配置add_header X-Content-Type-Options nosniff always; add_header Content-Security-Policy default-src self; script-src self unsafe-inline always; add_header X-Frame-Options SAMEORIGIN always;注意CSP里的unsafe-inline会削弱防护实际项目中要谨慎使用。4.3 前端代码层面的防御实践防御XSS不能全部指望后端过滤前端代码同样要做安全编码。我归纳了五条前端铁律尽量避免使用innerHTML、outerHTML、document.write拼接动态内容。Vue项目禁止随意使用v-htmlReact项目禁止随意使用dangerouslySetInnerHTML。如果必须渲染富文本先经过DOMPurify消毒。不要用eval执行动态拼接的字符串不要用new Function做“灵活处理”。从URL参数、location.hash、postMessage取到的数据必须做类型校验和格式校验后才能使用。前端自身的控制台不要输出敏感信息避免被XSS脚本窃取。CSP是前端防御里最值得投入的配置。一个粗略但有效的CSP策略是这样的Content-Security-Policy: default-src self; script-src self; object-src none; base-uri self这个策略的含义是所有资源默认只允许从当前域名加载脚本只允许当前域名的文件不允许任何外部域名脚本禁止加载object/embed等插件资源。这样即使XSS注入了一个外部脚本标签也会被浏览器拦截。如果项目里确实需要加载第三方JS可以在script-src中按需增加域名白名单但一定不要加https:这种过于宽泛的源否则CSP基本形同虚设。Cookie防护同样重要强烈建议给Session Cookie加上Set-Cookie: sessionIdxxxx; HttpOnly; Secure; SameSiteLaxHttpOnly防止JS读取Secure保证仅HTTPS下传输SameSiteLax可以缓解多数跨站请求伪造场景。这行配置是所有Web应用的标配。4.4 动态防御技术与安全响应机制所谓动态防御是相对于“静态过滤”而言的。静态过滤容易绕过动态防御会结合请求上下文、行为特征、业务语义来做实时判断常见手段包括WAF规则引擎通过正则、语义分析、行为模型识别恶意载荷对可疑请求返回403或阻断。RASP运行在应用内部通过插桩监听危险函数调用比如innerHTML、eval一旦发现数据流可疑就阻断。前端监控捕获CSP违规事件、异常script执行行为上报到安全中心实现快速感知和响应。但我不建议把防御重心完全放在WAF和RASP上这些产品适合作为辅助兜底而不是唯一手段。原因很简单WAF看到的只是请求包很难知道业务内部如何拼接输出RASP虽然能监控运行时行为但部署成本高误报率也不低。最可靠的方式还是从开发规范层面做好输出编码和前端安全编码把漏洞从根源上消除。另外防御体系要定期做“体检”。每次版本迭代后至少要在测试环境跑一遍XSS相关的用例检查新增的页面、接口、上传功能是否引入新的风险。如果公司有条件可以把常见XSS探测字符串做成自动化测试用例接入CI流程避免老问题回归。5. 常见问题排查与实战心得5.1 高频问题速查表现象可能原因解决方案输入框提交后弹出alert刷新页面后消失反射型XSS后端未对输出编码在响应输出前对动态内容做HTML实体编码发帖后每次打开页面都弹窗存储型XSS输入被持久化后未做过滤清理数据库中的恶意内容补输入白名单和输出编码只有URL带有某个参数时才弹窗后端接口没有这个参数DOM型XSS前端把URL参数传入危险的DOM API检查前端代码避免将可控数据传入innerHTML对数据做白名单校验上传PDF后点击文件预览时触发XSSPDF文件内嵌脚本或文件名展示未编码校验文件类型设置安全响应头文件名展示时编码过滤器把用户正常的富文本内容改坏了全局过滤器对富文本内容做了简单替换对富文本字段使用白名单过滤器普通字段才使用统一清洗Cookie被XSS脚本读取会话被劫持Session Cookie未设置HttpOnly设置HttpOnly属性从根源上阻断document.cookie读取页面报错但找不到XSS触发点可能是前端使用了v-html或innerHTML拼接全局搜索相关代码审查数据流引入DOMPurify5.2 我在渗透测试中总结的排错思路很多同学拿到一个疑似XSS的页面后第一反应是打开控制台直接复制网上的payload去打。我的建议恰恰相反先搞清楚“数据是从哪里来、到哪里去”再决定怎么测。第一步打开开发者工具在页面上输入一组明显且唯一的探测字符串比如XSS_TEST_123同时观察Network面板中的请求参数。第二步查看响应HTML中是否原样出现这个字符串如果出现看它落在什么位置是显示在HTML标签内容里还是出现在某个属性值里还是出现在JS代码片段中。第三步针对不同位置输入不同结构的探测字符。比如落在标签内时单测尖括号落在属性内时单测引号和尖括号落在JS中时单测引号和反斜杠。这种“按位置探测”的方法能帮你快速判断出过滤规则长什么样。如果发现输入被删了一部分可以尝试大小写绕过、实体编码绕过、Unicode编码绕过如果输入被原样输出但加了引号匹配就要看能否闭合上下文。但无论怎么绕一旦发现目标系统有成熟的CSP和HttpOnly配置实际利用难度会指数级上升这也说明防御做得好才是根本。5.3 最后分享几个踩坑经验我在实际项目中踩过不少跟XSS相关的坑挑几个典型的说一下。第一个坑全局过滤器“一刀切”导致业务数据被改。早年我为了图省事在过滤器里把所有参数里的全部替换成lt;结果用户提交的富文本内容、SQL语句合法业务场景、甚至邮件模板全部被转义后台页面显示一片HTML源码改了好几轮才收敛。后来我改成只对普通查询参数做清洗对富文本字段用专门的富文本过滤库对文件上传接口直接跳过参数清理。第二个坑只测了输入框没测URL参数。有一次评审一个活动页面所有输入框都测过没问题结果发现活动页的分享链接里带有fromxxx参数前端代码直接把它拼到页面标题里。这种像“隐形入口”的参数最容易被忽略后来我每次测试都会把页面里所有location.search、location.hash能取到的值都过一遍。第三个坑过滤器解决了当前漏洞但引入了新的“误拦”。比如过滤了导致前端JSON字段解析失败或者过滤了导致邮箱地址被改坏。这提醒我们安全方案必须小步上线、充分回归测试不能“为了安全牺牲所有正常功能”。第四个坑忽略第三方库引入的风险。有些项目的XSS漏洞不是自己代码写的而是前端引了一个老版本的富文本编辑器或者后端引入了一个有漏洞的JSONP库。安全审查一定要带上“依赖清单审计”不能只盯自己的业务代码。我个人体会最深的一点是XSS防御没有一劳永逸的方案它需要开发、测试、安全三方配合从代码规范、框架选型、运行时监控多个维度共同发力。每次改动上线前多想一想“这个输入有没有可能出现在HTML上下文中有没有可能被JS读取”就能把大多数风险挡在门外。如果你团队里还有同事觉得XSS就是“弹个窗”请把这篇内容转给他看看。
返回列表