Opened by detail-app[bot] · 6 days ago
Sign in to comment.
app.detail.dev/org_0f605249-55f3-46eb-880c-20ae3e45ae99/…/bug_996ffad3-126a-4586-b7d3-e368b5261106
Introduced in #61 by @ripgrim on Oct 23, 2026
listRepositories/userInstallationRepositories in packages/forge-github/src/client/installations.ts enumerate a GitHub App installation's repositories, and are the sole data source for the claim-verification in apps/worker/src/jobs/recover-installation.ts, whose granted-admin result is the DB-stored ownership proof that lets a user claim (link) an org installation.MAX_PAGES = 50 pages x PAGE_SIZE = 100 = 5000 repositories and, when total_count exceeds that, silently return a truncated list — body.total_count is already available but no shortfall check is done (no error, no log, no marker).installed list and a partial admin list that directly drive the adminOfEvery authorization decision; expected behavior is to detect found.length < total_count after the loop and fail loudly (or fail closed), never silently authorize against partial data.granted-admin, letting them claim/administer an org installation they do not fully control; symmetrically, a legitimate full admin can be wrongly denied.// packages/forge-github/src/client/installations.ts
const PAGE_SIZE = 100;
const MAX_PAGES = 50;
async listRepositories(installationId: string): Promise<GithubInstallationRepo[]> {
const token = await this.tokens.getToken(installationId);
const found: GithubInstallationRepo[] = [];
for (let page = 1; page <= MAX_PAGES; page++) {
// ... fetch page, push repos ...
if (
body.repositories.length < PAGE_SIZE ||
found.length >= body.total_count
) {
break;
}
// <-- BUG 🔴 when total_count > 5000 the loop exhausts MAX_PAGES,
// falls out and silently returns a truncated `found`
}
return found;
}
// apps/worker/src/jobs/recover-installation.ts
function adminOfEvery(
installed: GithubInstallationRepo[],
visible: UserInstallationRepo[],
): boolean {
const admin = new Set(
visible.filter((repo) => repo.admin).map((repo) => repo.externalId),
);
return installed.every((repo) => admin.has(repo.externalId));
}
// ...
return adminOfEvery(repositories, access.repositories) ? GRANTED : NOT_ADMIN;
// <-- BUG 🔴 `repositories` is possibly truncated; hidden repos are never checked
listRepositories and userInstallationRepositories stop after 50 pages regardless of body.total_count. When total_count exceeds 5000, the loop simply ends and returns whatever was accumulated, with no indication it is incomplete.
recover-installation uses these lists as the sole input to adminOfEvery. If the “installed repos” list is truncated, installed.every(...) can return true even when the user is not admin on repos that were omitted; if the “visible/admin” list is truncated, a true admin can be rejected.
packages/forge-github/src/client/reads.ts documents its pagination ceiling and notes that "evidence records the truncation", while installations.ts returns partial data silently even though its output feeds an authorization decision (granted-admin).
packages/forge-github/src/client/installations-pagination.test.ts (run and passing) demonstrates the issue with total_count: 6000:
adminOfEvery(truncatedInstalled, truncatedVisible) returns true when the user is only admin of repos 1..5000 (would grant granted-admin incorrectly)adminOfEvery returns false when a real admin’s user list is truncated (would deny incorrectly)admin on repos that appear within the first 5000 returned by /installation/repositories, but not on repos beyond that cutoff.listRepositories silently returns only 5000 repos; adminOfEvery returns true based on incomplete installed repo data; verify() persists granted-admin.granted-admin proof.After each pagination loop, compare fetched count to body.total_count and fail closed if found.length < total_count (throw or mark as truncated and ensure verify() refuses to authorize on truncated results). Apply the same to userInstallationRepositories, and add a test covering total_count > MAX_PAGES * PAGE_SIZE asserting truncation is detected.
This bug was introduced in commit 2e7c30f7 ("fix: only github admins can claim an installation"). The silent pagination cap pre-existed in 385e5989, but 2e7c30f7 made the truncated list part of the authorization path by adding adminOfEvery and introducing userInstallationRepositories with the same cap and no shortfall check.