Summary
GET /ipfs/{cid} (get_by_cid) serves tree, commit, and tag objects unconditionally under path-scoped visibility rules, disclosing the structure of a withheld subtree (every child filename and child object id) to a caller who is denied that subtree. Blob content stays protected; this is a metadata/structure leak, not a content leak.
This is the KTD3 behavior that #126 explicitly scoped out, surfaced during review of #133 (which correctly fixes the blob-content half of the same endpoint). Filing so the divergence is tracked rather than buried in a PR thread.
Where
crates/gitlawb-node/src/api/ipfs.rs (the per-object gate):
let path_scoped = has_path_scoped_rule(rules);
if path_scoped && obj_type == "blob" {
// ... allowed-set membership check; non-blobs skip it
}
Under a path-scoped rule the allowed-set gate applies only to obj_type == "blob". A tree/commit/tag object falls through and is served. A git tree object's raw body is <mode> <filename>\0<raw-oid> per entry, so fetching the tree CID of a withheld subtree returns every child filename and child oid in cleartext, recursively. The leaf blobs themselves still require allowed-set membership, so their content is protected.
Why it matters
This is the same confidentiality boundary that get_tree (crates/gitlawb-node/src/api/repos.rs:444, the N3 fix) deliberately defends: it gates on the requested subtree path precisely so "a caller denied a withheld subtree can still enumerate its names/SHAs" cannot happen. The two read surfaces over the same git data disagree: the REST tree-read path protects subtree names/oids, the IPFS CID path does not.
Bounded by the attacker needing the tree object's sha256 CID to request it. That is not free, but CID-enumeration surfaces exist (see #121), so it should not be relied on as the only barrier.
Scope / not a regression
Pre-existing. The pre-#133 deny-set (withheld_blob_oids) also enumerated only blob oids, so trees were never withheld there either. #133 preserves the behavior (and the merged test at test_support.rs asserts "tree object is served to anon (KTD3)"). This issue is about closing the divergence, not about #133.
Suggested direction
Gate non-blob objects under path-scoped rules against their own reachable path the way get_tree does, or make a deliberate, documented decision that tree/commit structure is non-confidential and reconcile get_tree to match. Either way the two surfaces should agree. A handler test should pin the chosen behavior by asserting the served body contents, not just the status code.
Summary
GET /ipfs/{cid}(get_by_cid) serves tree, commit, and tag objects unconditionally under path-scoped visibility rules, disclosing the structure of a withheld subtree (every child filename and child object id) to a caller who is denied that subtree. Blob content stays protected; this is a metadata/structure leak, not a content leak.This is the KTD3 behavior that #126 explicitly scoped out, surfaced during review of #133 (which correctly fixes the blob-content half of the same endpoint). Filing so the divergence is tracked rather than buried in a PR thread.
Where
crates/gitlawb-node/src/api/ipfs.rs(the per-object gate):Under a path-scoped rule the allowed-set gate applies only to
obj_type == "blob". Atree/commit/tagobject falls through and is served. A git tree object's raw body is<mode> <filename>\0<raw-oid>per entry, so fetching the tree CID of a withheld subtree returns every child filename and child oid in cleartext, recursively. The leaf blobs themselves still require allowed-set membership, so their content is protected.Why it matters
This is the same confidentiality boundary that
get_tree(crates/gitlawb-node/src/api/repos.rs:444, the N3 fix) deliberately defends: it gates on the requested subtree path precisely so "a caller denied a withheld subtree can still enumerate its names/SHAs" cannot happen. The two read surfaces over the same git data disagree: the REST tree-read path protects subtree names/oids, the IPFS CID path does not.Bounded by the attacker needing the tree object's sha256 CID to request it. That is not free, but CID-enumeration surfaces exist (see #121), so it should not be relied on as the only barrier.
Scope / not a regression
Pre-existing. The pre-#133 deny-set (
withheld_blob_oids) also enumerated only blob oids, so trees were never withheld there either. #133 preserves the behavior (and the merged test attest_support.rsasserts "tree object is served to anon (KTD3)"). This issue is about closing the divergence, not about #133.Suggested direction
Gate non-blob objects under path-scoped rules against their own reachable path the way
get_treedoes, or make a deliberate, documented decision that tree/commit structure is non-confidential and reconcileget_treeto match. Either way the two surfaces should agree. A handler test should pin the chosen behavior by asserting the served body contents, not just the status code.