fix(security): close Telegram-callback link CSRF and gate restricted supporters - #64
Merged
matthew-demidoff merged 2 commits intoAug 20, 2026
Merged
Conversation
…supporters Two independently-reported issues on privileged/auth paths: - /api/telegram/callback auto-linked a Telegram identity to the logged-in session user from a bare GET. A cross-site GET carries the victim's SameSite=Lax cookie, so an attacker could bind their own Telegram id onto an unlinked victim, then replay the payload with no session to log in as the victim. Linking now goes only through the /relink bot-approval flow; the callback redirects a signed-in hitter there and never links. The login branch additionally single-uses the signed payload so a captured one cannot be replayed within its freshness window. Removed the now-unused linkTelegram primitive so the vulnerable path cannot be reintroduced. - The security-review server actions (restrict/unrestrict, dismiss suspicion, security replies) gated only on support-team role, and server actions run before the (app) layout's restricted redirect, so a restricted security supporter could still drive the restriction console. Each action now calls assertNotRestricted right after resolving the session.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes two independently-reported High findings on privileged/auth paths, both verified against the code.
Telegram callback CSRF -> account takeover
GET /api/telegram/callbackauto-linked a Telegram identity to the logged-in session user from a bare GET. A cross-site GET carries the victim's SameSite=Lax session cookie, so an attacker could bind their own Telegram id onto an unlinked victim, then replay the payload with no session to log in as the victim./relinkbot-approval flow, which binds the action to an interactive Approve in the user's own Telegram. The callback redirects a signed-in hitter there and never links.SET NX) so a captured payload cannot be replayed within its freshness window.linkTelegramprimitive so the vulnerable path cannot be reintroduced. The ban gate on the login branch is unchanged.Restricted supporters retained privileged access
The
security-reviewserver actions gated only on support-team role, and server actions run before the(app)layout's restricted redirect, so a restrictedsecurity/security_highsupporter could still restrict/unrestrict users, dismiss suspicion events, and post security replies. Each of the five actions now callsassertNotRestrictedright after resolving the session, matching the pattern the actions had skipped.Verification
typecheck clean; full suite green (241 passing) including new regressions: callback single-use replay rejection, signed-in -> /relink with no link, and restricted-supporter bounce on the privileged actions. Adversarially reviewed: restricted-supporter fix confirmed complete with no adjacent ungated surfaces (support actions already gated, admin actions gated by step-up).
Known latent gap (not introduced here, out of scope)
Removing the auto-link branch leaves no first-time Telegram-link path:
/relinkgates on an existingtelegram_id. This affects nothing today because every account is created with Telegram linked (both registration paths require it) and there is no unlink feature, so nonull-telegram account exists. If OAuth-only signup or an unlink feature is ever added, a CSRF-safe first-time-link flow must be built alongside it.