ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Tolaria 安全渲染与用户正则防护实践:基于 ADR-0108 的消毒边界、正则校验与依赖钉扎全解析

Tolaria 安全渲染与用户正则防护实践:基于 ADR-0108 的消毒边界、正则校验与依赖钉扎全解析 Tolaria 安全渲染与用户正则防护实践基于 ADR-0108 的消毒边界、正则校验与依赖钉扎全解析【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolariaTolaria 是一款以 Markdown 知识库为数据源头的桌面应用Tauri v2 React其富文本编辑器基于 BlockNote支持 Mermaid 图表、KaTeX 数学公式、HTML 块等由可信库动态生成的渲染内容同时允许用户在视图过滤与编辑器查找替换中启用正则模式。本文以架构决策记录 docs/adr/0108-sanitized-rendered-markup-and-safe-regex.md 为主线结合仓库源码深入讲解 Tolaria 如何通过渲染标记消毒 DOM 节点挂载、用户正则编译前校验、SCA 传递依赖版本钉扎三道防线将 Codacy SRM 标记的 XSS / DoS 风险收敛为可审计、可维护的显式安全边界。读完本文你将掌握如何为 Mermaid SVG 与 KaTeX HTML 配置 DOMPurify 白名单并绕过dangerouslySetInnerHTML挂载节点、如何用safe-regex2配合长度上限拦截灾难性回溯正则、以及如何在 pnpm overrides 与 Rust lockfile 层面钉扎脆弱传递依赖。一、背景为什么渲染可信库的输出也需要消毒Tolaria 的富文本编辑体验依赖两类动态生成的标记Mermaid 图表用户在笔记中书写 fencedmermaid代码块编辑器将其渲染为 SVG参见 docs/adr/0088-markdown-durable-mermaid-diagrams.mdKaTeX 数学公式用户在笔记中书写$...$/$$...$$编辑器将其渲染为 HTML参见 docs/adr/0082-markdown-durable-math-notes.md。按常规认知由可信库生成的输出无需再次消毒。但 ADR-0108 明确指出Mermaid 的图表源文本与 KaTeX 的公式源文本均来自用户笔记一旦用户的笔记内容例如粘贴自网页的富文本、来自外部协作的 Markdown被带入渲染管线库的解析结果就可能被污染——例如 Mermaid 标签文本中嵌入的 HTML、KaTeX 输出中不期望的标签结构。与此同时Tolaria 允许用户在两类场景中显式开启正则匹配视图过滤条件自定义视图docs/adr/0040-custom-views-yml-filter-engine.md中的 YAML 过滤引擎支持正则模式docs/adr/0047-regex-mode-for-view-filter-conditions.md编辑器查找 / 替换Raw 编辑器的 Find 工具支持正则开关。当用户输入的正则存在灾难性回溯catastrophic backtracking时在 UI 线程直接执行会导致界面长时间卡死——这正是 Codacy SRMSoftware Risk Management代码健康门禁参见 docs/adr/0018-codescene-code-health-gates.md标记的两类Critical 级风险dangerouslySetInnerHTML式的裸标记插入XSS与直接构造正则DoS。决策核心不信任可信库而是把消毒与校验做成渲染/执行前的显式边界同时不牺牲用户已有的正则能力。二、决策总览两个运行时依赖三条防线ADR-0108 的决策可归纳为新增两个直接运行时依赖dompurifyHTML/SVG 消毒与safe-regex2正则安全检测渲染的 Mermaid SVG 与 KaTeX HTML必须消毒后再插入且通过DOM 节点挂载而不是 React 的dangerouslySetInnerHTML用户提供的正则源必须先做长度上限检查 safe-regex2检测通过后才允许编译执行SCA软件成分分析报告的脆弱传递依赖通过包管理器 overrides 钉扎到已修补版本Rust 侧则通过lockfile-only 更新保持 OpenSSL、rustls-webpki、tar 处于修补版本且不改变公开 Tauri API。在 package.json 中可以确认这两个依赖均为精确版本钉扎而非浮动版本依赖版本用途dompurify3.4.12精确渲染标记消毒Mermaid SVG / KaTeX HTML / HTML 块safe-regex25.1.1精确用户正则的灾难性回溯检测下方逐一展开三条防线的源码级实现。三、防线一渲染标记消毒与 DOM 节点挂载3.1 统一消毒组件SafeMarkup.tsxTolaria 将消毒边界收敛到一个文件src/components/SafeMarkup.tsx对外暴露两个组件SafeHtmlSpan接收 KaTeX 生成的 HTML 字符串SafeSvgDiv接收 Mermaid 生成的 SVG 字符串。两份 DOMPurify 配置清晰体现了按渲染格式差异化白名单的思路SafeMarkup.tsx 第 13-23 行const MERMAID_SVG_SANITIZE_CONFIG { USE_PROFILES: { svg: true, svgFilters: true, html: true }, ADD_TAGS: [foreignObject], ADD_ATTR: [xmlns], HTML_INTEGRATION_POINTS: { foreignobject: true }, } const KATEX_MARKUP_SANITIZE_CONFIG { // KaTeX uses inline SVG paths for stretched radicals such as \sqrt{x}. USE_PROFILES: { html: true, mathMl: true, svg: true }, }要点解读Mermaid 配置启用svg、svgFilters、html三个 profile并显式放行foreignObject标签与xmlns属性、声明foreignobject为 HTML 集成点——因为 Mermaid 的 HTML 标签如流程图节点中的富文本会生成foreignObject包裹的 HTML 内容需要让 DOMPurify 递归消毒这部分KaTeX 配置额外启用mathMlMathMLprofile以保留\sqrt{x}这类拉伸根号所用的内联 SVG 路径同时维持 HTML 消毒。3.2 通过 DOM 节点挂载规避dangerouslySetInnerHTML消毒完成后标记不是通过dangerouslySetInnerHTML注入而是解析为 DOM 节点后再挂载SafeMarkup.tsx 第 25-42 行function importSanitizedMarkupNodes(markup: string): Node[] { const sanitized DOMPurify.sanitize(markup, KATEX_MARKUP_SANITIZE_CONFIG) const parsed new DOMParser().parseFromString(sanitized, text/html) return Array.from(parsed.body.childNodes, (node) document.importNode(node, true)) } function importSanitizedSvgNode(svg: string): Node | null { const sanitized DOMPurify.sanitize(svg, MERMAID_SVG_SANITIZE_CONFIG) const parsed new DOMParser().parseFromString(sanitized, text/html) const parsedSvg parsed.body.querySelector(svg) if (!parsedSvg) return null const svgNode document.importNode(parsedSvg, true) svgNode.querySelectorAll(style).forEach((style) { style.setAttribute(nonce, RUNTIME_STYLE_NONCE) }) return svgNode }组件侧用useLayoutEffect与replaceChildren将节点替换进占位容器SafeMarkup.tsx 第 44-63 行保证每次markup变化都重新走一遍消毒 → 解析 → 导入流程。两个值得注意的细节document.importNode(node, true)跨文档深拷贝节点隔离来源文档避免共享引用style节点的nonce处理依赖 src/lib/runtimeStyleNonce.ts 注入的RUNTIME_STYLE_NONCE与 CSP 的script-src/style-src的 nonce 机制衔接保证内联样式能在严格 CSP 下正常生效对应 docs/adr/0141-scoped-linux-webkit-rendering-safeguards.md 中 WebKit 渲染护栏的整体思路。3.3 与渲染管线的接入点Mermaid 渲染src/components/MermaidDiagram.tsxMermaid 初始化即开启严格模式第 61-71 行——securityLevel: strict、htmlLabels: false、suppressErrorRendering: true从库侧先降低攻击面渲染在离屏宿主div中串行排队执行renderQueue第 37、135-156 行产物 SVG 先做节点标签居中等后处理最终交给SafeSvgDiv挂载第 160 行且整个视口区域通过stopPropagation阻断编辑事件避免与 BlockNote 光标交互第 180-181 行。KaTeX 数学公式src/components/editorSchema.tsxMathRender组件第 108-120 行将renderMathToHtml({ latex, displayMode })的输出交给SafeHtmlSpan渲染作为mathBlock的展示层。HTML 块同样是用户可控文本 → 库/解析器输出 → 消毒的链路src/utils/htmlBlockSandbox.ts 在构建沙箱srcdoc前用DOMPurify.sanitize(markup, SANITIZE_CONFIG)清洗第 170 行并叠加 CSPscript-src、style-src白名单与加载属性剥离参见 docs/adr/0154-sandboxed-fenced-html-blocks.md。3.4 回归测试src/components/SafeMarkup.test.tsx 用两条用例锁死消毒边界的行为正向用例\sqrt{x}经renderMathToHtml渲染后SafeHtmlSpan保留.katex svg path、viewBox与preserveAspectRatio属性——证明 KaTeX 需要的 SVG 结构不会被消毒误伤负向用例spansafe/spanscriptalert(x)/script经过SafeHtmlSpan后script被彻底移除、span文本保留——证明 XSS 载荷确实被拦截。这组保留所需、剔除威胁的双向断言正是 ADR-0108 所述显式消毒边界的可验证契约。四、防线二用户正则的编译前校验4.1 统一编译入口safeRegex.ts所有面向用户的正则 → RegExp编译都收敛到 src/utils/safeRegex.ts其核心是compileSafeUserRegex第 11-21 行const MAX_USER_REGEX_LENGTH 256 const REGEX_REPEAT_LIMIT 25 export type SafeRegexResult | { ok: true; pattern: RegExp } | { ok: false; reason: invalid | too_long | unsafe } export function compileSafeUserRegex(source: string, flags ): SafeRegexResult { if (source.length MAX_USER_REGEX_LENGTH) return { ok: false, reason: too_long } try { const pattern Reflect.construct(RegexConstructor, [source, flags]) as RegExp if (!safeRegex(source, { limit: REGEX_REPEAT_LIMIT })) return { ok: false, reason: unsafe } return { ok: true, pattern } } catch { return { ok: false, reason: invalid } } }三层拦截顺序是精心设计的长度上限超过MAX_USER_REGEX_LENGTH 256字符直接返回too_long在解析前就把超长表达式挡掉语法编译用Reflect.construct(RegexConstructor, ...)在try/catch中编译语法非法返回invalid回溯检测safe-regex2以limit: 25的重复上限分析表达式判定存在危险嵌套重复则返回unsafe正则根本不会被用于匹配——这正是 ADR 所述不安全或过大的表达式在校验阶段失败而不是在 UI 线程执行的落地方式。调用方拿到的SafeRegexResult是一个带reason的可判别联合方便 UI 向用户展示精确的错误原因。4.2 场景一自定义视图的正则过滤src/utils/viewFilters.ts 是视图过滤引擎的求值实现。当过滤条件的regex true且算子为contains/not_contains/equals/not_equals时usesRegex第 81-84 行通过compileRegex编译第 75-79 行function compileRegex(cond: FilterCondition, value: string): RegExp | null { if (cond.regex ! true) return null const compiled compileSafeUserRegex(value, i) return compiled.ok ? compiled.pattern : null }求值侧第 230-232 行对声明了正则但编译失败的情况做失败即拒绝if (usesRegex(cond) !regex) return false——即该条目不匹配绝不回退到把用户输入当字面量或原始正则执行。过滤引擎覆盖type、status、title、filename、body等内置字段以及关系字段与任意属性resolveField第 61-67 行正则模式对全部文本类字段生效。4.3 场景二编辑器查找 / 替换src/utils/editorFind.ts 是 Raw 编辑器 Find 工具的逻辑层。compileFindPattern第 33-46 行在非正则模式下先用escapeRegExp将查询串转义为字面量第 29-31 行再统一走compileSafeUserRegex正则模式下按g与大小写敏感标记拼接 flagsconst source options.regex ? query : escapeRegExp(query) const flags ${global ? g : }${options.caseSensitive ? : i} const compiled compileSafeUserRegex(source, flags) if (compiled.ok) return { error: null, pattern: compiled.pattern } return { error: compiled.reason invalid ? Invalid regex : Unsafe regex, pattern: null }这里把safeRegex的三态结果映射为面向用户的错误文案Invalid regex/Unsafe regex。findEditorMatches第 48-74 行还额外防御了零长度匹配当match[0].length 0时直接返回错误Regex must match text避免/(?a)/这类空匹配导致死循环。替换场景replacementForEditorFindMatch第 90-102 行同样复用同一编译入口保证查找与替换行为一致。4.4 场景三FilterBuilder 的输入即校验src/components/FilterBuilder.tsx 第 36 行在 UI 层做即时反馈return !compileSafeUserRegex(value, i).ok——用户输入的正则一旦不满足校验构建器立即标记错误做到边输入边拦截而不是等到视图求值时才暴露问题。4.5 小结正则防线形成了UI 即时校验FilterBuilder→ 统一编译入口safeRegex.ts→ 消费方失败即拒绝viewFilters / editorFind的完整闭环。用户的全部正则能力视图过滤、查找替换得以保留但任何不安全或超长的表达式都会在编译前被拒绝不会进入 UI 线程执行。五、防线三SCA 脆弱传递依赖的版本钉扎ADR-0108 的另一半工作是供应链安全SCA 报告出的脆弱传递依赖通过pnpm overrides钉扎到已修补版本直到上游依赖天然采用这些版本为止。5.1 pnpm overridesJS 侧钉扎package.json 第 139-172 行的pnpm.overrides覆盖了三条主线protobufjsprotobufjs: 7.6.5pnpm-lock.yaml 中已解析为protobufjs7.6.5是 MCP 通信栈与部分遥测链路的重要传递依赖MCP SDK web-server 栈hono、hono/node-server、path-to-regexp、express-rate-limit、body-parser、ip-address、undici、qs、form-data等均被钉到修补版本——这组依赖服务于 mcp-server见 mcp-server/package.json的 HTTP 服务面属于典型的暴露于网络输入组件Vite 解析 / 构建传递栈esbuild、postcss、rollup、picomatch、minimatch含3.1.2、3.1.3、9.0.5、9.0.6多个历史版本的统一上提、brace-expansion、markdown-it、linkify-it、flatted、js-yaml、fast-uri、fast-xml-builder等解析与序列化相关组件vitepressvite也被钉到6.4.3文档站构建栈。此外还有针对uuid的精确路径钉扎mermaiduuid、blocknote/coreuuid→11.1.1说明 overrides 支持包名依赖的嵌套路径语法可精确命中某一依赖链的指定包。配合patchedDependencies第 173-180 行对 BlockNote、TipTap、prosemirror-tables 的源码级补丁形成版本钉扎 补丁修补的组合策略。5.2 Rust lockfile-only 更新不改变 Tauri APIRust 侧src-tauri/Cargo.toml通过reqwest的rustls-tls特性使用 rustls 而非 OpenSSL第 44 行src-tauri/Cargo.lock 中确认关键安全相关 crate 处于修补版本cratelockfile 版本openssl0.10.81rustls-webpki0.103.13tar0.4.46ADR-0108 特别强调这些是lockfile-only 更新只升级锁文件中解析到的 crate 版本不触碰Cargo.toml中公开的 Tauri 依赖 API因此对应用层代码零侵入天然兼容跨平台发布制品流程docs/adr/0080-cross-platform-desktop-release-artifacts-and-portable-vault-names.md。六、效果评估与后续维护按 ADR-0108 的 Consequences 记录决策落地后的效果可归纳为三点显式消毒边界共享Mermaid 与数学公式渲染共用 src/components/SafeMarkup.tsx 这唯一消毒出口后续新增的渲染类型如 tldraw 白板快照预览、docs/adr/0107-markdown-durable-tldraw-whiteboards.md复用同一机制即可正则功能保留但受限用户正则能力没有被移除只是将不安全 / 过大的表达式在校验期拒绝UI 线程不再有机会执行灾难性回溯正则供应链风险收敛protobufjs、MCP web-server 栈、Vite 构建栈的脆弱传递依赖被钉到修补版本Rust 侧 OpenSSL / rustls-webpki / tar 保持在修补版本且不改公开 API。同时ADR 也明确了overrides 的退出条件一旦依赖图不再拉取脆弱版本overrides 应当移除——这是一条临时钉扎、到期清理的治理纪律防止 overrides 无限期累积成新的技术债。感兴趣的读者可以从 docs/adr/README.md 的索引继续追溯相关决策如 docs/adr/0108-direct-model-ai-targets.md 是同一编号下的另一条独立决策记录并在本地通过pnpm install pnpm dev运行后在 Raw 编辑器的 Find 工具与自定义视图过滤器中实测输入(a)$这类高回溯风险表达式观察其被Unsafe regex拒绝的即时反馈。【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表