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

jQuery 老项目安全升级指南:文件解析、漏洞修复与迁移实战

jQuery 老项目安全升级指南:文件解析、漏洞修复与迁移实战 ★ FEATURED ARTICLE
jQuery 这个库用了十几年很多人对它的认知就是“一个 jquery.min.js 引进去就完事”。直到某天做安全审查发现老项目还在跑 jQuery 1.x去官网下载页一看文件五花八门又开始研究漏洞修复记录和各个文件到底有什么用。今天这篇就把我在升级过程中的整理完整写出来既有文件功能对照也有漏洞修复思路和落地步骤给同样被困在老项目里的同学做个参考。1. 先把 jQuery 包里的这些文件认全很多人的项目里只有一个 jquery.min.js但对 jQuery 的发行包结构并不熟悉。尤其到了 3.x官方直接给了好几种文件名字就差一两个词功能却差不少。用错文件轻则功能缺失重则上线后白屏所以第一步就是把dist目录里这些文件搞清楚。1.1 一个发行包里到底躺着哪些文件以 jQuery 3.7.x 为例你从 npm 安装或者官网下载后核心文件一般都在dist/目录下文件名压缩过吗包含模块适合场景jquery.js否完整版本地开发调试报错信息可读能直接打断点jquery.min.js是完整版生产环境常规选型体积小加载快jquery.slim.js否删减版只用选择器和事件处理的调试场景jquery.slim.min.js是删减版生产环境极限压缩体积最小但功能少jquery.min.map否源码映射浏览器 DevTools 调试压缩代码时使用为什么会有压缩版和未压缩版因为生产环境在意传输体积和解析速度。压缩的不是简单去掉空格它还会把变量名换成短名称、删除注释、合并表达式最终代码几乎不可读。jquery.min.js的大小通常在 90KB 左右未压缩版jquery.js则在 270KB 上下差距很明显。但压缩带来的副作用就是报错信息难懂。比如线上代码报了个n.fn is not a function你看半天也看不出是哪一段逻辑。这时候.map文件就有用了也就是 source map 源码映射。浏览器在加载压缩脚本时如果压缩文件末尾有//# sourceMappingURLjquery.min.mapDevTools 就会自动把压缩代码中的行号和变量位置映射到原始源码上方便你定位问题。这里要提醒一句source map 在生产环境属于敏感文件。它会把完整的源码结构暴露出来如果项目是挂在公网上的我一般不建议默认把.map文件一起部署。真正需要排查线上压缩代码问题时临时把 map 放到同目录定位完再移除是更稳妥的做法。1.2 完整版和 slim 版不是“多几个方法”的区别slim 版本是 jQuery 3.x 开始引入的概念它做的事情不是简单去掉某个 API而是直接拿掉了两个大模块Ajax和Effects。拿掉 Ajax意味着$.ajax、$.get、$.post、load()这一类方法全部不存在了。拿掉 Effects意味着animate()、fadeIn()、fadeOut()、slideUp()、slideDown()这些动画方法也都没了。如果你在引了 slim 的页面里调用$.ajax()控制台会直接报$.ajax is not a function动画相关方法也同理。那 slim 存在的意义是什么如果你的项目只用了 jQuery 的选择器、事件绑定、DOM 操作这些核心能力根本没有 Ajax 和动画需求引 slim 可以省掉一部分体积。它更适合那种“杀鸡用牛刀但牛刀又太大”的场景。判断项目能不能用 slim我的习惯是把代码里所有$.ajax、$.get、$.post、animate、fade、slide全局搜索一遍一个都搜不到才敢换。只要有一个地方用到了老老实实上完整版别为了省几十 KB 给自己埋雷。另外在 webpack/Vite 这类构建工具项目里import $ from jquery默认导入的是完整版一定要用 slim 得写成import $ from jquery/dist/jquery.slim.min.js不写清楚构建工具不会自动帮你切。1.3 jquery-migrate.js 也是包里的常客但它不是 jQuery 本体很多人会把 jquery-migrate.js 当成 jQuery 的一部分其实它是一个独立的辅助插件。作用一句话就能说清楚当你的项目从旧版本 jQuery 升级到新版本时旧代码里那些已经删除或废弃的 API 不会直接让整个程序崩掉migrate 会在控制台打印警告告诉你哪些地方用了不兼容的写法。举例来说项目里如果还写着$.parseJSON、.bind()、.size()新版本 jQuery 里这些 API 要么没了要么是废弃状态。migrate 会把它们找出来输出类似 “JQMIGRATE: $.parseJSON is deprecated” 的提示并尽量用兼容逻辑把功能顶上。migrate 是过渡期工具不建议长期放在线上。它本身会带来额外的修正逻辑和性能损耗正确用法是升级期引入跑一遍测试按警告改完代码后再从项目里移除。这一点后面实战部分再细讲。2. 漏洞修复记录3.5.0 是绕不开的分水岭jQuery 的漏洞历史里有一组编号经常被安全扫描工具拎出来说CVE-2020-11022 和 CVE-2020-11023。这两个漏洞的修复版本都是 jQuery 3.5.0也就是说凡是低于 3.5.0 的版本都存在被利用的风险。很多老项目长期停留在 1.x、2.x、3.4.x安全审查一查就是一个高危原因就在这里。2.1 这两个漏洞到底是怎么回事要理解这个漏洞先要知道 jQuery 的一个使用习惯$(htmlString)可以直接把一个 HTML 字符串变成 DOM 元素。比如$(div classboxhello/div)jQuery 会识别出这是一个 HTML 片段而不是选择器然后把它插入页面。问题出在 jQuery 内部对 HTML 字符串的预处理。老版本里有一个函数叫htmlPrefilter它的职责之一是先用正则对传入的 HTML 做一次修正把一些畸形标签或历史遗留写法处理成标准形态。这套逻辑设计之初是为了兼容老浏览器的但是正则理解不了真正的 HTML 语法它只能按模式匹配。攻击者要做的事情就是构造一个能让这个正则预处理“判断失误”的字符串。一旦字符串没被正则拦截住它就会被原样交给浏览器的innerHTML解析。浏览器解析 HTML 的规则和 jQuery 的预期并不完全一致于是某些包含在标签属性里或者藏在特殊标签结构中的脚本逻辑就被执行了。这是典型的 XSS 跨站脚本攻击。用大白话说jQuery 老版本想当安检员但它只凭一张标签贴纸判断来者身份攻击者换了个包装就混过去了。整个过程不需要用户交互也不需要登录态只要页面上有接口数据或 URL 参数被拼进 jQuery 的 HTML 处理链里就可能中招。2.2 影响范围和触发条件CVE-2020-11022 影响的是 jQuery 3.5.0 之前的所有版本CVE-2020-11023 影响的是 3.0.0 到 3.4.1 的版本但无论如何结论都一样低于 3.5.0 的 jQuery 在安全层面是不合格的。不过也别被“高危漏洞”四个字吓到。漏洞触发是有前提条件的必须有不受信任的 HTML 内容传入 jQuery 的$()、.html()、.append()、.prepend()这类接口。如果你的项目所有 HTML 都是后端模板里的静态字符串没有任何用户输入或第三方数据参与拼接那实际风险很低。但问题在于大多数老项目都存在评论渲染、URL 参数回显、接口返回富文本这类场景你很难保证每一处都做了严格过滤。这也是安全修复中最尴尬的地方漏洞真实存在触发路径不一定能靠肉眼找全。所以不管当前有没有被实际利用版本升级是唯一稳妥的出路。2.3 jQuery 3.5.0 到底改了什么jQuery 3.5.0 的修复并不是简单给过滤正则加几个字符而是调整了整个处理链路的重心。新版本里htmlPrefilter不再负责那种“修改 HTML 字符串形态”的工作它在处理 HTML 片段时会更依赖浏览器原生的解析能力比如使用template元素这类标准机制去解析和承载 HTML。浏览器原生解析器是按照 HTML 规范逐字符处理的不会再被正则那种“看起来像不像标签”的判断方式绕过去。换句话说新版把安全边界交还给了浏览器和开发者。jQuery 自己不再尝试替你做“聪明的清洗”因为从设计上就不该由 jQuery 来判断一段 HTML 是否安全。这带来的一个明显变化是升级后如果有些老页面依赖 jQuery 当年那种“自动修正畸形 HTML”的行为展示效果可能会出现细小的差异后面回归测试时要注意。顺带一提3.5.0 之后jQuery 的版本演进并没有停3.6.x、3.7.x 一直在发布。对我来说3.5.0 只是一个必须跨过的最低门槛实际项目里我会直接升到 3.7.x 的最新稳定版避免在修复版上又堆积了半年的旧债。3. 实战给老项目做一次安全升级升级 jQuery 说难不难说简单也不简单。文件一换容易真正的坑在兼容性和业务回归上。我习惯把整个过程拆成“体检—换包—迁移—回归”四步走。3.1 升级前先搞清楚现状动手换文件之前先回答几个问题当前项目用的 jQuery 版本是多少有多少第三方插件依赖 jQuery它们对版本有没有硬性要求代码里有没有把用户输入、接口数据直接拼进 HTML 字符串查版本最简单的方式是打开浏览器控制台执行jQuery.fn.jquery会直接输出当前版本号。如果是全局引用的老项目也可以在页面源码里搜jquery找到 script 标签。紧接着要清点第三方插件。这一步经常被忽略却是翻车重灾区。很多老插件停留在 jQuery 1.x/2.x 时代直接配上 jQuery 3.7.x 可能根本跑不起来比如某些旧的轮播图、表单验证、弹窗组件。我的做法是把所有 script 引用列个清单逐个确认插件的兼容版本。如果一个插件已经停止维护且明确不兼容新 jQuery就得提前想好替代方案要么找功能等价的现代库要么自己用原生代码重写那一小块功能。最后把代码里所有可能接收外部数据的 HTML 操作点列出来重点搜.html()、.append()、.prepend()、.after()、.before()、$(htmlString)这几种调用。不用急着改升级前先知道风险面在哪里。3.2 不同部署方式的具体升级操作老项目最常见的两种部署方式一种是纯静态页面script 直接引文件另一种是用了构建工具打包。纯静态页面的流程最直接下载对应版本的jquery.min.js我一般从 npm 包或官方渠道拿文件确保文件没有被篡改。把原来的script srcjquery-1.x.min.js/script替换成新版本。页面里所有依赖 jQuery 的插件 script 引用顺序不要动必须保证 jQuery 在插件之前加载。在测试环境全量回归一遍重点看控制台有没有报错。如果是构建工具项目操作就变成 npm 层面的升级npm install jquery3.7.1 --save在模块代码里保持原来的import $ from jquery即可重新构建后自然用的是新版本。这里有个细节留意一下如果项目通过 ProvidePlugin 之类的插件把$自动注入所有模块升级后要重新构建并检查有没有缓存问题。另外使用 CDN 加载 jQuery 的项目建议给 script 加上 SRI子资源完整性校验。SRI 的作用是确保浏览器加载的脚本内容和预期一致即使 CDN 被篡改浏览器也会拒绝执行。生成校验值的方式是用 openssl 对文件做 SHA-384 哈希然后转成 base64openssl dgst -sha384 -binary jquery.min.js | openssl base64 -A把输出填到 script 标签的integrity属性里再加上crossoriginanonymous。如果升级后打开页面发现报错、jQuery 没有定义先检查 integrity 是否匹配这是最常见的问题之一。3.3 用 jQuery Migrate 处理不兼容代码换上新版本后业务代码不会立刻全崩但会出现各种怪异现象。原因是 3.x 版本删除了一部分 API同时对另一些 API 做了行为调整。这时候就轮到 jquery-migrate 上场了。操作顺序是这样在页面里引入 migrate 插件版本要和 jQuery 主版本匹配。比如升到 3.7.x就引入对应 3.x 的 migrate。打开控制台跑一遍项目的核心流程。控制台会输出所有 “JQMIGRATE” 开头的警告逐条定位到业务代码并修改。确认所有警告清零后移除 migrate再跑一遍回归。实际使用中最高频的兼容问题集中在这几个 API 上旧写法新推荐写法说明.bind(event, fn).on(event, fn)bind/off 在 3.0 后被移除.unbind(event).off(event)同上.delegate().on()事件委托统一用 on.size().lengthsize 方法早就被移除了.andSelf().addBack()方法改名$.parseJSON(text)JSON.parse(text)直接调用原生 JSON$.isArray(arr)Array.isArray(arr)原生方法$.trim(str)String.prototype.trim建议直接字符串原生处理migrate 警告是升级时最容易忽视的资源它比任何文档都准确直接告诉你哪一行代码出了问题。但要注意migrate 的兼容逻辑是尽力而为不可能 100% 还原旧行为。真正稳妥的做法是把警告当作必改项而不是当作可以忽略的提示。3.4 回归测试要覆盖哪些点说到回归测试很多人以为页面能打开、控制台没报错就够了其实还会漏掉很多细节。根据我实际踩过的坑至少要覆盖所有动态绑定的事件是否正常触发特别是用.on()绑定到未来元素的委托事件。页面里所有 Ajax 请求是否正常发出和拿到返回值如果项目里同时引了 slim 版本这一步会直接暴露问题。所有动画效果是否正常执行fadeIn、slideUp这一类方法有没有失效。使用$(document).ready()的页面是否在 DOM 加载后正确执行jQuery 3.x 对 ready 回调的异步行为有调整个别场景下执行顺序和旧版不一样。用到了:hidden、:visible、:animated这类伪类选择器的判断结果是否和之前一致。回归测试最好找一份业务核心流程清单从页面初始化到用户操作都走一遍。我的经验是升级 jQuery 这种基础库宁可多花一个下午做全量回归也不要觉着“就是换个文件而已”。4. 修复之外日常怎么跟 XSS 和平相处版本升级能堵住 jQuery 自身的漏洞但解决不了所有 XSS 问题。说白了jQuery 3.5.0 修的是“jQuery 被绕过的漏洞”不等于代码里可以肆无忌惮地往里插 HTML。安全习惯如果不变升级只是暂时安心。4.1 插入内容时要严格区分 html() 和 text()我见过非常多的 XSS 问题根源不是用了老版本的库而是分不清html()和text()的语义。$(el).html(content)会把 content 当作 HTML 解析标签会被浏览器真正解释执行。$(el).text(content)会把 content 当作文本节点处理哪怕里面写了script也只是显示成字符不会执行。所以有一个非常朴素但是有效的规则凡是来自用户输入、URL 参数、接口返回、第三方平台的内容默认一律用text()插入。只有业务明确要求展示富文本时才考虑走 HTML 渲染通道。如果有内部模板需要把变量插到 HTML 字符串里也要先做转义。最常见的转义函数长这样function escapeHtml(str) { return String(str) .replace(//g, amp;) .replace(//g, lt;) .replace(//g, gt;) .replace(//g, quot;) .replace(//g, #39;); }把变量交给escapeHtml()之后再拼 HTML 字符串能挡掉大部分通过script、事件属性、伪协议注入的攻击。这函数很简单但真到了业务代码里很多人嫌麻烦就直接拼了等出了事又怪框架。4.2 富文本内容必须走白名单过滤有些业务确实需要用户输入富文本比如发帖、评论带格式、后台编辑公告。这种情况千万不要直接$(el).html(userRichText)正确的姿势是先让富文本经过一个白名单清洗库处理。业内比较成熟的做法是使用 DOMPurify 这类工具它会把 HTML 里允许的标签和属性过滤一遍把onclick、onerror、javascript:这类危险内容直接剥掉。白名单过滤的另一个好处是它对新旧版本的 jQuery 都有效。就算哪天某个版本的 jQuery 又曝出 HTML 处理漏洞只要入口处的数据已经被清洗过攻击链就断在前面了。把安全边界前置永远好过在库内部跟漏洞赛跑。4.3 依赖安全要纳入例行检查客户端库的安全问题有一个特点代码不跑起来你根本感知不到它存在漏洞。老项目最怕的就是那种“上线后没人动一检查全高危”的状态。所以依赖安全检查要变成例行动作而不是安全审查来了才突击。npm 项目可以直接跑npm audit它会根据已安装包的版本和公开漏洞库做比对列出哪些依赖存在漏洞、影响程度如何、建议升级到什么版本。如果项目里还有一堆没纳入依赖管理的静态文件那就得手动维护一份“前端资源清单”定期过一遍每个库的最新版本和安全公告。我个人的习惯是给老项目建立版本升级节奏不用追求每次都升到最新但至少要保证所有线上依赖不在已知高危漏洞列表里。像 jQuery 这种底层库升一次级影响面大所以更要趁早做别拖到被扫描工具逼着改的时候才动手。5. 常见问题与排查实录升级过程中有几类问题几乎每个项目都会遇到。整理成速查表可以直接照着排查。问题现象常见原因排查与解决办法$.ajax is not a function引用了 slim 版本Ajax 模块被裁掉换成完整版jquery.min.js或确认项目不用 Ajax 再用 slimfadeIn is not a function/ 动画全部失效slim 版本没有 Effects 模块换成完整版检查所有动画调用页面打开提示jQuery is not definedscript 加载顺序颠倒、SRI 校验失败、CDN 文件被拦截确认 jQuery 先于插件加载检查 integrity 值与文件是否匹配本地部署一份备用控制台出现大量 JQMIGRATE 警告业务代码还在用旧版 API借助 migrate 插件逐条定位并改写改完移除 migrate压缩版报错后看不到具体业务位置生产环境使用 min 文件变量名被压缩本地开发用未压缩版线上排查时临时引入 map 文件定位后移除升级后所有事件都绑不上代码里混用了.bind()、.delegate()等旧 API全局搜索.bind(、.delegate(、.unbind(统一改成.on()/.off()动态添加的元素点击无效直接绑定的旧逻辑遗漏了委托事件检查是否用了.on(click, selector, fn)做事件委托动态元素必须靠委托这里重点说下 “jQuery is not defined” 这个现象它看着像是库没引进来实际原因可能很隐蔽。除了 script 顺序和 SRI 校验失败外还有一个高频场景页面里同时引入了多个版本的 jQuery后加载的版本把全局$覆盖了而老插件保存的还是旧版本引用。这种情况下控制台报错的位置常常千奇百怪排查时要先确认全局只有一个 jQuery 实例。另外一个坑是升级后事件出现“执行两次”。这种情况通常不是因为 jQuery 本身出了问题而是代码里同时用了$(document).ready(fn)和DOMContentLoaded监听或者某段初始化逻辑在页面里被加载了不止一次。升级前这些重复调用可能被旧版本的执行顺序掩盖了升级后异步行为变化让问题暴露出来。排查时直接在控制台给事件绑定打 log看看触发了几次往往能快速定位到是哪个文件重复初始化。还有一个很容易被忽略的点就是升级后个别选择器结果变了。jQuery 3.x 对部分伪类选择器和属性选择器的处理方式更贴近浏览器原生标准旧版本里一些“错误但能用”的写法结果会和以前不一样。比如$(div:first)这类写法在 3.x 里性能和结果都有细微调整。遇到选择器结果异常时先别怀疑是新版 bug大概率是旧代码依赖了一些已经被修正的怪癖行为。我个人操作这类升级时有一个固定的习惯先把文件版本换掉让业务方在测试环境跑一遍看能不能正常运转再挂上 migrate 插件花一个完整迭代把所有弃用 API 清干净最后移除 migrate再做一轮全量回归。这样能把“升级 jQuery”这件事拆成两个独立的风险点出问题的时候好定位。另外在安全这件事上永远不要指望任何库帮你兜底入库的内容先做转义和过滤比事后打补丁管用得多。
阅读完成 · 觉得有帮助?
咨询建站