ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Folo Desktop v1.12.0 更新全解析:URL Scheme 协议白名单加固、订阅限额提示优化与稳定性修复实录

2026/9/17 8:46:42 拓冰建站 浏览量
Folo Desktop v1.12.0 更新全解析:URL Scheme 协议白名单加固、订阅限额提示优化与稳定性修复实录 Folo Desktop v1.12.0 更新全解析URL Scheme 协议白名单加固、订阅限额提示优化与稳定性修复实录版本变更记录见 apps/desktop/changelog/1.12.0.md。本篇文章以该 changelog 为主体逐一还原每一项改进与修复背后的源码改动、测试用例与工程决策帮助开发者在阅读发布说明时能直接对应到 Folo 桌面端Electron与共享代码库中的具体实现。版本概览v1.12.0 改了什么v1.12.0 是 Folo Desktop 的一次偏「稳」的版本发布没有新增功能性大特性重点放在安全加固、错误处理体验与若干回归修复上。按 changelog 的官方表述本次发布包含两个 Improvements改进与四个 No longer broken修复改进一加固自定义 URL 集成只有受信任的协议URL Scheme才能被打开改进二当 RSSHub 订阅达到配额上限时向用户展示的错误信息更友好、更安全修复分享弹层执行完动作后残留不关闭、Stripe 逾期订阅升级失败、订阅分类的折叠箭头旋转失效、以及全局边框样式错乱。从仓库 Git 历史看这些改动集中在 2026-07-25 至 07-28 间合入 main并随 v1.12.0 桌面端发布release commit0eb0b13da。其中 URL 集成加固由社区贡献者sebastionossSebastion提交changelog 的 Thanks 部分对此特别致谢。一、自定义 URL 集成加固从「包含 :// 即放行」到「协议白名单」这是本版本最具技术含量的一项安全修复涉及桌面端主进程main process的 IPC 服务。相关提交为bd91b015efix(desktop/ipc): allowlist URL scheme protocols in openURLScheme改动落在两个文件上apps/desktop/layer/main/src/ipc/services/integration.tsapps/desktop/layer/main/src/ipc/services/integration.test.ts1.1 问题根源渲染进程字符串直达shell.openExternalFolo 桌面端的「集成Integration」能力允许用户把正在阅读的条目一键发送到 Obsidian、Bear、Drafts、Things、Notion、DEVONthink 等第三方应用。其底层通过integration.openURLScheme这个 IPC 方法把渲染进程renderer提交的 scheme 字符串直接交给 Electron 的shell.openExternal去唤起系统注册的协议处理器。修复前的校验逻辑非常脆弱——只检查字符串里是否包含://// 修复前的实现伪代码还原 if (!scheme.includes(://)) { throw new Error(Invalid URL scheme format. Must include protocol (e.g., app://)) } await shell.openExternal(scheme)风险在于shell.openExternal会把任何 OS 注册过的 scheme 交给系统分发而渲染进程在 Electron 应用中是可以被不受信任的 RSS 条目内容feed 里的 HTML经由 XSS 链路触达的。Electron 官方文档也明确警告过不要把不可信 URL 直接传给shell.openExternal。file://、smb://、ms-msdt:、search-ms:、jar:、res:、javascript:、data:、vbscript:等 scheme 都存在已知的利用链file://——本地文件泄露smb://——Windows 下可窃取 NTLM 凭据ms-msdt:——MSDT / Follina 风格远程代码执行RCEjar:/res:——Java / Windows 资源类的历史利用向量javascript:、data:、vbscript:——脚本注入面。也就是说旧实现只要攻击者能塞进一个形如file:///etc/passwd的字符串它就会越过://检查直达shell.openExternal。1.2 新实现严格 URL 解析 硬编码白名单修复后的openURLScheme完整逻辑位于 integration.ts核心分三步用new URL(scheme)严格解析并提取protocol去掉尾部:并转小写解析失败直接抛出「Invalid URL scheme format」用ALLOWED_URL_SCHEME_PROTOCOLS白名单校验 protocol只有白名单内的协议才会继续走日志脱敏与shell.openExternal。白名单定义在 integration.tsconst ALLOWED_URL_SCHEME_PROTOCOLS new Setstring([ http, https, mailto, obsidian, bear, drafts, things, notion, x-devonthink, ]) function isAllowedURLSchemeProtocol(protocol: string): boolean { return ALLOWED_URL_SCHEME_PROTOCOLS.has(protocol) }命中拒绝时的错误信息也足够可读会列出全部允许项URL scheme file:// is not allowed. Allowed schemes: bear, drafts, http, https, mailto, notion, obsidian, things, x-devonthink.白名单的构成与 UI 层内置的集成示例一一对应。渲染进程侧 apps/desktop/layer/renderer/src/modules/integration/url-scheme-handler.ts 中getExamples()返回的默认模板正是目标应用协议内置示例 schemeObsidianobsidianobsidian://new?vaultMyVaultname[title]content[content_markdown]Bearbearbear://x-callback-url/create?title[title]text[content_markdown]tagsfollowDraftsdraftsdrafts://x-callback-url/create?text[title]%0A%0A[content_markdown]Thingsthingsthings:///add?title[title]notes[summary]listReadingNotionnotionnotion://new?title[title]content[content_markdown]DEVONthinkx-devonthinkx-devonthink://createText?title[title]text[content_markdown]destinationInbox此外保留了通用 Web / 邮件协议http、https、mailto。可以推断白名单刻意不放行任何与本地文件、系统协议或脚本执行相关的 scheme即便某些协议在 Windows 上确实注册了系统处理器也会在这里被拦截。1.3 参数化测试危险的 scheme 一个都不能过与源码改动配套integration.test.ts 新增了两组it.each参数化用例合计十余个断言危险协议测试组blocks dangerous scheme ... and does not invoke shell.openExternal覆盖file:///etc/passwd、大小写变体FILE:///etc/passwd、smb://attacker.example/share、jar:http://attacker.example/x.jar!/、res://shell32.dll/1、ms-msdt:/id PCWDiagnostic、search-ms:querysecret、javascript:alert(1)、data:text/html,scriptalert(1)/script、vbscript:msgbox(1)断言这些调用全部 reject且shell.openExternal的 mock从未被调用放行协议测试组permits known integration scheme ...逐个验证 UI 内置的集成示例与通用协议仍然可用并断言shell.openExternal收到的就是传入的原字符串。这套测试的价值在于从正反两个方向锁死行为将来若有人为了「图方便」往白名单里加协议或反过来不小心改动解析逻辑漏掉某个内置集成回归测试都会第一时间报错。二、RSSHub 订阅配额错误从原始报错到可读、可行动的提示changelog 的第二个改进是当 RSSHub 订阅数达到上限时展示给用户的错误信息更好了。服务端在触发配额限制时会返回业务错误码2012其本地化文案在各语言目录下均有收录本次改动为每个文件新增 1 行// locales/errors/en.json第 22 行 2012: RSSHub feed subscription limit exceeded,其他语言对应为fr-FR的 Limite dabonnement aux flux RSSHub dépassée、ja的 RSSHubフィードの購読数制限を超えました、zh-CN/zh-TW的中文译文即错误消息不再以裸英文夹杂内部上下文的形式出现在界面上而是命中errors:2012的 i18n key 渲染成本地化文案。配套的客户端解析层改动位于移动端代码库该提交2350884ea同时被并入 v1.12.0 发布窗口apps/mobile/src/lib/error-message.ts新增sanitizeErrorMessage用正则/\r?\nRequest:[\s\S]*$/u切掉 Follow API 错误里附带的请求上下文Request: POST /subscriptions、请求参数、Headers 等apps/mobile/src/lib/error-parser.ts在getFetchErrorInfo、getFetchErrorMessage、toastFetchError等所有出口统一套用清洗逻辑新增两组单测 error-message.test.ts 与 error-parser.test.ts。测试用例还原了真实场景Follow API 抛出的错误原本会附带请求上下文其中甚至包含cookie: sessionsecret这样的敏感字段。旧逻辑会把这整段未经处理的内容展示给用户新逻辑只保留首行的业务消息。此外当服务端开启了支付能力isPaymentEnabled时命中 2012 错误码会直接弹出升级引导对话框showUpgradeRequiredDialog并展示免费档位说明文案而不是只弹一条没头没尾的 toast——这也让「订阅超限」从一句报错变成了一个可执行的升级动作。对普通读者而言可以这样理解本次改进以前订阅 RSSHub 路由达到上限时用户可能看到夹杂着内部请求参数的错误文本现在则是一条干净的多语言提示并在可升级环境下被引导到付费档位。三、修复分享弹层在执行完动作后不再「赖着不走」旧版本中用户在分享面板里完成「复制链接」或「通过社交平台分享」后弹层会残留打开需要用户再点一次别处或手动关闭。修复提交52f587dac从三层入手3.1 popover 状态管理新增dismissPopoverapps/desktop/layer/renderer/src/atoms/popover.ts 中showPopover在打开新 popover 前会先读取当前状态若已有弹层打开则先abort其AbortController避免状态互相覆盖新增导出dismissPopover()若当前弹层处于打开状态则 abort 并写回{ open: false }。3.2 SharePanel每次动作成功后主动关闭apps/desktop/layer/renderer/src/components/common/SharePanel.tsx 在所有成功路径末尾追加dismissPopover()「复制链接」成功含主流程失败后回退复制的场景后关闭复制分享 URL 成功copyToClipboard(shareUrl)后关闭通过window.open拉起社交平台分享窗口后关闭只有在彻底失败如复制失败时才保留弹层以显示share.copy_failed错误提示。3.3 PopoverProvider从命令式触发改为受控渲染apps/desktop/layer/renderer/src/providers/popover-provider.tsx 同步重构去掉了原先用useRef手动 dispatchMouseEvent(click)的 hack改为直接受控地传open{popoverState.open}、modal并在onOpenChange(false)时统一走dismissPopover()内容渲染也调整成AnimatePresence包裹的受控结构。这消除了「触发 → 模拟点击 → 关窗状态不同步」的根源。配合新增的 SharePanel.test.tsx 与 popover-provider.test.tsx共新增 300 余行测试交互回归被固化下来。四、修复Stripe 逾期订阅也能完成续费升级此前如果用户的 Stripe 订阅账单处于past-due逾期未付状态点击升级时会直接失败用户既不清楚原因也无处付款。修复提交3d65478fb同时更新了桌面端与移动端设置页apps/desktop/layer/renderer/src/modules/settings/tabs/plan.tsxapps/mobile/src/modules/settings/routes/Plan.tsx桌面端 plan.tsx 中定义了常量const ACTIVE_STRIPE_SUBSCRIPTION_EXISTS_ERROR_CODE ACTIVE_STRIPE_SUBSCRIPTION_EXISTS升级 mutation 的逻辑改为if (res.error?.code ACTIVE_STRIPE_SUBSCRIPTION_EXISTS_ERROR_CODE) { await openStripeBillingPortal() return } if (res.error) { throw new Error(res.error.message) } if (IN_ELECTRON res.data?.url) { window.open(res.data.url, _blank) }新抽出的openStripeBillingPortal()会向${env.VITE_API_URL}/billing/portal发起POST携带returnUrlElectron 内使用env.VITE_WEB_URLWeb 端使用window.location.href拿到 portal URL 后在 Electron 内window.open(..., _blank)Web 端则window.location.assign。useBillingPortal也被复用为同一个实现并统一补充了onError的 toast 提示。可以推断其产品意图当服务端识别到该账户仍持有活跃的 Stripe 订阅只是逾期时不再让用户走一遍新的 Checkout 流程而是直接把用户导向Stripe Billing Portal补缴欠款结清账单后订阅即可自动恢复避免重复扣费或订阅错乱。五、修复两个视觉回归5.1 订阅分类的折叠箭头不再旋转提交22c8a667e修复了侧边栏订阅分类FeedCategory收起/展开时箭头图标不再旋转 90° 的回归。问题出在 Tailwind 变体variant的书写顺序上// 修复前 [_.i-mgc-right-cute-fi]:data-[stateopen]:rotate-90 // 修复后apps/desktop/layer/renderer/src/modules/subscription-column/FeedCategory.tsx 第 326 行 flex h-8 items-center>创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考