Repository navigation
Repo-scoped read surfaces gated by existence only, not visibility (certs, issues, labels, bounties, stars) #120
Description
Activity
- addedkind:securityVulnerability fix or hardeningVulnerability fix or hardeningcrate:nodegitlawb-node — the serving node and REST APIgitlawb-node — the serving node and REST APIsubsystem:visibilityPath-scoped visibility and content withholdingPath-scoped visibility and content withholdingsubsystem:apiNode REST API request/response surfaceNode REST API request/response surfacesev:highMajor break or real security/trust risk, no easy workaroundMajor break or real security/trust risk, no easy workaround
on Jun 29, 2026 - added a commit that references this issue
on Jun 30, 2026 - added a commit that references this issue
on Jun 30, 2026 One more surface in this class, not in the list above:
list_repo_events(crates/gitlawb-node/src/api/events.rs), routeGET /api/v1/repos/{owner}/{repo}/events(server.rs:322). It takes noAuthenticatedDidand runs noauthorize_repo_read/visibility_check; it looks the repo up by owner/name and returns every local ref certificate plus received ref-update for it. It has never had a gate (not covered by #113).Same private ref metadata as the
certsentry above (ref_name,old_sha,new_sha,pusher_did,node_did,timestamp), since it mergeslist_ref_certificateswithlist_repo_ref_updates.Reproduced by execution, anonymous GET on a private repo with one received ref-update:
GET /api/v1/repos/z6MkOwner/secret/events (is_public=false, no auth) -> 200 {"count":1,"events":[{"ref_name":"refs/heads/main","old_sha":"0000…","new_sha":"aaaa…", "pusher_did":"did:key:z6MkPusher","node_did":"did:key:z6MkNode",…}]}Same fix: thread the optional caller and gate on
listable_at_root(deny -> 404 RepoNotFound). The two cross-repo feeds (events/ref-updates, GraphQLrefUpdates) were separately gated in #143; this per-repo endpoint is distinct and still open.- added 4 commits that reference this issue
on Jul 6, 2026 - added a commit that references this issue
on Jul 8, 2026 - added a commit that references this issue
on Jul 22, 2026
Summary
Several repo-scoped read handlers verify the repo exists (
get_repo) but never run the visibility gate (authorize_repo_read/visibility_check), so they return a private repo's metadata to an anonymous caller. This is the same class as #94 (hooks) and its PR #113 siblings (events, replicas, protected branches); these are the surfaces that batch did not cover. The mutation handlers on the same files do gate correctly; only the reads were missed.Where (all verified by reading the handler bodies; none take an
AuthenticatedDidcaller)crates/gitlawb-node/src/api/certs.rs—list_certs(:10),get_cert(:42). Returnsref_name,old_sha,new_sha,pusher_did,node_didfor the repo. This is the same private ref metadata as Unauthenticated GET /api/v1/events/ref-updates leaks private-repo ref metadata (REST analog of #112) #114 / git-receive-pack info/refs advertisement skips the visibility gate, leaking private repo ref metadata #116 (branch names, commit SHAs, pusher identity).crates/gitlawb-node/src/api/issues.rs—list_issues(:90),get_issue(:120),list_issue_comments(:191). Returns the repo's issue titles/bodies/comments from its git store.crates/gitlawb-node/src/api/labels.rs—list_labels(:74). Returns the repo's label set.crates/gitlawb-node/src/api/bounties.rs—list_repo_bounties(:114). Returns the repo's bounties (no repo lookup at all).crates/gitlawb-node/src/api/stars.rs—get_star_status(:78). Existence + star count oracle for a private repo.All sit on the anonymous read group (
optional_signature).Impact
For any private repo whose owner-DID slug an attacker can guess or enumerate, an unauthenticated request discloses its ref history and pusher identity (certs), issue content, labels, bounties, and existence.
certsis the most severe (ref names + commit SHAs + pusher DID, identical to the #114/#116 leak class); the others are lower but still leak private-repo metadata that the read path otherwise withholds.Fix
Add
authorize_repo_read(&state, &owner, &repo, caller, "/").await?to each read handler (deny -> 404 RepoNotFound, existence-hiding), threading theOption<Extension<AuthenticatedDid>>caller, exactly as #113 did for events/replicas/protected. The matchinggl/MCP read callers must then sign (the #115 client-signing class).Found during a read-only egress audit of every repo-data surface.