Summary
The pin path fails open relative to its candidate set, so an unreachable private blob can be replicated to public IPFS/Pinata in cleartext.
replicable_objects (crates/gitlawb-node/src/git/visibility_pack.rs) replicates any OID not explicitly in the withheld set:
all.into_iter().filter(|oid| !withheld.contains(oid)).collect()
The withheld set comes from blob_paths, which walks reachable objects only (git rev-list --all plus HEAD). That is correct for the serve path: upload-pack never exposes unreachable objects. But since #90, the pin full-scan fallback push_delta::list_all_objects (crates/gitlawb-node/src/git/push_delta.rs) runs git cat-file --batch-all-objects, which by its own docstring returns every object including unreachable/dangling ones. A private blob that is in the object DB but unreachable from any ref or HEAD is therefore never classified, never withheld, and replicates in cleartext.
Trigger
- An unreachable private blob: a force-push, amend, rebase, or reset that orphans a commit containing a secret, in the pre-GC window.
- A full-scan pin: the
FullScanRequired path in resolve_candidates_for_push (non-commit push tip, a rev-list error, or force-full-scan), or any reconciliation sweep that calls list_all_objects.
Scope
Present on main today via #90; independent of the withheld-classifier work in #84, which only walks the reachable graph (correct for serve). The asymmetry is between the reachable-only classifier and the all-objects pin candidate set.
Fix direction
Fail closed on the pin path: a candidate OID the classifier never assigned a path to should be treated as withheld when a path-scoped rule is active, rather than replicated. Equivalently, intersect the pin candidate set with the reachable set when a withholding policy applies. Keeping --batch-all-objects for the reconciliation sweep is fine as long as unclassified objects are withheld, not pinned.
Severity
Data exposure (a cleartext private blob to public IPFS). Low likelihood: requires an unreachable private blob, a full-scan pin trigger, and the pre-GC window.
Evidence
replicable_objects fail-open: crates/gitlawb-node/src/git/visibility_pack.rs
- candidate set
list_all_objects (cat-file --batch-all-objects): crates/gitlawb-node/src/git/push_delta.rs
- consumed by
pin_new_objects: crates/gitlawb-node/src/ipfs_pin.rs, crates/gitlawb-node/src/pinata.rs
Summary
The pin path fails open relative to its candidate set, so an unreachable private blob can be replicated to public IPFS/Pinata in cleartext.
replicable_objects(crates/gitlawb-node/src/git/visibility_pack.rs) replicates any OID not explicitly in thewithheldset:The
withheldset comes fromblob_paths, which walks reachable objects only (git rev-list --allplusHEAD). That is correct for the serve path:upload-packnever exposes unreachable objects. But since #90, the pin full-scan fallbackpush_delta::list_all_objects(crates/gitlawb-node/src/git/push_delta.rs) runsgit cat-file --batch-all-objects, which by its own docstring returns every object including unreachable/dangling ones. A private blob that is in the object DB but unreachable from any ref orHEADis therefore never classified, never withheld, and replicates in cleartext.Trigger
FullScanRequiredpath inresolve_candidates_for_push(non-commit push tip, a rev-list error, or force-full-scan), or any reconciliation sweep that callslist_all_objects.Scope
Present on
maintoday via #90; independent of the withheld-classifier work in #84, which only walks the reachable graph (correct for serve). The asymmetry is between the reachable-only classifier and the all-objects pin candidate set.Fix direction
Fail closed on the pin path: a candidate OID the classifier never assigned a path to should be treated as withheld when a path-scoped rule is active, rather than replicated. Equivalently, intersect the pin candidate set with the reachable set when a withholding policy applies. Keeping
--batch-all-objectsfor the reconciliation sweep is fine as long as unclassified objects are withheld, not pinned.Severity
Data exposure (a cleartext private blob to public IPFS). Low likelihood: requires an unreachable private blob, a full-scan pin trigger, and the pre-GC window.
Evidence
replicable_objectsfail-open:crates/gitlawb-node/src/git/visibility_pack.rslist_all_objects(cat-file --batch-all-objects):crates/gitlawb-node/src/git/push_delta.rspin_new_objects:crates/gitlawb-node/src/ipfs_pin.rs,crates/gitlawb-node/src/pinata.rs