Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit6b75ac1

fix(security): assertRepoReadable fails CLOSED on DB error

fix(security): assertRepoReadable fails CLOSED on DB error

Follow-up to 5bb52fa. The access gate previously returned null (allow) when
the repo-privacy lookup threw, which would re-expose private repos during a DB
hiccup — the exact hole the gate closes. Now it returns 404 (deny) on error.

To keep the DB-less test harness working without inferring allow from an
exception, add an explicit positive check: when DATABASE_URL is unset (dev/test,
no persisted repos exist) the gate is skipped up front. In production
DATABASE_URL is always set, so any lookup error now fails closed.

Flagged by automated security review (Authorization Fail-Open on Error).
web-routes.test.ts: 13/13 pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ccanty labs committed on July 7, 2026Parent: 5bb52fa
1 file changed+15116b75ac1c90c2e2a0c7ab076b64e96fad0dc5bb85
1 changed file+15−11
Modifiedsrc/routes/web.tsx+15−11View fileUnifiedSplit
121121 404
122122 );
123123
124 // Explicit dev/test bypass: when no database is configured at all there are
125 // no persisted repositories to protect (private repos only exist as DB rows),
126 // and the browse routes render purely from git-on-disk. This is a positively
127 // established condition (DATABASE_URL unset), NOT an error inference — so the
128 // access gate below can safely fail CLOSED on any runtime error. In
129 // production DATABASE_URL is always set, so this branch never runs there.
130 if (!config.databaseUrl) return null;
131
124132 try {
125133 const [ownerRow] = await db
126134 .select({ id: users.id })
149157
150158 return null;
151159 } catch (err) {
152 // The DB is unreachable — we cannot read the repo's privacy flag, so we
153 // cannot positively enforce the gate here. We fail OPEN rather than 404
154 // every repo (including public ones) during a DB outage: a private repo
155 // only exists as a DB row in the first place, and when the DB is down the
156 // platform is non-functional across the board. This mirrors the codebase's
157 // behavior when the DB is absent (e.g. the test harness runs the browse
158 // routes with no DATABASE_URL). It does NOT weaken the fix for the actual
159 // vulnerability, which is anonymous browsing of private repos while the DB
160 // is up — that path reads the row and denies above.
160 // Fail CLOSED. A DB is configured (checked above) but the privacy-flag
161 // lookup errored, so we cannot positively establish that this repo is
162 // public. Denying is the only safe choice for an access-control gate —
163 // failing open here would re-expose private repos during a DB hiccup,
164 // which is the exact vulnerability this function exists to close.
161165 console.warn(
162 `[web] assertRepoReadable: access check degraded for ${owner}/${repo}:`,
166 `[web] assertRepoReadable: access check failed for ${owner}/${repo} — denying:`,
163167 err instanceof Error ? err.message : err
164168 );
165 return null;
169 return notFound();
166170 }
167171}
168172
169173