Make magic-link verify safe against JS-executing mail scanners#15
Draft
tnsardesai wants to merge 1 commit into
Draft
Make magic-link verify safe against JS-executing mail scanners#15tnsardesai wants to merge 1 commit into
tnsardesai wants to merge 1 commit into
Conversation
The invisible auto-submit verify shim assumed link scanners only issue GETs, but a JS-executing mail scanner opens the emailed URL and runs the auto-submit <script> itself, consuming the single-use token ~10s after send — before the human taps. Mobile Safari users lose this race the majority of the time (their tap latency exceeds the scanner window) and get "expired or used" on a link they never used. Gate the invisible auto-submit on a short-lived login-intent cookie set when the link is requested (POST /login). The browser that requested the link keeps the invisible sign-in; every other opener — a scanner, a share/comment link, or a link opened in a different browser — gets a no-JS confirm button, so only a genuine tap consumes the token. Also add Cache-Control: no-store to the token-bearing verify pages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
Problem
Opening a
/login/verifymagic link on mobile Safari frequently shows "LINK EXPIRED OR USED" on a link the human never used.Root cause: the invisible auto-submit verify shim (the "GET confirms, POST consumes" design) assumed link scanners only issue GETs. In production a JavaScript-executing mail link scanner opens the emailed URL and runs the page's auto-submit
<script>itself, issuing the consuming POST ~10s after the email is sent — before the human taps. Whoever POSTs first wins the single-use token; the token lands in the scanner's cookie jar (useless to the human), who then gets the 410.Evidence from
audit_log(session.created= token consumed):login_link.requestedUA is the human's device (e.g.iPhone … AppleWebKit/605.1.15); the matchingsession.createdcomes from a headlessX11; Linux x86_64 … Chrome/150.Fix
Gate the invisible auto-submit on a short-lived login-intent cookie set when the link is requested (
POST /login, scoped to/login, 15-min TTL):confirmSignInPage) with no auto-submit script. Only a genuine tap consumes the token, so a JS scanner can render the page but can't burn the link.The POST consume path is unchanged (still atomic, single-use). Also adds
Cache-Control: no-storeto the token-bearing verify GET responses.Trade-off
Share/comment-link recipients and cross-browser opens now tap one button instead of auto-submitting — the only cases where invisible sign-in was never safe anyway. The common self-serve login (request + open in the same browser) stays zero-tap.
Tests
lib/auth/verify-pages.test.ts: addedconfirmSignInPagecoverage (POST form, hidden token/next, visible button, no<script>, HTML-escaping).tsc --noEmitclean.scripts/e2e.ts) share-link flow still valid: a share link has no intent cookie, so it renders the confirm page (200) and the POST consume is unchanged.Fixes KERNEL-1734.