Skip to content

fix: reject requests for repos not in the tracked allowlist#142

Merged
MkDev11 merged 4 commits into
MkDev11:mainfrom
web-dev0521:fix/repo-route-allowlist-check
May 25, 2026
Merged

fix: reject requests for repos not in the tracked allowlist#142
MkDev11 merged 4 commits into
MkDev11:mainfrom
web-dev0521:fix/repo-route-allowlist-check

Conversation

@web-dev0521
Copy link
Copy Markdown
Contributor

@web-dev0521 web-dev0521 commented May 22, 2026

Summary

All five GitHub-PAT-proxying routes under /api/gt/repos/[owner]/[name]/ were forwarding arbitrary owner/name params directly to Octokit with no allowlist check, letting any authenticated dashboard user read files, READMEs, and metadata from private repos accessible to the server's PAT.

  • Added src/lib/assert-tracked-repo.ts — a single async helper that resolves the live repo list via getLiveReposAsyncServer() and returns a 404 Not Found response for any owner/name not present in it.
  • Called assertTrackedRepo() at the top of every PAT-proxying GET handler before the first Octokit call: contents, readme, contributing, health, and the base route.ts.

Related Issues

Closes #141

Type of Change

  • Bug fix

Testing

  • pnpm build passes (TypeScript type-check clean)
  • Manual browser smoke test (for UI changes)
  • N/A — docs / config only

Verification: Requesting /api/gt/repos/some-random-org/private-repo/contents?path=.env now returns {"error":"Not found"} with status 404. Tracked repos continue to work as before.

Checklist

  • Self-reviewed the diff
  • Follows existing code patterns and naming
  • No unrelated changes included
  • Documentation updated if behavior changed

Summary by CodeRabbit

  • New Features
    • Added a server-side repository tracking check and access gate used across API endpoints (contents, readme, contributing, health, repo metadata, issues, pulls, timelines, related items, owner comments).
    • Requests for untracked repos are now immediately rejected with a 404-style response; allowed repos retain existing behaviors and responses.

Review Change Stack

All five GitHub-PAT-proxying routes under
/api/gt/repos/[owner]/[name]/ were passing arbitrary owner/name
params directly to octokit without checking the tracked repo list,
allowing any authenticated dashboard user to read files, READMEs and
metadata from private repos accessible to the PAT.

Add assertTrackedRepo() helper (src/lib/repos-server) that resolves
the live repo list and returns 404 for any owner/name not present.
The helper is called at the top of every PAT-proxying GET handler
before the first octokit call.

Closes MkDev11#141
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a8f330e-0832-4cac-8517-5a13a9839942

📥 Commits

Reviewing files that changed from the base of the PR and between cb48351 and 0ba6de7.

📒 Files selected for processing (4)
  • src/app/api/related-prs/[owner]/[name]/[number]/route.ts
  • src/app/api/repos/[owner]/[name]/owner-comments/route.ts
  • src/lib/assert-tracked-repo.ts
  • src/lib/repos-server.ts

📝 Walkthrough

Walkthrough

Adds a new exported helper assertTrackedRepo(owner, name) that checks the live tracked repo list and returns a 404 NextResponse when a repo is not allowed; integrates this preflight check into multiple API GET handlers so requests for untracked repos are short‑circuited before any privileged GitHub calls.

Changes

Repo Allowlist Authorization

