From 4a96d42cdb9b8ff44dd686d42be5e03aa5b148f3 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:04:25 -0700 Subject: [PATCH] docs(db): guardrail listInstallations/listRepositories against tenant-facing misuse (#4797) Audited every query touching installations, repositories, and repository_settings for query-layer tenant-scoping. Every single-row lookup (getRepositorySettings, getRepository, getInstallation, listInstalledRepoFullNamesForInstallation) is already correctly scoped by an explicit parameter at the SQL WHERE clause. listInstallations/listRepositories have no WHERE clause at all -- every row, every call. Traced all 30+ call sites: every one is internal cross-repo maintenance/sweep machinery or an admin/maintainer/owner- role-gated dashboard route (verified via canSessionAccessPath/ requireAppRole). No live leak exists today, but the isolation is only true because every current caller happens to be admin-gated, not because the query itself can't return cross-tenant data -- exactly the class of risk the issue's problem statement calls out. Added doc-comment guardrails pointing future callers at the correctly-scoped alternatives, so a future Rent-a-Loop customer endpoint reaching for these by habit doesn't leak every tenant's repos/installations. --- src/db/repositories.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 6f59a14d33..a95f404cb4 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -301,6 +301,12 @@ export async function updateInstallationPermissions(env: Env, installationId: nu await db.update(installations).set({ permissionsJson: jsonString(permissions), updatedAt: nowIso() }).where(eq(installations.id, installationId)); } +/** #4797: NOT tenant-scoped at the query layer -- returns every installation with no filter. Every current + * caller is either internal cross-repo maintenance (backfill/sweep machinery, which legitimately needs the + * fleet-wide view) or an admin/maintainer/owner-role-gated dashboard route (verified via `canSessionAccessPath` + * / `requireAppRole` at each call site, src/api/routes.ts). Do NOT call this from any customer/tenant-facing + * path -- use {@link getInstallation} (single, id-scoped) instead. A future Rent-a-Loop customer endpoint that + * reaches for this function by habit would leak every other tenant's installations. */ export async function listInstallations(env: Env): Promise { const db = getDb(env.DB); const rows = await db.select().from(installations).orderBy(desc(installations.updatedAt)).limit(100); @@ -537,6 +543,12 @@ export async function getRepository(env: Env, fullName: string): Promise { const db = getDb(env.DB); const rows = await db.select().from(repositories).orderBy(desc(repositories.isRegistered), repositories.fullName);