Skip to content

fix(desktop): allow double dots in bundle filenames - #5515

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/bundle-dotdot-filenames
Jun 20, 2026
Merged

fix(desktop): allow double dots in bundle filenames#5515
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/bundle-dotdot-filenames

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • only reject bundle paths with a .. path segment
  • allow ordinary double dots inside file or directory names
  • add regression coverage for both traversal and valid .. filenames

Fixes #5514

Tests

  • bun test packages/desktop/packages/shared/src/utils/__tests__/bundle-files.test.ts
  • bun run typecheck:shared in packages/desktop
  • npx eslint src/utils/bundle-files.ts src/utils/__tests__/bundle-files.test.ts in packages/desktop/packages/shared
  • npx prettier --check packages/desktop/packages/shared/src/utils/bundle-files.ts packages/desktop/packages/shared/src/utils/__tests__/bundle-files.test.ts
  • git diff --check

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

📌 Same maintainer note — full version on #5509.

This is the ..-prefixed-name / path-boundary theme again (same class as the merged #5458 / #5460, now for desktop bundle filenames). Please fold the path-boundary + dot-prefix work into one PR and reuse the existing shared helper (isSubpath / isPathWithinRoot in packages/core/src/utils/paths.ts, or the desktop-shared equivalent) instead of another per-file check. The change looks correct — this is purely grouping + DRY.

中文说明

📌 同一条维护者说明 —— 完整版见 #5509

这又是 .. 前缀文件名 / 路径边界那个主题(和已合的 #5458 / #5460 同类,现在用在 desktop bundle 文件名上)。请把路径边界 + dot 前缀相关的改动合并成一个 PR,并复用已有共享 helper(packages/core/src/utils/paths.ts 里的 isSubpath / isPathWithinRoot,或 desktop 对应实现),而不是再写一份逐文件检查。改动看起来是对的 —— 这条纯粹是归并 + DRY。

@tt-a1i
tt-a1i marked this pull request as ready for review June 20, 2026 19:18
@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template: the body uses "## Summary" / "## Tests" / "## AI Assistance Disclosure" instead of the template headings ("What this PR does", "Why it's needed", "Reviewer Test Plan" with OS matrix, "Risk & Scope", "Linked Issues"). Not blocking on this — the substance is clear and it's a small fix — but future PRs should follow the template for faster review.

Direction: this is a clear-cut bug fix. The .. substring check is overly broad and rejects legitimate filenames like release..notes.md. Directly fixes #5514 which is open and labeled type/bug. No question about whether this should ship.

Approach: minimal and focused — one helper function, one line changed, two regression tests. No scope creep, no drive-by refactors. Exactly the right size for the problem.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板: PR 正文使用了 "## Summary" / "## Tests" / "## AI Assistance Disclosure" 而非模板要求的标题。不阻断——内容清楚且改动很小——但后续 PR 请参照模板以加速审查。

方向: 明确的 bug 修复。.. 子字符串检查过于宽泛,会拒绝 release..notes.md 等合法文件名。直接修复已开放的 #5514(标记为 type/bug)。方向无疑。

方案: 最小化且聚焦——一个辅助函数、一行改动、两个回归测试。无范围蔓延、无顺手重构。问题规模的恰当修复。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: I'd replace the includes('..') substring check with a segment-level check — split by /, check for .. as an exact segment. Add tests for docs/../escape.txt (still reject) and release..notes.md (accept). That's the minimum correct fix.

The PR matches this exactly. The hasParentTraversalSegment() helper is clean — split('/').includes('..') is the right check given that BundleFile.relativePath is always forward-slash separated (per the file's own doc comment). No correctness issues, no security gaps I can see. The existing ../etc/passwd test still passes, the new docs/../escape.txt test covers mid-path traversal, and release..notes.md confirms the original bug is fixed.

No concerns to raise. The implementation is straightforward and correct.

Testing

Unit tests (25/25 pass):

$ bun test src/utils/__tests__/bundle-files.test.ts

bun test v1.3.14 (0d9b296a)

src/utils/__tests__/bundle-files.test.ts:
✓ bundle-files > toPortableRelPath > normalizes forward slashes (no-op on POSIX) [0.51ms]
✓ bundle-files > toPortableRelPath > handles single file name [0.13ms]
✓ bundle-files > fromPortableRelPath > converts back to native (no-op on POSIX) [0.14ms]
✓ bundle-files > validateBundleFile > accepts a valid file [0.45ms]
✓ bundle-files > validateBundleFile > rejects path traversal (..) [0.15ms]
✓ bundle-files > validateBundleFile > rejects parent traversal segments below the bundle root [0.13ms]
✓ bundle-files > validateBundleFile > accepts double dots inside a path segment [0.14ms]
✓ bundle-files > validateBundleFile > rejects absolute paths [0.12ms]
✓ bundle-files > validateBundleFile > rejects backslash paths [0.13ms]
✓ bundle-files > validateBundleFile > rejects double slashes [0.17ms]
✓ bundle-files > validateBundleFile > rejects size mismatch [0.13ms]
✓ bundle-files > validateBundleFile > rejects empty relativePath [0.12ms]
✓ bundle-files > collectDirectoryFiles > collects files recursively [1.01ms]
✓ bundle-files > collectDirectoryFiles > skips hidden files [0.37ms]
✓ bundle-files > collectDirectoryFiles > respects skipFiles option [0.31ms]
✓ bundle-files > collectDirectoryFiles > respects skipDirs option [0.40ms]
✓ bundle-files > collectDirectoryFiles > produces deterministic (sorted) output [0.35ms]
✓ bundle-files > collectDirectoryFiles > uses portable forward-slash paths [0.40ms]
✓ bundle-files > collectDirectoryFiles > correctly encodes file content as base64 [0.25ms]
✓ bundle-files > collectDirectoryFiles > returns empty array for non-existent directory [0.13ms]
✓ bundle-files > restoreFiles > restores files to target directory [0.40ms]
✓ bundle-files > restoreFiles > creates subdirectories as needed [0.39ms]
✓ bundle-files > restoreFiles > throws on path traversal [0.29ms]
✓ bundle-files > restoreFiles > throws on absolute path [0.28ms]
✓ bundle-files > restoreFiles > round-trips with collectDirectoryFiles [0.66ms]

 25 pass
 0 fail
 32 expect() calls
Ran 25 tests across 1 file. [31.00ms]

Both new tests pass:

  • rejects parent traversal segments below the bundle root — confirms docs/../escape.txt is still blocked
  • accepts double dots inside a path segment — confirms release..notes.md is now allowed

Tmux real-scenario testing: not applicable. validateBundleFile() is an internal utility with no direct CLI surface — there's no user-facing command to demonstrate the fix end-to-end. The unit tests fully exercise the behavior change.

Typecheck/lint: pre-existing baseUrl deprecation in packages/webui causes npm run typecheck to fail on main as well — unrelated to this PR. The changed code is type-trivial (boolean-returning function replacing a boolean expression).

中文说明

代码审查

独立方案:includes('..') 子字符串检查替换为段级检查——按 / 分割,检查是否有完全匹配的 .. 段。添加 docs/../escape.txt(仍拒绝)和 release..notes.md(接受)的测试。这是最小正确修复。

PR 完全匹配此方案。 hasParentTraversalSegment() 辅助函数简洁——鉴于 BundleFile.relativePath 始终使用正斜杠分隔,split('/').includes('..') 是正确的检查。无正确性问题,无安全漏洞。

测试

单元测试(25/25 通过):两个新增测试均通过——docs/../escape.txt 仍被拒绝,release..notes.md 现已接受。

Tmux 真实场景测试: 不适用。validateBundleFile() 是内部工具函数,无直接 CLI 入口。单元测试已完整覆盖行为变更。

类型检查/lint: packages/webui 中预先存在的 baseUrl 弃用警告导致 npm run typecheckmain 分支也失败——与本 PR 无关。改动代码类型平凡。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This is the kind of PR that's easy to approve. Clear bug, clear fix, clean code, good tests.

The original includes('..') check was a textbook false-positive bug — it caught the security case but also rejected any filename containing two consecutive dots. The fix is the minimum correct change: a segment-level check that preserves the security guarantee while allowing legitimate filenames. Four lines of production code, two focused regression tests. Nothing more, nothing less.

The only note for future PRs is the template — the "Reviewer Test Plan" section with OS coverage helps maintainers prioritize review. But for a fix this small and well-tested, it didn't slow things down.

LGTM. Approving. ✅

中文说明

这是一个容易批准的 PR。清晰的 bug、清晰的修复、干净的代码、良好的测试。

原来的 includes('..') 检查是典型的误报 bug——捕获了安全场景但也拒绝了任何包含两个连续点的文件名。修复是最小正确变更:段级检查在保留安全保障的同时允许合法文件名。四行生产代码,两个聚焦的回归测试。不多不少。

唯一建议是后续 PR 请使用模板——"Reviewer Test Plan" 部分及操作系统覆盖有助于维护者优先审查。但对于如此小且测试良好的修复,这并不影响审查效率。

LGTM. 批准。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

🔬 Local real-build verification on macOS (head 5b3dacb46)

Ran the change end-to-end against the real source of bundle-files.ts — the desktop shared package uses bun:test and isn't in the npm workspaces, so I imported the actual .ts via tsx and also ran the full shipped suite through vitest (compatible API). Driven inside tmux, tee'd to a log. (Complements the maintainer DRY note above / on #5509 — this is the evidence side.)

Verdict: ✅ the fix is correct and well-tested. It replaces a substring check (relativePath.includes('..'), which false-rejects legitimate names like release..notes.md) with a segment-wise check (split('/').includes('..')), which is the right way to detect parent-traversal. All real traversal vectors stay blocked. One optional, Windows-only hardening note at the end (non-blocking).

1 · Full shipped suite

bun:test (the real runner) isn't installed locally, so I ran the exact test file against the real source via vitest (same describe/it/expect): 25 / 25 pass, including the 2 added cases.

2 · Behavioral A/B — NEW (fix) vs OLD (base includes('..')), real source via revert-and-rerun

Input OLD includes('..') NEW segment-wise
release..notes.md ❌ "Path traversal" (false reject) ✅ allowed the bug, fixed
docs/release..notes.md ❌ "Path traversal" ✅ allowed fixed
a..b/c.txt, file...txt, foo.., ..foo ❌ "Path traversal" ✅ allowed fixed
../etc/passwd · docs/../escape.txt · .. · a/../b · sub/.. · ../.. rejected rejected traversal still blocked ✅
docs\..\escape.txt (backslash) rejected rejected (by the backslash check, not the .. check) no regression ✅
test.txt, subdir/file.txt allowed allowed control ✅

3 · The new test is non-vacuous (mutation)

Reverting bundle-files.ts to base (7e66b1710) and re-running:

  • accepts double dots inside a path segmentFAILS on base (old code returns Path traversal detected), passes on the PR → it genuinely guards the fix.
  • rejects parent traversal segments below the bundle rootpasses on both → confirms the fix didn't open a hole to add the feature.

4 · End-to-end restoreFiles (the real consumer: resource & session bundle restore)

  • legit docs/release..notes.md → written to <target>/docs/release..notes.md, content verified ✅
  • docs/../escape.txt → throws Invalid bundle file: Path traversal detected
  • Defense-in-depth intact: restoreFiles still re-checks fullPath.startsWith(targetDir + sep) after join (line 192), independent of validateBundleFile.

5 · Security note — one Windows-only edge case (non-blocking)

The segment-equality check newly allows a segment that is .. plus a trailing space or dot (.. , and ...), which the old substring check happened to reject. On POSIX this is safe and I verified containment.. /escape.txt restores to a literal .. subdirectory inside the target (escaped-target = false). On Windows, the filesystem strips trailing spaces/periods from path components, so .. can canonicalize back to ..; and the in-code startsWith guard does not catch it because path.win32.join(target, '.. /escape.txt') keeps the literal .. segment (= target\.. \escape.txt, which does start with target\). I could not reproduce the final FS step on macOS, so I'm flagging it as documented-but-unverified rather than a confirmed escape.

This is exactly the argument for the maintainer's DRY suggestion: routing restore through a single realpath-based boundary helper (e.g. isSubpath in packages/core/src/utils/paths.ts, or a realpathSync + containment check in restoreFiles) would close this without a hand-rolled per-file rule. Minimal alternative: trim trailing dots/spaces per segment before the .. comparison.

Verdict

The fix is correct, minimal, and the new test is meaningful — safe to merge (or fold into #5509 per the DRY note). The Windows .. -segment hardening is an optional, separate follow-up.

🇨🇳 中文版(完整对应)

🔬 macOS 本地真实构建验证(head 5b3dacb46

针对 bundle-files.ts真实源码做了端到端验证 —— desktop shared 包用 bun:test、且不在 npm workspaces 里,所以我用 tsx 直接 import 真实 .ts,并把完整的现有测试文件通过 vitest(API 兼容)跑了一遍。整个过程在 tmux 里运行并 tee 到日志。(与上方/ #5509 的维护者 DRY 说明互补 —— 这条是证据侧。)

结论:✅ 这个修复是正确的、且有测试覆盖。 它把子串检查(relativePath.includes('..'),会误拒 release..notes.md 这类合法文件名)换成了按路径段检查(split('/').includes('..')),这才是检测父级穿越的正确方式。所有真正的穿越向量依旧被拦。末尾有一条可选的、仅限 Windows 的加固说明(不阻塞合并)。

1 · 完整现有测试

真正的 runner bun:test 本地没装,所以我把原封不动的测试文件通过 vitest(同样的 describe/it/expect)对真实源码跑了一遍:25 / 25 通过,含新增的 2 个用例。

2 · 行为 A/B —— NEW(修复) vs OLD(base 的 includes('..')),用回退源码重跑真实代码

输入 OLD includes('..') NEW 按段检查
release..notes.md ❌ "Path traversal"(误拒) ✅ 放行 这就是 bug,已修
docs/release..notes.md ❌ "Path traversal" ✅ 放行 已修
a..b/c.txtfile...txtfoo....foo ❌ "Path traversal" ✅ 放行 已修
../etc/passwd · docs/../escape.txt · .. · a/../b · sub/.. · ../.. 拒绝 拒绝 穿越依旧被拦 ✅
docs\..\escape.txt(反斜杠) 拒绝 拒绝(走的是反斜杠检查,不是 .. 检查) 无回归 ✅
test.txtsubdir/file.txt 放行 放行 对照 ✅

3 · 新增测试非空过场(变异测试)

bundle-files.ts 回退到 base(7e66b1710)重跑:

  • accepts double dots inside a path segment → 在 base 上失败(旧代码返回 Path traversal detected),在 PR 上通过 → 它确实守护了这个修复。
  • rejects parent traversal segments below the bundle root两边都通过 → 证明为加这个特性并没有开出新的口子。

4 · 端到端 restoreFiles(真实调用方:resource / session bundle 还原)

  • 合法 docs/release..notes.md → 正确写到 <target>/docs/release..notes.md,内容校验通过 ✅
  • docs/../escape.txt → 抛出 Invalid bundle file: Path traversal detected
  • 纵深防御仍在:restoreFilesjoin 之后依旧独立地复查 fullPath.startsWith(targetDir + sep)(第 192 行),不依赖 validateBundleFile

5 · 安全说明 —— 一个仅限 Windows 的边界情况(不阻塞)

按段相等检查现在会放行.. 后面再跟一个空格或点」的段(.. ,以及 ...)—— 这些旧的子串检查恰好会拒。在 POSIX 上是安全的,我验证了它被包含在目标内 —— .. /escape.txt 会还原到目标目录内部一个字面叫 .. 的子目录(escaped-target = false)。在 Windows 上,文件系统会去掉路径段尾部的空格/点,于是 .. 可能被规范化回 ..;而代码里的 startsWith 防护也拦不住它,因为 path.win32.join(target, '.. /escape.txt') 会保留字面的 .. 段(= target\.. \escape.txt,确实以 target\ 开头)。最后这一步 FS 规范化我在 macOS 上无法复现,所以把它标为「有据可查但未实测」,而不是已确认的逃逸。

这恰好印证了维护者的 DRY 建议:让还原走一个统一的、基于 realpath 的边界 helper(比如 packages/core/src/utils/paths.ts 里的 isSubpath,或在 restoreFiles 里加一道 realpathSync + 包含性检查),就能不靠逐文件手写规则把这个口子也堵上。最小改法:在做 .. 比较前,先去掉每段尾部的点和空格。

结论

修复正确、改动精简、新增测试有意义 —— 可以合并(或按 DRY 说明并入 #5509)。Windows 的 .. 段加固是一个可选的、独立的后续项。

@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

✅ Local runtime verification — PR #5515 (fix(desktop): allow double dots in bundle filenames)

Verdict: PASS. The fix resolves #5514: validateBundleFile no longer rejects ordinary filenames that merely contain .. (e.g. release..notes.md, assets/v1..2/icon.svg), while real traversal (../escape.txt, docs/../escape.txt) is still rejected. Verified by driving the real validateBundleFile() over a path matrix and the real restoreFiles() filesystem path (actual writes to a temp dir), as an A/B of the PR vs origin/main, inside tmux with bun.

How it was tested

  • A verify test runs (1) validateBundleFile() across 15 relativePath variants (traversal / legit double-dots / control guards), and (2) restoreFiles() against a real temp target dir, observing whether each file is actually written to disk or throws.
  • A/B = same test, run once on the PR's bundle-files.ts (segment check split('/').includes('..')) and once with that file surgically reverted to origin/main (substring check includes('..')). Only the fix differs.

A/B differential — validateBundleFile() verdict

relativePath kind main (before) PR (after)
../escape.txt traversal 🛡️ REJECT 🛡️ REJECT
docs/../escape.txt traversal 🛡️ REJECT 🛡️ REJECT
.. traversal 🛡️ REJECT 🛡️ REJECT
a/../../b.txt traversal 🛡️ REJECT 🛡️ REJECT
docs/.. traversal 🛡️ REJECT 🛡️ REJECT
release..notes.md legit .. ❌ REJECT 🐛 ✅ OK
docs/release..notes.md legit .. ❌ REJECT 🐛 ✅ OK
assets/v1..2/icon.svg legit .. ❌ REJECT 🐛 ✅ OK
..hidden.txt legit .. ❌ REJECT 🐛 ✅ OK
foo...bar.txt legit .. ❌ REJECT 🐛 ✅ OK
a..b/c..d.txt legit .. ❌ REJECT 🐛 ✅ OK
docs/normal.txt control OK OK
/etc/passwd control (absolute) REJECT (Absolute path) REJECT (same)
docs\escape.txt control (backslash) REJECT (Backslash) REJECT (same)
docs//x.txt control (double slash) REJECT (double slash) REJECT (same)
  • 6 legitimate double-dot filenames flip REJECT → OK.
  • All 5 real traversal cases stay REJECT on both arms (security preserved).
  • The independent guards — absolute path, backslash separator, double slash — are unchanged and still reject with their own distinct messages. Narrowing the .. check opens no new traversal surface (a backslash variant like docs\..\escape is still caught by the backslash guard).

A/B differential — real restoreFiles() to a temp directory

This is the user-facing effect: does a bundle containing such a file actually restore?

relativePath main (before) PR (after)
docs/release..notes.md THREW Path traversal detected 🐛 WROTE to disk (content verified)
v1..2/icon.txt THREW Path traversal detected 🐛 WROTE to disk (content verified)
docs/../escape.txt 🛡️ THREW Path traversal detected 🛡️ THREW Path traversal detected

On main, a bundle holding a legitimate release..notes.md fails to restore entirely (the whole restoreFiles call throws). On the PR it restores correctly, and real traversal still throws on both.

Committed tests + teeth (counterfactual)

check result
committed bundle-files.test.ts (bun test) on PR 25 pass / 0 fail (incl. the 2 new tests)
counterfactual — new test accepts double dots inside a path segment vs main bundle-files.ts FAILSReceived: "Path traversal detected: docs/release..notes.md"

The counterfactual failing proves the new test genuinely pins the fixed behavior.

Why the fix is sound

The traversal check changes from "rejects any .. substring" to split('/').includes('..') — rejecting only path segments that are exactly ... A .. inside a segment (release..notes.md) is no longer a false positive, while ../, docs/.., and a/../../b still contain a literal .. segment and are rejected. Defense-in-depth is intact: the separate absolute/backslash/double-slash guards run unchanged, and restoreFiles keeps its second fullPath.startsWith(targetDir + sep) containment check.

🇨🇳 中文版(点击展开)

✅ 本地运行时验证 — PR #5515fix(desktop): allow double dots in bundle filenames

结论:通过。 该修复解决了 #5514validateBundleFile 不再因为文件名里仅仅包含 ..(如 release..notes.mdassets/v1..2/icon.svg)就拒绝,而真正的路径穿越(../escape.txtdocs/../escape.txt)仍然会被拒绝。验证方式:驱动真实的 validateBundleFile() 跑一个路径矩阵,以及真实的 restoreFiles() 文件系统路径(真实写入临时目录),在 tmux 中用 bun 对 PR 与 origin/main 做 A/B 对比。

测试方法

  • 验证测试运行:(1) validateBundleFile() 覆盖 15 个 relativePath 变体(穿越 / 合法双点 / 对照守卫);(2) restoreFiles() 针对真实临时目标目录,观察每个文件是真的写入磁盘还是抛错。
  • A/B = 同一个测试,分别在 PR 的 bundle-files.ts(分段检查 split('/').includes('..'))上跑一次,再把该文件精确还原回 origin/main(子串检查 includes('..'))跑一次。两组之间只有这一处修复的差别。

A/B 差异对比 — validateBundleFile() 判定

relativePath 类型 main(修复前) PR(修复后)
../escape.txt 穿越 🛡️ 拒绝 🛡️ 拒绝
docs/../escape.txt 穿越 🛡️ 拒绝 🛡️ 拒绝
.. 穿越 🛡️ 拒绝 🛡️ 拒绝
a/../../b.txt 穿越 🛡️ 拒绝 🛡️ 拒绝
docs/.. 穿越 🛡️ 拒绝 🛡️ 拒绝
release..notes.md 合法双点 ❌ 拒绝 🐛 ✅ 通过
docs/release..notes.md 合法双点 ❌ 拒绝 🐛 ✅ 通过
assets/v1..2/icon.svg 合法双点 ❌ 拒绝 🐛 ✅ 通过
..hidden.txt 合法双点 ❌ 拒绝 🐛 ✅ 通过
foo...bar.txt 合法双点 ❌ 拒绝 🐛 ✅ 通过
a..b/c..d.txt 合法双点 ❌ 拒绝 🐛 ✅ 通过
docs/normal.txt 对照 通过 通过
/etc/passwd 对照(绝对路径) 拒绝 (Absolute path) 拒绝 (相同)
docs\escape.txt 对照(反斜杠) 拒绝 (Backslash) 拒绝 (相同)
docs//x.txt 对照(双斜杠) 拒绝 (double slash) 拒绝 (相同)
  • 6 个合法的双点文件名从 拒绝 → 通过
  • 全部 5 个真实穿越用例在两个版本上都保持 拒绝(安全性保留)。
  • 独立的守卫——绝对路径、反斜杠分隔符、双斜杠——都没变,仍以各自独立的消息拒绝。收窄 .. 检查没有打开新的穿越面(像 docs\..\escape 这种反斜杠变体仍会被反斜杠守卫拦下)。

A/B 差异对比 — 真实 restoreFiles() 写入临时目录

这是用户可见的效果:包含这种文件的 bundle 能不能真的恢复?

relativePath main(修复前) PR(修复后)
docs/release..notes.md 抛错 Path traversal detected 🐛 写入磁盘(内容已校验)
v1..2/icon.txt 抛错 Path traversal detected 🐛 写入磁盘(内容已校验)
docs/../escape.txt 🛡️ 抛错 Path traversal detected 🛡️ 抛错 Path traversal detected

main 上,含有合法 release..notes.md 的 bundle 会整体恢复失败(整个 restoreFiles 调用抛错)。在 PR 上它能正确恢复,而真实穿越在两个版本上仍然抛错。

已提交测试 + 有效性(反事实)

检查项 结果
已提交 bundle-files.test.tsbun test)在 PR 上 25 通过 / 0 失败(含 2 个新增测试)
反事实 —— 新测试 accepts double dots inside a path segmentmainbundle-files.ts 失败 —— Received: "Path traversal detected: docs/release..notes.md"

反事实失败证明新增测试真正锁定了修复后的行为。

修复为什么是正确的

穿越检查从「拒绝任何 .. 子串」改为 split('/').includes('..')——只拒绝恰好等于 .. 的路径分段。段内的 ..release..notes.md)不再误报,而 ../docs/..a/../../b 仍含有字面量 .. 分段、会被拒绝。纵深防御保持完整:独立的绝对路径 / 反斜杠 / 双斜杠守卫照常运行,restoreFiles 也保留了第二道 fullPath.startsWith(targetDir + sep) 包含性检查。

Reproducible: real validateBundleFile() + real restoreFiles() (actual temp-dir writes) driven by a bun verify test (15 path variants + 3 restore cases), A/B origin/main vs PR 5b3dacb, plus the committed suite and a reverted-prod counterfactual — all in tmux. Working tree restored clean.

@wenshao
wenshao merged commit ca7638e into QwenLM:main Jun 20, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bundle file validation rejects filenames containing double dots

3 participants