diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f4f5bc..d981aa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ ### Security and Correctness -- **Security:** Resolve symlink aliases component-by-component before applying later `..` segments in the exported root-path validator, and validate raw `root()` inputs before normalized I/O, preventing alias-dependent traversal from being approved under a different lexical path. +- **Security — `resolveRootPath()` / `resolveRootPathSync()`:** Published releases through 0.4.7 validated a lexically normalized path spelling, so a caller-supplied path traversing an in-root symlink could pass validation while resolving outside the root. Version 0.5 fixes this with component-wise alias resolution. `root()` handles were **not** affected: their operations have contained this case since `5ddca80`, so exposure is limited to direct users of these two exported helpers. - Prefer macOS 15.4's `O_RESOLVE_BENEATH` for native opens, retain the guarded component walk on older kernels, and apply an `F_GETPATH` post-open escape detector to both routes without claiming rename-race atomicity. - Report open containment explicitly: native `openBeneath()` returns `{ fd, containment }` with `kernel-atomic` on Linux and `best-effort` on macOS/Windows, while JavaScript root open/read/writable results report `best-effort`. - Serialize async `jsonStore` writes and read-modify-write updates in-process by canonical store path before taking the cross-process sidecar lock, preventing overlapping `write`, `update`, and `updateOr` calls from silently losing updates; reject nested same-path mutations with typed `store-reentrant-update` errors. Thanks @yetval for reporting this. diff --git a/docs/migrating-to-0.5.md b/docs/migrating-to-0.5.md index f077e0d..c3025a4 100644 --- a/docs/migrating-to-0.5.md +++ b/docs/migrating-to-0.5.md @@ -21,6 +21,11 @@ optional. import fs-safe, but native-only features and missing JS archive decoders fail with actionable errors. +If you call `resolveRootPath()` or `resolveRootPathSync()` directly, upgrade to +0.5: versions through 0.4.7 could approve an in-root symlink traversal that +resolved outside the root. `root()` handles were not affected. See the +[affected versions and exposure](security-model.md#affected-versions-exposure). + ## 2. Replace Python helper configuration Change startup configuration before the first filesystem operation: diff --git a/docs/security-model.md b/docs/security-model.md index 85835e5..cbd7de2 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -4,6 +4,19 @@ The same shape exists in other languages: Go's [`os.Root` / `OpenInRoot`](https://go.dev/blog/osroot) and Rust's [`cap-std`](https://github.com/bytecodealliance/cap-std) both expose a root handle whose operations refuse to escape it. `fs-safe` is the Node-side equivalent: a single `root()` capability that carries the boundary across every read, write, move, and remove, instead of leaving each call site to redo `path.resolve(...).startsWith(...)` and hope. +## Affected versions / exposure + +In published releases through 0.4.7, the exported `resolveRootPath()` and +`resolveRootPathSync()` helpers validated a lexically normalized path spelling. +A caller-supplied path traversing an in-root symlink could therefore pass +validation while resolving outside the root. Version 0.5 fixes this with +component-wise alias resolution, resolving each alias before applying later +path components. + +`root()` handles were **not** affected: their operations have contained this +case since `5ddca80`. Exposure is limited to consumers that call +`resolveRootPath()` or `resolveRootPathSync()` directly. + ## Threat model You hand a `root()` boundary to a piece of code that takes caller-controlled relative paths. The library defends against a caller that: