fix(electron): the loopback must not report a sign-in it dropped - #205
Merged
Merged
Conversation
The return page POSTs the token back to the desktop loopback and reports
success from the HTTP status:
if (!r.ok) throw new Error('handoff rejected: ' + r.status);
h1.textContent = 'Signed in';
label.innerHTML = '<span class="ok">✓</span> Signed in';
dispatchAuthToken drops a token whose nonce does not match — that is the
drive-by-deep-link defence and it is right — but it returned void, and
the handler answered 204 regardless. So the browser told the user they
were signed in, with a checkmark and the button greyed out, while the app
had received nothing. The page's catch branch, the one that says to open
Cumora itself, was unreachable for this case.
Reaching it takes no attacker. armAuthHandoff "supersedes any previous
unused nonce", and AuthScreen re-arms on window focus: click sign-in, the
browser opens behind, click back into Cumora, click sign-in again, then
finish the FIRST tab. Its nonce is now stale.
Second half: consumeAuthNonce cleared the armed nonce before validating,
so that stale tab also disarmed the live one — the obvious recovery,
going back to finish the other tab, failed too. Clear only on a match, or
on a real expiry. The timing-safe compare and the length check are
unchanged, and a test pins them so this cannot quietly loosen the
comparison it guards.
The deep-link handler ignores the new return value on purpose: there is
nobody to answer there.
Three of the eight cases go red against the shipped file, and the
matchers self-test both ways — a source-scraping guard's failure mode is
becoming a silent no-op.
Merged
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.
The browser says "Signed in ✓" while the desktop app received nothing.
The return page hands the token to the loopback and reports success straight from the HTTP status:
dispatchAuthTokendrops a token whose nonce does not match — that is the drive-by-deep-link defence, and it is right — but it returnedvoid:and the handler answered 204 either way:
So the drop is silent in the one place the user is looking. The page's
catchbranch — the one that tells them to open Cumora themselves — is unreachable for exactly the case it was written for.No attacker needed
armAuthHandoff's own comment says it "supersedes any previous unused nonce", andAuthScreenre-arms on windowfocus. So:Token dropped, page says signed in, user closes the tab.
The recovery failed too
consumeAuthNoncecleared the armed nonce before validating it:So the stale tab's POST disarmed the live nonce on its way out. Going back and finishing the second tab — the obvious thing to try — then failed as well, with the same false success.
Now it clears only on a match, or on a genuine expiry. The timing-safe compare and the length pre-check are untouched, and a test pins them so this cannot quietly loosen the comparison it guards.
The change
dispatchAuthTokenreturns whether it accepted.catchand shows "Ready when you are" with the "Open Cumora" route.consumeAuthNonceconsumes on match.The
cumora://deep-link handler ignores the new return value deliberately — there is nobody to answer there.Verification
electron/has no runtime harness (main.cjsrequireselectron), so this is a source-level guard in the shape ofelectron-tray-unread-dot.test.ts. Eight cases; three go red against the shipped file:Both matchers self-test in both directions — reject the shipped shape, accept the fixed one — because a source-scraping check's failure mode is becoming a silent no-op. The anchors fail loudly if the functions move rather than passing vacuously.
One case is purely a guard on the change itself:
crypto.timingSafeEqualand the length check must still be how the nonce is compared.biome lint .clean; unit suite 1110 pass / 0 fail.I could not exercise a real OAuth round trip here, so the flow above is argued from the control flow and the page's own script rather than observed end to end.