Layer / File(s) Summary
Centralized repo allowlist helper
src/lib/assert-tracked-repo.ts, src/lib/repos-server.ts
assertTrackedRepo(owner, name) delegates to isTrackedRepoServer(fullName) and returns a 404 NextResponse when the repo is not tracked; isTrackedRepoServer refreshes live data and falls back to a DB membership check.
Apply allowlist to /api/gt/repos endpoints
src/app/api/gt/repos/[owner]/[name]/contents/route.ts, src/app/api/gt/repos/[owner]/[name]/contributing/route.ts, src/app/api/gt/repos/[owner]/[name]/health/route.ts, src/app/api/gt/repos/[owner]/[name]/readme/route.ts, src/app/api/gt/repos/[owner]/[name]/route.ts
Each route imports assertTrackedRepo and invokes it early in the GET handler, returning its denial NextResponse immediately for untracked repos; otherwise existing content/readme/contributing/health logic is unchanged.
Apply allowlist to issue/pull/related/timeline endpoints
src/app/api/issue/[owner]/[name]/[number]/route.ts, src/app/api/issue/[owner]/[name]/[number]/timeline/route.ts, src/app/api/pull/[owner]/[name]/[number]/route.ts, src/app/api/related-issues/[owner]/[name]/[number]/route.ts, src/app/api/related-prs/[owner]/[name]/[number]/route.ts, src/app/api/repos/[owner]/[name]/owner-comments/route.ts
Each route imports assertTrackedRepo and calls it at the top of GET, short‑circuiting with the denial response when applicable; otherwise existing issue/pull/timeline/related/owner-comments flows continue unchanged.
sequenceDiagram
  participant Client
  participant APIRoute
  participant assertTrackedRepo
  participant isTrackedRepoServer
  participant DB
  Client->>APIRoute: GET /api/.../{owner}/{name}
  APIRoute->>assertTrackedRepo: assertTrackedRepo(owner, name)
  assertTrackedRepo->>isTrackedRepoServer: isTrackedRepoServer(fullName)
  isTrackedRepoServer->>DB: DB repo_weights lookup (fallback)
  DB-->>isTrackedRepoServer: membership boolean
  isTrackedRepoServer-->>assertTrackedRepo: allowed? (true/false)
  assertTrackedRepo-->>APIRoute: NextResponse or null
  APIRoute-->>Client: 404 short-circuit or continue to GitHub calls
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I guard the list and check each name,
A quiet 404 puts risk to shame,
One helper hops where gaps were wide,
Five gates now close, no secrets slide,
The rabbit nods — the repo gate is tied.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix: reject requests for repos not in the tracked allowlist' directly and clearly summarizes the main security fix: validating that API requests only access tracked repos.
Linked Issues check ✅ Passed The PR fully addresses all coding requirements from issue #141: it adds assertTrackedRepo() guard to prevent unauthorized PAT access via /api/gt/repos routes and extends protections to related-prs and owner-comments routes previously flagged by reviewers.
Out of Scope Changes check ✅ Passed All changes are directly related to the allowlist security fix: new assertTrackedRepo helper, isTrackedRepoServer lookup function, and their integration into affected API routes. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/app/api/related-prs/[owner]/[name]/[number]/route.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

src/app/api/repos/[owner]/[name]/owner-comments/route.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

src/lib/assert-tracked-repo.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 1 others

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/lib/assert-tracked-repo.ts (1)

5-5: Update: getLiveReposAsyncServer uses in-memory throttling for upstream fetch, but still hits SQLite every call.

  • refreshLiveIfStale() caches the latest upstream snapshot in liveByLc/liveConfigJsonByLc, throttles remote fetches with REFRESH_MS (5 min) + lastAttemptAt backoff, and dedupes concurrent refreshes via inFlight—so no upstream network request per request.
  • getLiveReposAsyncServer() still calls buildList()readAll(), which runs SELECT full_name, weight, config_json FROM repo_weights on every invocation (then sorts), so there’s per-request DB overhead remaining.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/assert-tracked-repo.ts` at line 5, The current call to
getLiveReposAsyncServer triggers buildList/readAll (running SELECT on
repo_weights) on every request; instead use the in-memory cache maintained by
refreshLiveIfStale (liveByLc and liveConfigJsonByLc) or change
getLiveReposAsyncServer to return the cached snapshot to avoid per-request
SQLite hits. Update the code that currently does "const { repos } = await
getLiveReposAsyncServer()" so it reads from the cached structures maintained by
refreshLiveIfStale (or add a new accessor like getCachedLiveRepos that returns
liveByLc/liveConfigJsonByLc) and keep the existing
REFRESH_MS/lastAttemptAt/inFlight throttling logic intact to dedupe and backoff
remote refreshes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/lib/assert-tracked-repo.ts`:
- Line 5: The current call to getLiveReposAsyncServer triggers buildList/readAll
(running SELECT on repo_weights) on every request; instead use the in-memory
cache maintained by refreshLiveIfStale (liveByLc and liveConfigJsonByLc) or
change getLiveReposAsyncServer to return the cached snapshot to avoid
per-request SQLite hits. Update the code that currently does "const { repos } =
await getLiveReposAsyncServer()" so it reads from the cached structures
maintained by refreshLiveIfStale (or add a new accessor like getCachedLiveRepos
that returns liveByLc/liveConfigJsonByLc) and keep the existing
REFRESH_MS/lastAttemptAt/inFlight throttling logic intact to dedupe and backoff
remote refreshes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a3a5f085-dac9-4e69-a8b9-3ea3e8fd9fef

