e3d5f0b1474ba392a87b2f65dc9701db3e88bfa2 max Sat Sep 26 21:54:08 2026 -0700 hgLogin: require POST and a per-page token to change the pending-signup address diff --git src/hg/hgLogin/hgLogin.c src/hg/hgLogin/hgLogin.c index 431b4643002..119471da2fe 100644 --- src/hg/hgLogin/hgLogin.c +++ src/hg/hgLogin/hgLogin.c @@ -461,31 +461,36 @@ /* A rejected address (changePendingEmail) leaves its reason here, and this page is where the * user sees it -- nothing else prints it on the way through. */ if (isNotEmpty(errMsg)) hPrintf("
%s
", errMsg); hPrintf( "A confirmation email has been sent to you. \n" "Please click the confirmation link in the email to activate your account.
" "You may have to look in your spam folder for an email from genome-www@soe.ucsc.edu, " "especially if you use Microsoft Outlook or Hotmail.
"); if (offerNewAddress) { /* If that address is wrong the confirmation can never arrive, and this account has no other * way in, so offer to replace it here. */ hPrintf("If %s is not the right email address, enter the right one, and we will " "send the confirmation there instead.
", encAddress); + /* One token per render of this form, checked back by changePendingEmail: a link built + * without having seen this page cannot carry it. */ + char *formToken = makeRandomKey(128+33); + cartSetString(cart, "oauth_pending_formToken", formToken); hPrintf(""); } hPrintf("\n", returnURL); freeMem(encProvider); freeMem(encAddress); cartRemove(cart, "hgLogin_email"); cartRemove(cart, "hgLogin_userName"); cartRemove(cart, "hgLogin_actMailProvider"); cartRemove(cart, "hgLogin_actMailTo"); @@ -3050,33 +3055,39 @@ gbMembersFreeList(&matches); setPendingIdentity(id); completeAccountPage(conn); } void changePendingEmail(struct sqlConnection *conn) /* Put a different address on the unactivated account behind a still-valid pending identity, and * send the confirmation there. Reached only from the confirmation page that resolveIdentity * shows such an account (see displayActMailSuccess), and the only way out for someone who * mistyped their address when the account was made: with no password and an address they cannot * read, every other route back in wants an activated account or a login cookie. * The pending signature is the authorization. Only resolveIdentity mints one, only after a real * provider round trip, and only for this browser, so a request arriving here without one is * refused rather than trusted. */ { -char *provider = cartUsualString(cart, "oauth_pending_provider", ""); +// Clone this: clearPendingIdentity below frees the cart's copy, but we still use provider +// after calling it, to label the confirmation page. +char *provider = cloneString(cartUsualString(cart, "oauth_pending_provider", "")); char *subject = cartUsualString(cart, "oauth_pending_subject", ""); -if (isEmpty(provider) || isEmpty(subject) || !pendingIdentityValid()) +char *formToken = cartUsualString(cart, "oauth_pending_formToken", ""); +boolean postedRightToken = sameString(emptyForNull(cgiRequestMethod()), "POST") && + isNotEmpty(formToken) && sameString(formToken, cgiUsualString("pendingFormToken", "")); +cartRemove(cart, "oauth_pending_formToken"); // one-time use either way +if (isEmpty(provider) || isEmpty(subject) || !pendingIdentityValid() || !postedRightToken) { clearPendingIdentity(); freez(&errMsg); errMsg = cloneString("Your sign-in expired. Please sign in again."); displayLoginPage(conn); return; } struct oauthIdentity id; ZeroVar(&id); id.provider = provider; id.subject = subject; struct gbMembers *m = memberForIdentity(conn, &id); /* Only an account that is still waiting to be confirmed. Once it is activated this page is * not reachable any more, and changing the address of a working account belongs in the * change-email flow, which confirms the new address before it takes effect. */ @@ -3100,30 +3111,32 @@ freeMem(addrMatch); bad = (sqlQuickNum(conn, query) > 0); } if (!bad) { char query[512]; sqlSafef(query, sizeof(query), "UPDATE gbMembers SET email='%s', emailToken='', emailTokenExpires='' WHERE idx=%u", email, m->idx); sqlUpdate(conn, query); setupNewAccount(conn, email, m->userName); // new address, so a new token is right cartSetString(cart, "hgLogin_actMailTo", email); /* The page we are about to show is the same one the user just came from, so say that the * change went through. Otherwise the swapped-in address is the only sign of it. */ cartSetString(cart, "hgLogin_actMailChanged", "1"); + // One change per pending identity: clear it so the form this came from cannot be reused. + clearPendingIdentity(); } else { freez(&errMsg); errMsg = cloneString("Please enter an email address that is not already in use."); cartSetString(cart, "hgLogin_actMailTo", m->email); cartRemove(cart, "hgLogin_actMailChanged"); } cartSetString(cart, "hgLogin_actMailProvider", oauthProviderLabel(provider)); cartSetString(cart, "hgLogin_actMailUser", m->userName); cartRemove(cart, "hgLogin_email"); gbMembersFree(&m); displayActMailSuccess(); } @@ -3374,30 +3387,31 @@ * takes CGI variables verbatim (loadCgiOverHash in hg/lib/cart.c), so a copy arriving with * the request would stand in for the copy we stored. Drop those before anything reads them; * a flow whose state is dropped fails closed and the user starts it again. Note that * excludeVars would not do this job: it governs what is saved at the end of a request, not * what is read during it. */ { static char *serverOwned[] = { "oauth_state", "oauth_provider", "oauth_pending_provider", "oauth_pending_subject", "oauth_pending_email", "oauth_pending_email_unverified", "oauth_pending_email_verified", "oauth_pending_name", "oauth_pending_time", "oauth_pending_sig", "emailLogin_email", "emailLogin_tokenMd5", "hgLogin_actMailProvider", "hgLogin_actMailTo", "hgLogin_actMailUser", "hgLogin_actMailUnverified", "hgLogin_actMailChanged", + "oauth_pending_formToken", }; int i; for (i = 0; i < ArraySize(serverOwned); i++) if (cgiVarExists(serverOwned[i])) cartRemove(cart, serverOwned[i]); } void doMiddle(struct cart *theCart) /* Write the middle parts of the HTML page. * This routine sets up some globals and then * dispatches to the appropriate page-maker. */ { struct sqlConnection *conn = hConnectCentral(); // on mirrors, try to add the field 'recovEmail' to gbMembers. This may or may not work, depending on their config