资讯动态

Slidev 导出路径安全加固:将不可信的 exportFilename 收敛为 basename 的完整方案解析

发布时间:2026/9/9 23:56:31 来源:尧图企业网站定制
Slidev 导出路径安全加固将不可信的 exportFilename 收敛为 basename 的完整方案解析【免费下载链接】slidevPresentation Slides for Developers项目地址: https://gitcode.com/GitHub_Trending/sl/slidev导读在 Slidev 中演讲者既可以通过命令行参数slidev export --output path指定导出文件位置也可以在演示文稿的 frontmatter 中配置exportFilename来决定生成文件的名字。本篇基于仓库 plans/016-confine-export-output-path.md 中编号 016 的安全加固计划深入剖析来自 deck 配置的exportFilename未经约束即可影响磁盘写入路径这一路径穿越风险讲解其成因、威胁模型、path.basename收敛方案、单元测试设计与验证流程。读完后你将理解 Slidev 导出管线中操作者输入可信、deck 配置不可信的信任边界以及如何在不破坏既有--output行为的前提下安全落地此类修复。一、问题背景为什么exportFilename会成为安全隐患Slidev 导出产物的文件名并不只有一种来源它遵循一条优先级链。在 packages/slidev/node/commands/export.ts 的getExportOptions中可以看到outFilename output || outFilename || options.data.config.exportFilename || ${path.basename(entry, .md)}-export四个候选来源依次为output来自 CLI--output参数由**操作者operator**提供视为可信输入outFilename函数入参由上层调用方显式传入options.data.config.exportFilename来自deck 配置frontmatter / headmatter随演示文稿一同分发兜底默认值[entry 去掉 .md]-export。安全隐患恰恰出在第 3 个来源。幻灯片文件本质上是可以被分发、共享甚至自动下载的内容其中的 frontmatter 由幻灯片作者编写。当操作者运行slidev export或slidev build --download时deck 作者可能并非操作者本人例如在 CI 中批量构建他人提交的仓库。此时exportFilename就是一份不可信数据。而 Slidev 在拿到这个名字后只做了追加扩展名处理例如 packages/slidev/node/commands/export.ts 中的.pdf后缀补全并未校验其是否包含目录成分。getExportOptions返回的output随后会被各gen*函数真正写入磁盘export.ts中 PDF/PNG/MD/PPTX 四条产物路径的fs.writeFile因此一旦exportFilename含有目录穿越片段生成的产物就可能落在操作者预期目录之外。具体的攻击形态以slidev export输出到 PDF为例前端拼接逻辑大致等价于最终写入路径 ≈ output含 exportFilename 的值 .pdf若攻击者在一份看似人畜无害的 slides.md frontmatter 中写入--- exportFilename: ../../../../home/victim/.ssh/authorized_keys ---path.basename未生效时导出产物会携带目录成分配合后续join/resolve语义即可写出预期输出目录在slidev build --download场景下见 packages/slidev/node/commands/build.ts文件名同样直接取自 deck 配置const filename options.data.config.exportFilename || slidev-exported await exportSlides({ port, base: config.base, ...getExportOptions(args, options, join(outDir, ${filename}.pdf)) })这里的join(outDir, ...)意味着只要exportFilename里的..足够多最终路径就可能逃逸outDir。这也正是计划将本问题定性为security 类别P2风险 LOW工作量 S的原因可利用性取决于部署方式本地开发者手动运行对自己代码风险有限但在 CI、多租户构建或渲染远程内容时则是真实的文件写入越权面。二、信任边界CLI 参数与 deck 配置必须区分对待016 号计划反复强调的核心设计原则是保持信任来源的显式区分数据来源提供者信任级别约束CLI--output运行命令的操作者可信trusted保持原样允许指向操作者选择的任意目录frontmatterexportFilenamedeck / 幻灯片作者不可信untrusted必须收敛constrain为 basename对应地计划划定了明确的范围范围内In scope加固 packages/slidev/node/commands/export.ts净化 deck 配置文件名与 packages/slidev/node/commands/build.ts净化--download文件名并为净化函数补充单元测试范围外Out of scopeCLI--output路径操作者输入、可信应保留其指向任意目录的能力浏览器清理与临时服务器端口等事项由其他计划如 007/013 负责。之所以只净化 deck 来源而非一刀切地约束所有路径是因为两个来源的威胁模型截然相反——约束操作者自己指定的目录反而会破坏合法工作流例如把 PDF 直接导出到上级目录或临时目录。三、现状盘点导出文件名在各处的实际流转在做修复之前先用仓库源码把exportFilename的完整消费面梳理清楚。除上述export.ts:609与build.ts:150两处写入路径外还能在仓库中找到它出现在以下位置packages/slidev/node/cli.tsslidev export-notes命令在未指定--output时同样回退到options.data.config.exportFilename并拼接-notes后缀docs/guide/exporting.md 与 docs/custom/index.md文档将exportFilename描述为 headmatter 中控制导出文件名的配置packages/types/src/frontmatter.ts类型声明为可选的字符串exportFilename?: string | nullpackages/parser/src/config.ts默认值初始化为空字符串packages/client/pages/export.vue 与 packages/client/utils.ts浏览器端下载 PDF按钮也使用该配置拼出导出链接的文件名该路径最终回到服务端导出。可见exportFilename同时影响服务端磁盘写入export / build与前端下载文件名。016 计划的改动聚焦在会真正产生fs.writeFile的服务端两处且计划特别提示审查者Reviewer需要复核是否存在其他未经净化的 deck 配置值进入写入路径。四、修复方案用path.basename把不可信文件名收敛为 basename修复思路非常直接既然 deck 提供的只是文件名就应当强制它只能是文件名——任何目录成分../../x、绝对路径/etc/foo、形如a/b的子目录都必须被剥离。Step 1新增可导出的净化辅助函数建议在export.ts或node/utils.ts中新增一个小型导出函数import path from node:path // Deck 控制的文件名不允许携带目录成分。 export function sanitizeExportBasename(name: string): string { return path.basename(name) }path.basename会把任意目录前缀裁掉../../x → x/etc/foo → foo。这正是对deck 提供的名字只能是一个 basename这一约束的直接实现。使用pathe还是 Node 内置node:path均可二者在 Windows/Linux 的差异上需要以仓库实际 import 为准export.ts 目前经由pathe引入path落地时可保持一致。Step 2仅净化getExportOptions中的 deck 回退分支关键点在于净化动作只落在deck 配置这一来源上outputCLI 参数保持原样const deckName options.data.config.exportFilename ? sanitizeExportBasename(options.data.config.exportFilename) : undefined outFilename output || outFilename || deckName || ${path.basename(entry, .md)}-export相比原实现的差别是原代码直接读取options.data.config.exportFilename新实现先经sanitizeExportBasename兜一圈。当该值缺失时结果为undefined行为与之前完全一致因此正常的无配置与合法 basename路径零影响。Step 3同步加固build.ts的--download文件名对于slidev build自动附带 PDF 下载的场景采用同样的收敛逻辑const filename options.data.config.exportFilename ? sanitizeExportBasename(options.data.config.exportFilename) : slidev-exported此后再执行join(outDir, \${filename}.pdf)产物就被严格限制在outDir内部——这正是计划描述中join(outDir, ...)随后把它约束在outDir 内的含义。Step 4单元测试覆盖三种典型输入新增packages/slidev/node/commands/export.test.ts或扩展现有测试文件覆盖确定性输入无需文件系统import { describe, expect, it } from vitest import { sanitizeExportBasename } from ./export describe(sanitizeExportBasename, () { it(keeps a plain name, () expect(sanitizeExportBasename(talk)).toBe(talk)) it(strips directory traversal, () expect(sanitizeExportBasename(../../talk)).toBe(talk)) it(strips absolute dirs, () expect(sanitizeExportBasename(/etc/talk)).toBe(talk)) })测试语义与威胁模型一一对应普通名字原样保留守卫了快乐路径不回归目录穿越被剥离与绝对路径被剥离则分别对应相对穿越与绝对路径两种逃逸手法。五、为何收敛为 basename是正确约束而非resolve 后断言计划在 STOP conditions 中留了一个重要的设计旁路如果发现某个被文档化/被测试依赖的特性要求exportFilename允许携带子目录那么不应机械地 basename-strip而应改为 resolve 之后断言结果仍在 outDir 内resolve-and-assert-within-outDir的方式修复并且在改变方案前先上报。这是两种修复哲学的取舍basename 收敛简单、幂等、任何输入都能安全归一代价是永远无法通过配置写出带子目录的产物resolve 断言保留表达力允许exportFilename: exports/foo之类写法但需要在写入前resolve()后校验前缀逻辑更复杂、更容易出现边界漏洞如outDir自身的符号链接、大小写、..归一不完全。016 计划选择 basename 方案的前提正是它先验证了仓库内没有文档或测试依赖exportFilename含子目录这一能力——这也是计划把该场景列为 STOP 条件、要求先搜索 docs/tests 再动手的原因。六、验证矩阵怎么证明修复没有破坏功能计划给出的验证由命令与行为断言两部分组成命令验证目的命令预期结果安装依赖pnpm install退出码 0构建pnpm build退出码 0运行测试含新增用例pnpm test -- export全部通过类型检查pnpm typecheck退出码 0行为断言快乐路径零回归正常的exportFilename: my-talk仍应在预期位置产出my-talk.pdf产物文件名与目录均不变CLI 路径不受影响slidev export --output ./some/dir/name依旧可用操作者仍可把产物写到任何自己指定的目录恶意路径被收敛exportFilename: ../../talk、/etc/talk等输入经净化后只会得到talk最终写入被锁定在预期目录内。完成标准Done criteriadeck 配置的exportFilename在export.ts与build.ts两处使用前均被归约为 basenameCLI--output行为不变仍可指向任意目录净化函数具备单元测试覆盖pnpm build pnpm typecheck退出码为 0仅修改范围内文件通过git status复核更新 plans/README.md 中的状态行。七、工程落地规范与维护约定计划中还明确了落地时的工程约束可作为同类安全修复的通用范式分支与提交规范建议分支名fix/confine-export-filenameConventional Commit 提交信息形如fix(security): treat deck exportFilename as a basename未获指示不推送 PR维护性注释代码注释中应持续显式保留信任区分——CLI 参数 操作者可信deck 配置 不可信避免后续维护者在追加新写入路径时忘记套用净化审查清单审查者需要确认没有其他 deck 配置值未经净化就进入写入路径。从当前仓库看cli.ts 的export-notes分支同样消费了exportFilename拼接-notes虽然不在 016 范围内却正是审查者值得关注的疑似同类面。八、延伸阅读导出管线的核心实现packages/slidev/node/commands/export.tsgetExportOptions在 L580文件名优先级链在 L609各gen*写盘逻辑覆盖 L380-L545build --download的写入路径packages/slidev/node/commands/build.tsexportFilename的文档用法docs/guide/exporting.md、docs/custom/index.md、CLI--output说明见 docs/builtin/cli.md类型与默认值定义packages/types/src/frontmatter.ts、packages/parser/src/config.ts浏览器端导出下载对同一配置的使用packages/client/pages/export.vue 与 packages/client/utils.ts理解这则计划的本质不止于记住导出前要path.basename一下更重要的是领会其背后的安全方法论先识别数据来源的信任级别只对不可信来源施加最小必要约束用可复现的单元测试固化对抗样本并为更宽容的替代方案预留显式的 STOP 与上报通道。【免费下载链接】slidevPresentation Slides for Developers项目地址: https://gitcode.com/GitHub_Trending/sl/slidev创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价