📥 Commits

Reviewing files that changed from the base of the PR and between 85ba452 and d743fa1.

📒 Files selected for processing (6)
  • src/app/api/gt/repos/[owner]/[name]/contents/route.ts
  • src/app/api/gt/repos/[owner]/[name]/contributing/route.ts
  • src/app/api/gt/repos/[owner]/[name]/health/route.ts
  • src/app/api/gt/repos/[owner]/[name]/readme/route.ts
  • src/app/api/gt/repos/[owner]/[name]/route.ts
  • src/lib/assert-tracked-repo.ts

@MkDev11
Copy link
Copy Markdown
Owner

MkDev11 commented May 24, 2026

This is a good start, but it does not fully fix the privileged-PAT access-control issue.

The allowlist is added to the five /api/gt/repos/[owner]/[name]/... routes, but other authenticated API routes still accept caller-controlled owner/name and call GitHub through withRotation() using the server PAT:

  • src/app/api/pull/[owner]/[name]/[number]/route.ts
  • src/app/api/issue/[owner]/[name]/[number]/route.ts
  • src/app/api/related-issues/[owner]/[name]/[number]/route.ts
  • src/app/api/issue/[owner]/[name]/[number]/timeline/route.ts

Those routes can still fetch private issue/PR bodies, timeline events, comments, commit metadata, etc. from arbitrary repos accessible to the PAT.

Please apply the same tracked-repo allowlist check to all caller-controlled GitHub proxy routes before any withRotation() call.

… routes

Apply assertTrackedRepo() to the four remaining PAT-proxying routes
missed in the initial fix:

- src/app/api/pull/[owner]/[name]/[number]/route.ts
- src/app/api/issue/[owner]/[name]/[number]/route.ts
- src/app/api/related-issues/[owner]/[name]/[number]/route.ts
- src/app/api/issue/[owner]/[name]/[number]/timeline/route.ts

Requested in PR MkDev11#142 review by MkDev11.
@web-dev0521
Copy link
Copy Markdown
Contributor Author

Just updated. Please check again.

@MkDev11
Copy link
Copy Markdown
Owner

MkDev11 commented May 25, 2026

Thanks for updating this. The previously flagged issue/pull detail and timeline routes are now covered, but the privileged-PAT allowlist fix is still incomplete.

Two caller-controlled routes can still trigger GitHub fetches for arbitrary untracked repos:

  • src/app/api/related-prs/[owner]/[name]/[number]/route.ts

    • Calls refreshIssueLinkedPrsIfStale(params.owner, params.name, issueNum) without assertTrackedRepo().
    • That helper calls GitHub through the server PAT via fetchIssueLinkedPrs() and can also hydrate missing PR metadata.
  • src/app/api/repos/[owner]/[name]/owner-comments/route.ts

    • Calls refreshCommentsIfStale(owner, name) without assertTrackedRepo().
    • That path calls issues.listCommentsForRepo through the server PAT.

Please add the tracked-repo allowlist check to these remaining caller-controlled routes before any helper that can reach GitHub. Until then, the root privileged-PAT access-control issue is not fully fixed, so Closes #141 is not accurate.

…okup

Two more caller-controlled routes reached the privileged PAT without an
allowlist check, flagged in PR MkDev11#142 review:

- related-prs/[owner]/[name]/[number]: refreshIssueLinkedPrsIfStale ->
  fetchIssueLinkedPrs -> withRotation
- repos/[owner]/[name]/owner-comments: refreshCommentsIfStale ->
  fetchIssueCommentsFromGithub -> withRotation

Audited all 22 [owner]/[name] API routes; these were the only two
remaining that transitively hit withRotation. The rest read solely from
the local DB / gittensor.io (refreshIssues/PullsIfStale are imported but
void-ed, not invoked).

Also address the CodeRabbit note about per-request SQLite hits: add
isTrackedRepoServer(), an O(1) lookup against the in-memory live snapshot
(liveByLc) with a DB fallback only on cold start, and have
assertTrackedRepo() use it instead of building+sorting the full repo list
on every gated request.
@web-dev0521
Copy link
Copy Markdown
Contributor Author

Thank you for your review, @MkDev11 , Could you please review this again?

@MkDev11 MkDev11 merged commit f79487a into MkDev11:main May 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants