What
A result computed with a caller-supplied PAT is written to a cache slot that has no auth component in its key, and every public route then reads that same slot anonymously. So a private-repo answer — project path, issue/PR title, release tag — can be served to someone who has no access to that repo.
Mechanism
cacheKey() is called with five parts and nothing else:
('res', '<host>/<projectPath>', 'issue#N' | 'pr#N' | 'sha:X', 'cull'|'strict', 'nopre'|'pre')
Every one of these callsites builds it identically:
packages/web/src/routes/lookup.ts:71 — /api/lookup, computes with makeProvider(env, req, host)
packages/web/src/routes/issue.tsx:101 — public permalink page
packages/web/src/routes/pr.tsx:86 — public permalink page
packages/web/src/routes/result.tsx:79 — public permalink page
packages/web/src/routes/badge.ts:110 — public badge
makeProvider → resolveProviderToken (auth.ts:13,29) honours the X-User-Github-Token / X-User-Gitlab-Token request headers in preference to the server secret. checkSameOrigin (auth.ts:56) returns true when there is no Origin header, so plain curl reaches /api/lookup.
The token therefore changes what is computed but not where it is stored.
Reproduction shape
- A user (legitimately, this is the documented private-repo path) posts their own PAT:
curl -H 'X-User-Github-Token: <pat>' -d '{"input":"acme/private#42"}' https://released.blabberate.com/api/lookup
The result is written to the shared slot for 30 minutes (or 30 days once resolve.ts policy applies).
- Anyone who guesses or knows the repo path + number opens
https://released.blabberate.com/i/acme/private/42 — a cache hit, rendered as a full public card with the issue title.
No PAT is leaked, but the content it unlocked is.
Scope — this is pre-existing on main, not introduced by #144
Worth being precise, because it was raised as a #144 finding. The review's claim was that #144 "widens it to the full card" because it aligns /internal's key with the public one, adding the OG PNG as a reader.
That part is not right: issue.tsx on main already uses the byte-identical 5-part key (git show origin/main:packages/web/src/routes/issue.tsx) and already renders the full card, including the title, on a public unauthenticated URL. pr.tsx and result.tsx likewise. So the full-card exposure exists today with no OG involvement at all; #144 adds one more rendering of a slot that is already publicly readable, and widens nothing.
That makes this a defect in code #144 does not touch, which is why it is filed here rather than folded into that PR.
Options
- Namespace user-PAT lookups. Add a key part when the token came from a request header — e.g. a 6th part that is
anon for the server-token path and a digest of the PAT otherwise. Cheap, keeps the anonymous slot shared and hot, and each PAT holder gets their own slot.
- Do not cache PAT-computed results at all. Simplest and safest; costs a recompute per private lookup, which is rare traffic.
- Accept it, and say so. Document on
/how-it-works that a lookup made with your own token populates a public cache. This is the only option that needs a product call rather than a code change.
Option 1 or 2 look right; the choice between them is a taste call about whether private lookups deserve caching at all.
Not urgent, but not nothing
Exploiting it needs an attacker to know the exact private repo path and issue/PR number, and needs a PAT holder to have looked it up first. There is no evidence in the error history of it happening. But it is a confidentiality bug with a small, well-understood fix.
Found by the round-18 automated review on #144 (thread PRRT_kwDOSe0Tzs6dTung); premise re-verified against origin/main before filing.
What
A result computed with a caller-supplied PAT is written to a cache slot that has no auth component in its key, and every public route then reads that same slot anonymously. So a private-repo answer — project path, issue/PR title, release tag — can be served to someone who has no access to that repo.
Mechanism
cacheKey()is called with five parts and nothing else:Every one of these callsites builds it identically:
packages/web/src/routes/lookup.ts:71—/api/lookup, computes withmakeProvider(env, req, host)packages/web/src/routes/issue.tsx:101— public permalink pagepackages/web/src/routes/pr.tsx:86— public permalink pagepackages/web/src/routes/result.tsx:79— public permalink pagepackages/web/src/routes/badge.ts:110— public badgemakeProvider→resolveProviderToken(auth.ts:13,29) honours theX-User-Github-Token/X-User-Gitlab-Tokenrequest headers in preference to the server secret.checkSameOrigin(auth.ts:56) returnstruewhen there is noOriginheader, so plaincurlreaches/api/lookup.The token therefore changes what is computed but not where it is stored.
Reproduction shape
curl -H 'X-User-Github-Token: <pat>' -d '{"input":"acme/private#42"}' https://released.blabberate.com/api/lookupThe result is written to the shared slot for 30 minutes (or 30 days once
resolve.tspolicy applies).https://released.blabberate.com/i/acme/private/42— a cache hit, rendered as a full public card with the issue title.No PAT is leaked, but the content it unlocked is.
Scope — this is pre-existing on
main, not introduced by #144Worth being precise, because it was raised as a #144 finding. The review's claim was that #144 "widens it to the full card" because it aligns
/internal's key with the public one, adding the OG PNG as a reader.That part is not right:
issue.tsxonmainalready uses the byte-identical 5-part key (git show origin/main:packages/web/src/routes/issue.tsx) and already renders the full card, including the title, on a public unauthenticated URL.pr.tsxandresult.tsxlikewise. So the full-card exposure exists today with no OG involvement at all; #144 adds one more rendering of a slot that is already publicly readable, and widens nothing.That makes this a defect in code #144 does not touch, which is why it is filed here rather than folded into that PR.
Options
anonfor the server-token path and a digest of the PAT otherwise. Cheap, keeps the anonymous slot shared and hot, and each PAT holder gets their own slot./how-it-worksthat a lookup made with your own token populates a public cache. This is the only option that needs a product call rather than a code change.Option 1 or 2 look right; the choice between them is a taste call about whether private lookups deserve caching at all.
Not urgent, but not nothing
Exploiting it needs an attacker to know the exact private repo path and issue/PR number, and needs a PAT holder to have looked it up first. There is no evidence in the error history of it happening. But it is a confidentiality bug with a small, well-understood fix.
Found by the round-18 automated review on #144 (thread
PRRT_kwDOSe0Tzs6dTung); premise re-verified againstorigin/mainbefore filing.