消除@platejs/media中的 js-video-url-parser ReDoS 漏洞:从依赖清理到自研解析器的安全修复实践
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
本文以 plate 仓库中 2026-04-24-js-video-url-parser-redos.md 修复计划与 安全解决方案记录 为主线,完整复盘一次真实的第三方依赖 ReDoS(正则表达式拒绝服务)漏洞处置:如何识别js-video-url-parser的 CVE-2026-5986 暴露面、用URLAPI 自研解析器替换依赖、用计时回归测试锁定性能、以及通过 changeset 与自动发布工作流完成发布闭环。读完本文,你将掌握一套可复用的“依赖 CVE 修复方法论”,并能在@platejs/media中自行验证 YouTube / Vimeo / Dailymotion / Youku / Coub 五种平台的视频链接解析行为。
背景:媒体嵌入功能与 ReDoS 攻击面
@platejs/media是 plate 富文本编辑器生态中的媒体插件包(package.json)。它负责把用户粘贴或输入的原始链接(如 YouTube、Vimeo 视频地址)解析为统一的嵌入数据,再插入编辑器的 mediaEmbed 节点。解析入口是 parseVideoUrl.ts 导出的parseVideoUrl,它被 insertMediaEmbed.ts 与 submitFloatingMedia.ts 通过urlParsers机制组合调用。
问题出在该解析器历史上依赖第三方包js-video-url-parser。该包被安全公告 CVE-2026-5986 标记为存在 ReDoS 漏洞:其getTime相关的时间参数正则解析在遭遇精心构造的输入时会发生灾难性回溯,导致解析线程长时间阻塞。在本仓库中,pnpm-lock.yaml曾锁定js-video-url-parser@0.5.1,packages/media/package.json也直接声明了该依赖,两个位置共同构成了发布包的漏洞暴露面。
ReDoS 的实际表现
修复计划中记录了漏洞的实证复现方式:
- 复现命令:
bun test packages/media/src/lib/media-embed/parseVideoUrl.spec.ts,针对一个25 位非法t=参数值的输入,解析耗时约360ms后失败; - 恶意输入形如
https://www.youtube.com/watch?v=M7lc1UVf-VE&t=111...111x——一段超长数字串后跟一个不匹配的x,正是触发正则回溯的经典载荷; - 安全扫描器会同时报告
js-video-url-parser@0.5.1同时出现在package.json与pnpm-lock.yaml。
对于服务端渲染或 AI 批量处理场景,这种毫秒级停滞可被放大为拒绝服务,因此该问题被评级为 medium 严重度(见解决方案文档的 frontmatter)。
为什么“升级版本”与“局部加固”都行不通
修复计划明确排除了两条看似省事的路径,这对任何依赖 CVE 处置都有参考价值:
- 版本升级不可行:当时已知受影响版本覆盖
0.5.0与0.5.1,在公告语境下不存在可安全升级的目标版本,也就是“没有 safe bump target”; - 仅在 Plate 调用点加固不够:即便在
parseVideoUrl外层做输入校验或降级处理,被公告标记的漏洞依赖仍留在已发布包的依赖图(package graph)中,扫描器依然会告警。
因此结论是:持久性修复必须移除依赖,并把 Plate 真正需要的这一小片解析逻辑收归自有代码。这既是安全修复,也是一次“控制面收敛”——第三方包为通用场景提供了大量本仓库用不到的元数据与时间解析能力,而这些能力恰恰是攻击面所在。
解决方案:用 URL API 自建零正则解析器
修复的核心思路在 parseVideoUrl.ts 中落地:放弃正则驱动的字符串匹配,改用标准URLAPI + 少量确定性字符级校验函数。整个文件不引入任何正则表达式。
解析主流程
const parseUrl = (url: string) => { if (!isUrl(url)) return; try { return new URL(url); } catch { return; } };parseUrl先用platejs导出的isUrl做协议/形态粗校验,再用new URL(url)完成标准化解析;非法 URL 会抛异常并被捕获返回undefined。主函数parseVideoUrl随后遍历内置的提供商常量表,逐个尝试解析:
export const parseVideoUrl = (url: string): EmbedUrlData | undefined => { const parsedUrl = parseUrl(url); if (!parsedUrl) return; for (const provider of VIDEO_PROVIDER_VALUES) { const id = parsersprovider; if (!id) continue; const embedUrl = `${providerUrls[provider]}${id}`; return { id, provider, sourceKind: 'url', sourceUrl: embedUrl === url ? undefined : url, url: embedUrl, }; } };返回的EmbedUrlData结构定义在 parseMediaUrl.ts,包含id、provider、sourceKind、sourceUrl、url五个字段。值得注意的是:当输入 URL 本身就是官方 embed 地址(如https://player.vimeo.com/video/97276391)时,embedUrl === url,此时sourceUrl会被置为undefined,避免冗余存储。
该结构随后还会经过 parseMediaUrl.ts 的二次加固:只放行http:/https:协议,其余协议一律丢弃,以防御 XSS。
防 ReDoS 的字符级校验工具
真正消除灾难性回溯的是下面这套基于字符码的判断函数(全部为线性扫描,无正则、无回溯):
isAlphaNumeric(value):逐字符检查是否为0-9、A-Z、a-z;isNumeric(value):逐字符检查是否为0-9;isYoutubeId(value):要求长度严格为 11,且仅含-、_或字母数字;stripNonAlphaNumericSuffix(value):找到第一个非字母数字字符的位置并截断其后内容,用于处理 Dailymotion/Youku 链接末尾的标题后缀。
这些工具函数的时间复杂度均为 O(n),与输入长度线性相关,从根本上不存在正则引擎的指数级回溯路径。
五家平台的 ID 提取策略
| 平台 | 支持的 host 形态 | ID 提取来源 |
|---|---|---|
| YouTube | youtu.be、youtube.com、*.youtube.com | ?v=/?ci=查询参数;路径中embed、v、vi、videos、shorts、live段之后的位置 |
| Vimeo | vimeo.com、*.vimeo.com、vimeopro.com、*.vimeopro.com | 路径中从后向前找到的第一个纯数字段 |
| Dailymotion | dai.ly、dailymotion.com、*.dailymotion.com | dai.ly取首段;其余取video段之后的位置,并截掉非字母数字后缀 |
| Youku | youku.com、*.youku.com | VideoIDS查询参数;路径中embed、sid段;或以id_开头的段(截掉.html等后缀) |
| Coub | coub.com、*.coub.com | view或embed段之后的位置 |
解析得到的 embed 前缀常量同样定义在 parseVideoUrl.ts:YouTube 为https://www.youtube.com/embed/,Vimeo 为https://player.vimeo.com/video/,Dailymotion 为https://www.dailymotion.com/embed/video/,Coub 为https://coub.com/embed/,Youku 为https://player.youku.com/embed/。
测试策略:供应商变体 + 计时回归双保险
修复计划强调“在删除解析器依赖之前,先用测试保住公共 helper 行为”,并专门克隆了上游jsVideoUrlParser仓库交叉核对 provider 变体。落地后的测试文件是 parseVideoUrl.spec.ts,其测试矩阵分为四层:
1. 主 URL 与变体 URL
- YouTube 主 URL:
https://www.youtube.com/watch?v=M7lc1UVf-VE; - YouTube 变体:
youtu.be短链、m.youtube.com/details?v=、/embed/路径、/v/路径、gdata.youtube.com/feeds/api/videos/路径; - Vimeo 主 URL 与变体:
/channels/staffpicks/、/album/2903155/video/、/groups/shortfilms/videos/、/showcase/12345/video/、vimeopro.com/staff/frame/video/,以及“已是 embed 地址时sourceUrl应为undefined”的断言。
2. 非 YouTube 平台
- Dailymotion:
/video/x7tgcz与带_后缀标题的/video/x1e2b95_bruce-lee-nin-kayip-kedisi_animals、短链dai.ly/x1e2b95; - Youku:
v.youku.com/v_show/id_XMzI1NjYxNTI=.html(需去掉.html)、player.youku.com/player.php/sid/XMTQ3OTM4MzMxMg==/v.swf、static.youku.com/...?VideoIDS=...%3D%3D(查询参数需 URL 解码); - Coub:
coub.com/view/abc123与coub.com/embed/by7sm(embed 形态sourceUrl为undefined)。
3. ReDoS 计时回归(核心)
it('does not backtrack on invalid time parameters', () => { const url = `https://www.youtube.com/watch?v=M7lc1UVf-VE&t=${'1'.repeat(25)}x`; const start = performance.now(); expect(parseVideoUrl(url)).toEqual({ id: 'M7lc1UVf-VE', provider: 'youtube', sourceKind: 'url', sourceUrl: url, url: 'https://www.youtube.com/embed/M7lc1UVf-VE', }); expect(performance.now() - start).toBeLessThan(100); });这条用例直接复刻漏洞载荷(25 位数字 + 非法x),断言两件事:一是解析结果不受影响(t参数被忽略、ID 仍正确提取);二是整体耗时必须小于 100ms——与此前 ~360ms 的失败表现形成对照。它保证了未来任何重新引入正则解析的改动都会被 CI 立即拦住。
4. 边界输入
parseVideoUrl('not a url')返回undefined。
发布链路:changeset + 自动合并 Release PR
由于@platejs/media是已发布包,本次修复涉及“包输出或用户可见依赖变化”,因此必须走 changeset 流程:
- 运行
pnpm install后,pnpm-lock.yaml中的js-video-url-parser@0.5.1被移除; - 新增 changeset 文件(本仓库 changeset 使用方式见 .changeset/README.md),记录版本变更意图;
- 执行完整验证链:
bun test packages/media/src/lib/media-embed/parseVideoUrl.spec.ts→pnpm turbo build --filter=./packages/media→pnpm turbo typecheck --filter=./packages/media→pnpm lint:fix→pnpm check。
修复计划还提到一处自动化增强:为 changeset PR 增加“自动发布”勾选项。其设计为——PR 正文带一个受管理的复选框,release 工作流读取已合并PR 的复选框状态,若为勾选则对生成的[Release] Version packagesPR 启用 auto-merge。配套增加了检测 changeset PR、保留受管复选框状态、忽略受管块之外零散复选框文本的共享辅助函数,并新增changeset-auto-release工作流以在 changeset PR 上持续保留该复选框,同时更新release.yml读取合并 PR 的勾选结果。相关验证包括bun test tooling/scripts/auto-release-pr.test.mjs、node --check、git diff --check、YAML 解析与pnpm check。
可复用的 ReDoS 依赖修复清单
综合本次修复的完整过程,可以沉淀出如下适用于任何 npm 包的处置流程:
- 定位暴露面:确认漏洞包同时出现在
package.json(直接依赖)与锁文件(传递依赖)的哪些位置,评估受影响版本范围; - 复现攻击形状:构造触发正则回溯的最小载荷,用计时测试(如
performance.now())量化延迟,作为修复前后的对照基线; - 评估升级可行性:若受影响范围覆盖当前锁定版本且无安全替代版本,果断放弃“升版本”路线;
- 移除依赖并自建最小实现:只保留业务真正需要的解析面,优先用标准库(如
URLAPI)替代正则,字符级线性扫描替代复杂匹配; - 用测试锁行为:迁移上游测试用例覆盖各 provider 变体,并为 ReDoS 形状专门添加计时回归断言;
- 清理锁文件:执行
pnpm install(或对应包管理器)让锁文件摘除漏洞包,确保发布图干净; - 走发布流程:通过 changeset 记录版本变更,完成 build、typecheck、lint 全量验证后合入;如有自动发布需求,可在 changeset PR 中启用受管理的自动合并复选框。
这套方法不仅适用于js-video-url-parser,对任何“正则驱动的第三方解析器 + CVE 公告”组合都同样成立——把攻击面从不可控的第三方正则引擎,收回到可控、可测、可审计的自有代码中。
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考