{"record":{"id":"6d8443b1a3ea0b7b","repo":"gitbutlerapp/gitbutler","slug":"path-to-read-from-at-isn-t-in-the-worktree-di","errorCode":null,"errorMessage":"Path to read from at '{}' isn't in the worktree directory '{}'","messagePattern":"Path to read from at '(.+?)' isn't in the worktree directory '(.+?)'","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/gitbutler-repo/src/commands.rs","lineNumber":308,"sourceCode":"            Some(entry) => {\n                let blob = repo.find_blob(entry.id())?;\n                FileInfo::from_content(relative_path, &blob.data)\n            }\n            None => FileInfo::deleted(),\n        })\n    }\n\n    /// Note that `path` can be relative or absolute, and we must validate that it's in the worktree.\n    fn read_file_from_workspace(&self, path: &Path) -> Result<FileInfo> {\n        let workdir = self.workdir_or_fail()?;\n        let canonical_workdir = gix::path::realpath(&workdir)?;\n        let path = gix::path::realpath(canonical_workdir.join(path))?;\n        // Double-check that the path is still in the worktree - this might not be the case\n        // if it was aboslute to begin with, or leads through symlinks.\n        let relative_path = match path.strip_prefix(&canonical_workdir) {\n            Ok(relative_path) => relative_path.to_owned(),\n            Err(_) => {\n                bail!(\n                    \"Path to read from at '{}' isn't in the worktree directory '{}'\",\n                    path.display(),\n                    canonical_workdir.display()\n                );\n            }\n        };\n\n        let out = match path.symlink_metadata() {\n            Ok(md) => {\n                if md.is_file() {\n                    let content = std::fs::read(&path)?;\n                    FileInfo::from_content(&relative_path, &content)\n                } else if md.is_symlink() {\n                    let content = std::fs::read_link(&path)?;\n                    FileInfo::utf8_text_or_binary(&relative_path, &gix::path::into_bstr(content))\n                } else if md.is_dir() {\n                    // Directories on disk (notably git submodules, which appear\n                    // as real directories in the worktree but are represented","sourceCodeStart":290,"sourceCodeEnd":326,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/gitbutler-repo/src/commands.rs#L290-L326","documentation":"Thrown by read_file_from_workspace when the requested path, after both it and the worktree root are canonicalized with gix::path::realpath, no longer lies inside the worktree. The double-check exists because the input may be absolute or travel through symlinks; realpath resolves symlinks, so a link inside the worktree pointing outside is caught. This is a sandbox guard preventing reads of arbitrary files through the workspace API.","triggerScenarios":"Calling read_file_from_workspace(path) where path is absolute and outside the worktree, contains '..' components that escape after canonicalization, or traverses a symlink inside the worktree that resolves to a location outside it - strip_prefix(canonical_workdir) then fails.","commonSituations":"Monorepos where build tooling symlinks node_modules or cache directories outside the tree; user- or agent-supplied absolute paths passed unfiltered; worktrees checked out under symlinked directories (macOS /tmp -> /private/tmp style) making prefix comparison fail.","solutions":["Pass paths relative to the worktree root and let the API join them onto the workdir","Remove or relocate symlinks that resolve outside the worktree","If you legitimately need a file outside the worktree, read it directly with std::fs instead of this API"],"exampleFix":"// before: read_file_from_workspace(Path::new(\"/etc/hosts\"))\n// after: read only files inside the worktree, by relative path\nread_file_from_workspace(Path::new(\"src/main.rs\"))?;","handlingStrategy":"validation","validationCode":"let workdir = gix::path::realpath(repo.workdir())?;\nlet candidate = gix::path::realpath(workdir.join(path))?;\nif candidate.strip_prefix(&workdir).is_err() {\n    // would escape the worktree (symlink or absolute path) - reject before calling\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat workspace file APIs as sandboxed to the worktree","Audit symlinks inside repos consumed by automation","Canonicalize user-supplied paths before making policy decisions"],"tags":["git","path-validation","symlink","security","sandbox"],"backgroundTag":"path-traversal-rejected","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}