Skip to content

Private repos (is_public=false) are enumerable via unauthenticated list/stats/GraphQL surfaces #97

Description

@beardthelion

Summary

A repo created with is_public = false is still fully enumerable by an unauthenticated caller. is_public gates content reads (blob/tree/clone via visibility_check) but no listing surface honors it, so a private repo's name, owner_did, description, is_public flag, default_branch, and created_at are returned to anyone. This is a metadata/discoverability leak, not a content leak.

Affected surfaces (all unauthenticated)

  • GET /api/v1/repos (list) — crates/gitlawb-node/src/api/repos.rs:230-246; route mounted open at server.rs:277.
  • GET /api/v1/repos/federatedrepos.rs:1126; route open at server.rs:278.
  • GET /api/v1/stats repos count — server.rs:454 via Db::count_repos_deduped; route open at server.rs:400. Counts private repos toward the public total.
  • GraphQL repos query — crates/gitlawb-node/src/graphql/query.rs:12. /graphql runs queries without auth (server.rs:61-67; auth is attached only when a signature is present, and only mutations enforce it).

Each maps rows through to_response / RepoType, which include name, owner_did, description, is_public, default_branch, created_at.

Verified

  • visibility_check only consumes is_public for path/content authorization (visibility.rs, api/mod.rs, git/visibility_pack.rs, api/encrypted.rs, api/pulls.rs). No listing query filters on it.
  • Content is still gated: for is_public = false, no rules, anonymous caller, path /, visibility_check returns Deny (visibility.rs:89-94), so clone/blob/tree of a private repo are blocked. Only metadata leaks.
  • Pre-existing: the REST list path has always enumerated all rows; this is not introduced by the recent dedup work (fix(node): dedupe mirror and canonical repo rows on list surfaces (#6) #73), which only added dedup to the GraphQL and stats surfaces.

Reproduce

  1. Create a repo with is_public: false.
  2. As an anonymous caller: GET /api/v1/repos, GET /api/v1/stats, or POST { repos { name ownerDid description } } to /graphql.
  3. The private repo's name/owner/description appear in the response and its existence counts toward /api/v1/stats.

Suggested direction

Have the listing surfaces honor is_public for non-owner/anonymous callers: a repo should appear in a listing only when visibility_check(rules, is_public, owner_did, caller, "/") == Allow (the same gate clone uses at root), or, more cheaply for the anonymous case, filter to is_public = true plus repos the authenticated caller owns. The count surface (count_repos_deduped) needs the matching filter so the stat and the list agree. The owner viewing their own repos must still see their private ones.

Open question

Confirm intent: should is_public = false hide a repo from anonymous discovery, or is repo existence intentionally public with privacy enforced only at the content layer? If the latter, this is a documentation fix (state that listings are public by design) rather than a code change. Filing because is_public exists specifically to mark a repo private and no surface currently honors it for discovery.

Metadata

Metadata

Assignees

No one assigned

    Labels

    crate:nodegitlawb-node — the serving node and REST APIkind:securityVulnerability fix or hardeningsev:mediumDegraded but workaround existssubsystem:apiNode REST API request/response surfacesubsystem:visibilityPath-scoped visibility and content withholding

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions