mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-30 12:09:15 +00:00
fix(stacks): harden stack file path containment against symlink escapes (#1415)
* fix(stacks): harden stack file path containment against symlink escapes The legacy managed-stack methods enforced path containment lexically (path.resolve + startsWith), which does not follow symlinks. A stack directory under the compose root that is itself a symlink or junction could let a managed-file operation (write compose.yaml/.env, delete a stack, backup, restore, snapshot) follow the link and read, write, or delete a file of the same name outside the compose root. Add a realpath-based containment guard that walks up to the deepest existing path component, confirms its canonical location is inside the canonical compose root, and rejects both an out-of-tree resolution and a dangling symlink (which a write or mkdir would still follow). The guard runs at every legacy managed-stack sink, alongside the existing lexical barriers. A legitimately symlinked compose root is not a false positive because both sides are canonicalized through the same link, and the guard is a no-op for not-yet-created targets so stack creation and the flat-to-directory migration are unaffected. * fix(stacks): satisfy the path-injection sanitizer in the symlink containment guard assertRealWithinBase resolves a user-derived path and probes it with realpath/lstat to run the containment check, which static analysis flags as path injection because the probes lacked the inline barrier its sanitizer recognizes. Add the canonical path.resolve + startsWith barrier at the top of the helper, the same form every other sink in this file uses, and seed the realpath walk from the sanitized value. Behavior is unchanged: callers always pass an absolute, already-contained path, so the barrier is a no-op pre-check for them, and the realpath walk still catches the symlink and dangling-link escapes.
This commit is contained in:
@@ -27,6 +27,12 @@ vi.mock('fs', () => ({
|
||||
rename: vi.fn(),
|
||||
copyFile: vi.fn(),
|
||||
unlink: vi.fn(),
|
||||
// deleteStack now realpath-checks the stack dir against the compose root
|
||||
// before rm. Resolve to the absolute path (no symlink) so the containment
|
||||
// guard passes and these tests still exercise the rm error translation;
|
||||
// the guard's symlink-escape behaviour is covered in
|
||||
// filesystem-symlink-escape.test.ts.
|
||||
realpath: vi.fn((p: string) => Promise.resolve(path.resolve(p))),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user