Skip to content

GraphQL refUpdates serves private-repo ref metadata to anonymous callers (visibility bypass) #112

Description

@beardthelion

The GraphQL refUpdates query is not visibility-gated, so an anonymous caller can read ref-level metadata for a private repo by passing its name. This is the same withholding-bypass class as #97 (repo listing) and #110 (blob content), on the GraphQL ref-history surface. The #97 visibility gate was applied to the sibling repos resolver but not to refUpdates.

Where

crates/gitlawb-node/src/graphql/query.rs, the ref_updates resolver (async fn ref_updates, around line 48).

  • The resolver calls db.list_ref_updates_filtered(repo.as_deref(), limit) and maps every row straight to RefUpdateType. It never reads ctx.data::<crate::auth::AuthenticatedDid>() and never consults visibility rules. The repos resolver directly above it threads the caller DID through crate::visibility::listable_at_root; ref_updates does not.
  • Db::list_ref_updates_filtered (crates/gitlawb-node/src/db/mod.rs, ~line 1975) runs SELECT ... FROM received_ref_updates WHERE repo=$1 ORDER BY timestamp DESC LIMIT $2 with no is_public, no visibility join, and no reader-DID predicate. The unfiltered branch (no repo arg) is equally open.
  • /graphql is wrapped only by auth::optional_signature, not require_signature (crates/gitlawb-node/src/server.rs:66), so the query is reachable anonymously.

Impact

For any private repo whose name an attacker can guess or enumerate, an unauthenticated GraphQL query returns its refName, oldSha, newSha, pusherDid, nodeDid, and timestamp. That discloses the repo's existence, its branch/tag names, commit SHAs, and the identity of who pushed, for a repo the read path otherwise withholds.

Verified by execution: a test that seeds a private repo (is_public=false), records a received ref update for it, then runs an anonymous { refUpdates(repo: "secret-repo") { refName newSha pusherDid } } returns the row:

{"refUpdates":[{"newSha":"deadbeef...","pusherDid":"did:key:zPUSHER...","refName":"refs/heads/main"}]}

Suggested remediation

Gate ref_updates on the same "/" visibility decision the repos resolver and the per-repo endpoints use. Resolve the named repo, fetch its visibility rules, and return ref data only when listable_at_root(rules, is_public, owner_did, caller) allows the caller (anonymous = None); fail closed (empty result) otherwise. The unfiltered branch must filter each row's repo through the same gate before returning.

Metadata

Metadata

Assignees

No one assigned

    Labels

    crate:nodegitlawb-node — the serving node and REST APIkind:securityVulnerability fix or hardeningsev:highMajor break or real security/trust risk, no easy workaroundsubsystem: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