fix(security): harden remote pairing PIN persistence#821
Conversation
PR Reviewer Guide 🔍(Review updated until commit d5b3d09)Here are some key observations to aid the review process:
|
appergb
left a comment
There was a problem hiding this comment.
Senior security review — exact head dec4d6d186548c573f050b6f33f48a0819eaead0
Strengths
- 改动聚焦在
remote_server/mod.rs,没有前端/UI 改动,也没有记录 PIN;日志只包含路径与错误。 - Unix 新文件使用同目录 UUID 临时文件、
create_new(true)与mode(0o600),然后write_all→ filesync_all→ rename → parentsync_all。正常路径不会出现 world-readable 创建窗口,且同文件系统 rename 的结构合理。 - legacy 0644、普通 symlink/hardlink、无效/partial PIN 与最终替换路径都有测试。detached exact-head focused run:
remote_server::pin_persistence_tests6/6 passed;changed-file rustfmt check 与git diff --checkpassed。 - exact head 的 5 个 check runs 全部 success:Android cargo check、macOS checks、Windows checks、Linux checks、PR-Agent。
Critical
- None.
Important
- Unix 的 nonregular/link 防护发生得太晚,并且 permission repair 存在 pathname TOCTOU。
read_valid_persisted_pin在任何symlink_metadata/regular-file 检查前先执行read_to_string(lines 172-184)。因此 FIFO 会在启动路径无限阻塞,设备/超大 regular file 也会先产生 I/O 或无界内存开销;symlink target 也会先被打开。随后repair_pin_permissions是symlink_metadata(path)再set_permissions(path)(lines 245-259),攻击者可在两步间把 path 换成 symlink/hardlink,使 chmod 落到另一个 inode。我的文件系统 contract 复现了两点:FIFO reader 在 250ms 后仍阻塞;通过检查后把 path 换成 symlink,path chmod 将 target 从 0644 改为 0600。请在 Unix 使用O_NOFOLLOW打开一次文件,在同一 fd 上fstat/检查 regular+nlink、File::set_permissions,并限定最多读取 PIN 所需字节;不要在验证后重新按 pathname 打开/chmod。 - Windows fallback 不是原子替换,并会在第二次 rename 失败时删除唯一旧 PIN。
replace_pin_filelines 231-240 在 destination 存在时先remove_file(path),再 rename temp;这中间有可见缺口,且第二次失败会丢失旧文件并错误地返回第一次 rename 的 error。调用链又在Coordinator::regenerate_remote_pin先更新内存 PIN,save_pin吞掉持久化错误后仍 refresh server(coordinator.rs:1557-1564),所以 reset 可报告/启用新 PIN,但磁盘凭据已丢,重启后再变化。这违反 #813 的 atomic replacement 与 persistent/reset semantics。请使用 Windows 原生 replace primitive(例如 ReplaceFileW/等价安全封装),并让 reset 只在持久化成功后提交内存/server 状态。 - PR 的“reject symlink and hard-link PIN paths”在 non-Unix 上并未实现。
repair_pin_permissions的cfg(not(unix))分支无条件Ok(())(lines 263-265),所以 Windows 上一个指向六位文本的 symlink 会被读取并接受;Windows CI 只对主 lib tests--no-run,没有执行这组 Windows 行为测试。至少应跨平台拒绝 symlink/nonregular;若 hard-link rejection 仅承诺 Unix,需要在 PR/代码契约中明确并为 Windows replacement/error path 添加可执行测试。
Minor
new_pin_file_is_owner_only_from_creation只观察 rename 后的最终 mode,测试本身不能证明“from creation”;这一点目前由OpenOptionsExt::mode(0o600)的代码审查支持。建议用 umask/同步观察或封装后的 open helper 测试强化该断言。- PR 当前仍是 Draft,PR base 记录为
fa53f47f…;currentbeta已到c645b168…,exact head 相对 current beta 为 1 behind / 1 ahead。修复后应同步 beta 并重新跑五项 checks。 - 全仓
cargo fmt --all -- --check受大量与本 PR 无关的既有格式 drift 影响而失败;本次只据 changed-filerustfmt --check通过作判断,没有裸跑会写文件的cargo fmt。
Ready-to-merge verdict
COMMENTED — logical REQUEST_CHANGES. 0600 创建、正常 Unix rename 与 legacy repair 主方向正确,但 pathname TOCTOU/nonregular pre-read 与 Windows delete-then-rename 会留下安全和数据持久化缺口。上述 Important 项闭环并补相应失败路径测试前,不建议转 Ready 或合并。
dec4d6d to
aaa0348
Compare
|
Persistent review updated to latest commit aaa0348 |
|
Persistent review updated to latest commit 0e7493e |
0e7493e to
d5b3d09
Compare
|
Persistent review updated to latest commit d5b3d09 |
appergb
left a comment
There was a problem hiding this comment.
Independent security re-review — logical verdict: APPROVE
Reviewed exact head d5b3d09d7833904f9b1b8ebbd419a72a7cb963a9 against current beta 472b914985ec525bb082582fd7968a43c7d9ce41. The current conflict-free merge tree is ddd143c8baa94071aec18f401d1194b1407b268b.
Critical
None.
Important
None. The three prior Important blockers are closed.
Minor
- The Windows runner executes the real successful
ReplaceFileWpath plus symlink, hard-link, size, and generic replacement-failure behavior, but the native backup-restoration branch is still verified by code review/source contract rather than a deterministic Win32 fault-injection test. A future seam coveringERROR_UNABLE_TO_MOVE_REPLACEMENT_2would make the rollback proof stronger. This is non-blocking because the implementation retains a unique same-directory backup and restores it whenever the destination is absent, matching Microsoft's documented failure state: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-replacefilew
Security evidence
- Unix opens once with
O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC, then uses metadata from that descriptor to require a regular single-link file and enforce the 64-byte limit before reading. Permission repair is descriptor-based (File::set_permissions), and the post-validation read is capped at 65 bytes to detect concurrent growth. - The Unix behavior suite proves nonblocking FIFO rejection, device/directory rejection, symlink-to-FIFO and ordinary symlink rejection without target access, hard-link rejection without target mutation, path-swap resistance, bounded oversized-file handling, descriptor-based legacy
0600repair, owner-only temporary creation, and atomic replacement cleanup. - Windows opens with
FILE_FLAG_OPEN_REPARSE_POINTand validates the opened handle usingGetFileTypeandGetFileInformationByHandle, rejecting directories/reparse points, multiple links, non-disk files, and oversized files before reading. - Existing Windows destinations are replaced with
ReplaceFileWand a unique backup; first creation usesMoveFileExW(MOVEFILE_WRITE_THROUGH). There is no delete-old-then-rename window. Documented partial replacement states restore the backup when the destination disappeared, while temporary files are cleaned on failure. save_pinandload_or_create_pinnow returnResult; the Tauri reset command propagates failure. Reset persists first, commits the in-memory PIN second, and refreshes the server last. Focused tests prove a persistence error leaves memory/server state unchanged and success exposes the committed PIN to refresh.
Verification
- Unix PIN persistence:
13/13; focused reset transaction:2/2. - Local Rust-only backend harness:
119/119; exact-head Windows backend harness:130/130, including all 7 Windows PIN tests. Windows MSVC cross-target backend check passed. - Current merge-tree Rust library suite:
687/687;cargo check --locked --libpassed. npm run build, PIN security contract, 9 standalone TypeScript tests, macOS capsule, Windows UI/startup, Android updater, and settings-modal contracts passed.npm audit: 0 vulnerabilities.cargo audit: 0 vulnerabilities and 18 repository-allowed warnings. Gitleaks found no leaks across all four PR commits.git diff --checkpassed.- GitNexus reports low impact, 55 changed symbols, and 0 affected execution flows.
- All five exact-head GitHub checks are green: Android cargo check, Linux checks, Windows checks, macOS checks, and
pr_agent_job.
Exact-head CI ran against the PR's recorded base 9991b325...; beta later advanced by one commit. The local build, cross-checks, and full suites above were run on the current merge tree. The PR remains Draft/Open and was not merged. Because the authenticated account is the PR author, this is submitted as COMMENTED; the semantic verdict is APPROVE.
|
Persistent review updated to latest commit d5b3d09 |
User description
Summary
ReplaceFileW/MoveFileExWplus rollback backup handlingSecurity behavior
Unix
O_NOFOLLOW | O_NONBLOCK | O_CLOEXECfstatmetadata0600through the opened descriptor0600before renameWindows
FILE_FLAG_OPEN_REPARSE_POINTReplaceFileWfor existing destinations andMoveFileExW(MOVEFILE_WRITE_THROUGH)for first creationState consistency
TDD and verification
Exact pushed head:
d5b3d09d7833904f9b1b8ebbd419a72a7cb963a9Rebased onto
beta:9991b325b8ea035ab167d486c5ba3c71688418a9RED evidence included a FIFO-read timeout, oversized-file acceptance, missing atomic-replacement fault seam, and reset transaction compile failures before the implementation was added.
cargo test --locked --manifest-path src-tauri/Cargo.toml --lib: 665 passedcargo test --locked --manifest-path src-tauri/backend-tests/Cargo.toml: 119 passedcargo check --locked ... --target x86_64-pc-windows-msvc: passed29392896847: Android, macOS, Linux, and native Windows checks passed; Windows included Rust backend unit test executionnpm run buildincluding the latestbetaprebuild contract: passednpm audit: 0 vulnerabilitiescargo audit: 0 vulnerabilities; 18 existing allowed maintenance/unsoundness warningsNo visible UI layout changes.
Closes #813
PR Type
Bug fix, Enhancement, Tests
Description
Harden PIN file permissions on Unix to 0600, repair legacy modes
Replace PIN atomically: temp file with 0600, then rename or ReplaceFileW
Validate file type, reject symlinks, FIFOs, oversized files
Propagate persistence errors to Tauri command and memory state
Add tests for atomic replacement, permission repair, and invalid files
Diagram Walkthrough
flowchart LR A["load_or_create_pin"] --> B{File exists?} B -- Yes --> C["Open with security flags"] C --> D["Validate file metadata"] D --> E{Valid?} E -- Yes --> F["Read bounded PIN"] F --> G["Return PIN"] E -- No --> H["Generate new PIN"] B -- No --> H H --> I["Create temp file (0600)"] I --> J["Write PIN + sync"] J --> K["Atomic replace: rename or ReplaceFileW"] K --> L["Return new PIN"]File Walkthrough
2 files
Add PIN persistence test module importCreate security contract test for PIN persistence4 files
Make regenerate_remote_pin return ResultAdd persist-and-commit transaction for PINDelegate PIN persistence to dedicated moduleImplement secure PIN persistence with atomic replacement2 files
Add PIN persistence security check to CIAdd npm script for PIN persistence check2 files
Add dependencies for PIN persistence file operationsAdd dependencies for backend tests