Skip to content

REST read API bypasses path-scoped visibility: private repo/subtree content served unauthenticated #51

@beardthelion

Description

@beardthelion

Summary

The repo-scoped REST read endpoints do not enforce path-scoped visibility. Private repositories, and private subtrees of public repositories, have their content and metadata served to unauthenticated callers. This bypasses the entire withholding mechanism that the git smart-HTTP and replication paths enforce.

The git clone/fetch path (git_upload_pack / info_refs) and the IPFS/Pinata replication path both gate on visibility_check and withhold private blobs correctly. The REST API does not, so the same private bytes are reachable through a different door.

Impact

Highest severity is content disclosure. GET /api/v1/repos/{owner}/{repo}/blob/{*path} returns the raw bytes of any file, including files in a withheld subtree and every file in a fully-private repo, with no auth and no visibility check:

GET /api/v1/repos/alice/website/blob/secret/.env      -> 200, cleartext

GET .../pulls/{n}/diff likewise returns diffs (file content) ungated.

Affected endpoints (confirmed, all with zero visibility/is_public checks)

Content:

  • GET /blob/{*path} (repos::get_blob) — raw file bytes
  • GET /pulls/{n}/diff (pulls::get_pr_diff) — diffs

Structure / metadata:

  • GET /tree, GET /tree/{*path} (repos::get_tree_root, get_tree)
  • GET /commits (repos::list_commits) — commit messages
  • GET /refs (repos::list_refs)
  • GET /changelog (changelog::get_changelog)
  • GET /pulls, GET /pulls/{n} (pulls::list_prs, get_pr)
  • GET /repos/{owner}/{repo} (repos::get_repo) — repo metadata

These are mounted in the read_routes router ("open for public repos") and merged with no gating layer, so they are fully public.

Root cause

Path-scoped visibility was retrofitted onto the git protocol and replication paths but never applied to the REST read surface. The handlers call get_repo and read straight from disk; none consult list_visibility_rules or visibility_check.

Scope

Pre-existing. get_blob dates to the initial public release and is present on main today, so this is a live hole independent of the in-flight visibility work. It is called out here because the visibility/withholding feature is meaningless while this bypass exists.

Suggested fix

Two tiers:

  1. Repo-level read gate for every repo-scoped read endpoint: resolve the repo, load its rules, and visibility_check(rules, is_public, owner_did, caller, "/"); return 404 on Deny. This closes fully-private repos across the whole surface and is best done so new routes cannot silently skip it.
  2. Path-level gate for endpoints that expose subtree content: get_blob must additionally visibility_check at the requested file path, because a public repo with a private subtree passes the / gate but the specific blob must still be withheld. get_pr_diff needs equivalent treatment.

To authenticate authorized readers over REST (so the gate allows them, not just the owner/public), the gated routes need the optional_signature middleware, mirroring the git read routes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    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