fork_repo authorizes the caller only at the whole-repo / path, then git clone --mirrors the entire source. A caller allowed at / but denied a path-scoped subtree can fork the full mirror and obtain blobs the filtered git_upload_pack path would have withheld. This bypasses path-scoped visibility (INV-2).
Where
crates/gitlawb-node/src/api/repos.rs
fork_repo authorizes at / only:
let (source, _rules) =
crate::api::authorize_repo_read(&state, &owner, &name, Some(auth.0.as_str()), "/").await?;
- then clones the whole repo as a mirror:
let output = std::process::Command::new("git")
.args(["clone", "--mirror", source_path..., disk_path...])
The _rules returned by authorize_repo_read are discarded, so any path-scoped withholding is ignored for the fork path.
Impact
A non-owner caller with root (/) read access but without access to a path-scoped subtree (e.g. /secret/**) can fork the repo and read every blob in that subtree from their own copy. The read path (git_upload_pack) withholds those blobs via withheld_blob_oids; the fork path does not, so fork is a clean bypass of path-scoped visibility.
This is pre-existing (not introduced by #60). Surfaced while reviewing #60, which optimizes the same withheld-walk on the read path; the fork path defeats that invariant wholesale.
Suggested remediation
Either:
- Fail closed for non-owners when the source has any path-scoped rule (
has_path_scoped_rule), using did_matches(...) for the owner-only bypass, and preserve the existing not-found-style response for protected-data failures; or
- Build the fork from the same filtered projection used by
git_upload_pack rather than from a full --mirror, so withheld blobs are never copied.
Option 1 is the smaller, safer fix; option 2 preserves fork-for-readers semantics but is more involved. Owner forks (and forks of repos with no path-scoped rules) must continue to work unchanged.
fork_repoauthorizes the caller only at the whole-repo/path, thengit clone --mirrors the entire source. A caller allowed at/but denied a path-scoped subtree can fork the full mirror and obtain blobs the filteredgit_upload_packpath would have withheld. This bypasses path-scoped visibility (INV-2).Where
crates/gitlawb-node/src/api/repos.rsfork_repoauthorizes at/only:The
_rulesreturned byauthorize_repo_readare discarded, so any path-scoped withholding is ignored for the fork path.Impact
A non-owner caller with root (
/) read access but without access to a path-scoped subtree (e.g./secret/**) can fork the repo and read every blob in that subtree from their own copy. The read path (git_upload_pack) withholds those blobs viawithheld_blob_oids; the fork path does not, so fork is a clean bypass of path-scoped visibility.This is pre-existing (not introduced by #60). Surfaced while reviewing #60, which optimizes the same withheld-walk on the read path; the fork path defeats that invariant wholesale.
Suggested remediation
Either:
has_path_scoped_rule), usingdid_matches(...)for the owner-only bypass, and preserve the existing not-found-style response for protected-data failures; orgit_upload_packrather than from a full--mirror, so withheld blobs are never copied.Option 1 is the smaller, safer fix; option 2 preserves fork-for-readers semantics but is more involved. Owner forks (and forks of repos with no path-scoped rules) must continue to work unchanged.