Skip to content

bug: /api/gt/repos proxy uses privileged PAT with no repo allowlist - authenticated users can read any accessible private repo #141

@dlhiccup

Description

@dlhiccup

Description

Routes under /api/gt/repos/[owner]/[name]/ proxy GitHub API calls through the server's privileged PAT (GITHUB_PAT / GITHUB_PATS) without validating that the requested owner/name is in the tracked/allowed repo list. Any authenticated dashboard user can supply an arbitrary owner and repo name to read files, READMEs, or metadata from repos the PAT can access — including private repos the user was never meant to see.

Affected routes (all follow the same pattern):

Route GitHub API call
GET /api/gt/repos/[owner]/[name]/contents?path= repos.getContent — returns full file content
GET /api/gt/repos/[owner]/[name]/readme repos.getReadme — returns full README markdown
GET /api/gt/repos/[owner]/[name]/contributing repos.getContent on CONTRIBUTING.md
GET /api/gt/repos/[owner]/[name]/health various repo metadata calls
GET /api/gt/repos/[owner]/[name]/miners repos.listContributors or similar

Steps to Reproduce

  1. Log in to the dashboard with any valid account.
  2. Send a GET request with an arbitrary repo not in the tracked list:
    GET /api/gt/repos/<any-org>/<private-repo>/contents?path=.env
    
  3. The server fetches the file using its privileged PAT and returns the full content.

No special privileges required beyond a valid session cookie.

Expected Behavior

Requests for repos not in the tracked/allowed list should be rejected with 404 or 403. The server's PAT should only be used to fetch repos that are explicitly configured in the dashboard's repo list.

Actual Behavior

src/app/api/gt/repos/[owner]/[name]/contents/route.ts (lines 20–21) passes params.owner, params.name, and the raw ?path= query param directly to the GitHub API with no allowlist check:

const r = await withRotation((octokit) =>
  octokit.rest.repos.getContent({ owner: params.owner, repo: params.name, path }),
);

The same unchecked pattern applies to every other route in the same directory. The only protection is the auth middleware (valid session required), but any legitimate dashboard user can exploit this.

Worst-case scenario: If GITHUB_PAT is a classic token with repo scope, the attacker can read any file from any private repo the token owner has access to — including .env files, credentials, and private source code across the entire organization.

Proposed Fix

Validate owner/name against the allowed repo list before proxying the GitHub API call. Add this check at the top of each affected route handler:

import { getLiveReposAsyncServer } from '@/lib/repos-server';

const { repos } = await getLiveReposAsyncServer();
const fullName = `${params.owner}/${params.name}`.toLowerCase();
const allowed = repos.some((r) => r.fullName.toLowerCase() === fullName);
if (!allowed) return NextResponse.json({ error: 'Not found' }, { status: 404 });

Alternatively, centralise the check in a shared helper to avoid repeating it across all five routes.

Environment

  • Browser: N/A (API route)
  • OS: N/A
  • Node version: any

Additional Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions