reject symlinks pointing outside the workspace (T-102)

resolveUnderRoot already blocked path-layer traversal but explicitly
did NOT follow symlinks — a repo symlink config -> /etc/shadow
passed the containment check because the link path was under root.
clide would then read the target.

Add resolveUnderRootFollowingSymlinks: resolves any symlinks at the
target and re-verifies containment against the resolved real root.
The split keeps pure path math testable without filesystem access.
files.read and files.ls now route through it.

Tests cover: plain non-symlink passthrough, non-existent target
(returns path-layer result so caller surfaces not-found cleanly),
single-hop and chained symlinks whose targets escape the workspace,
and tolerance of symlinks in the root path itself (macOS /tmp).

Also adds the T-101 CHANGELOG entry that the docs commit missed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 21:01:56 +02:00
co-authored by Claude Opus 4.7
parent dd3b38ba85
commit 06b08b7388
5 changed files with 128 additions and 2 deletions
+4 -2
View File
@@ -74,7 +74,9 @@ void registerFilesCommands(DaemonDispatcher d, FilesService files) {
}
final String absPath;
try {
absPath = resolveUnderRoot(files.root, path);
// Follow symlinks + re-check containment so a `config -> /etc/shadow`
// symlink under the workspace can't be read (T-102).
absPath = resolveUnderRootFollowingSymlinks(files.root, path);
} on PathOutsideRoot {
return IpcResponse.err(id: req.id, error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'path outside workspace: $path'));
}
@@ -90,7 +92,7 @@ void registerFilesCommands(DaemonDispatcher d, FilesService files) {
final dir = (req.args['path'] as String?) ?? '';
if (dir.isNotEmpty) {
try {
resolveUnderRoot(files.root, dir);
resolveUnderRootFollowingSymlinks(files.root, dir);
} on PathOutsideRoot {
return IpcResponse.err(id: req.id, error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'path outside workspace: $dir'));
}