75593a4e317eac40b781c83718961f0eb7f84f3a max Thu Aug 27 06:32:34 2026 -0700 Blue bar login and logout now return to the page they were clicked on #Preview2 week - bugs introduced now will need a build patch to fix The Login link in the blue bar sent everyone to the My Sessions page after they signed in, and the Sign out and account links in the logged-in dialog did the same, so a visitor reading a help page or looking at an item details page lost their place. Only hgTracks was handled, as a special case. Adds wikiLinkEncodeCurrentPageReturnUrl(), which builds a returnto for the page the CGI is currently serving, including its query string, since the track and item parameters of pages like hgTrackUi and hgc are not all kept in the cart. hgTracks keeps its old behaviour of returning to hgTracks?hgsid=, because its state is in the cart and its query string can hold a one-shot zoom or drag. hgMenubar does the same for the static pages it is included into, using the page path from the SSI environment. The two checks hgLogin runs on an incoming returnto (URL shape, and the optional login.approvedReturn host list) move to wikiLink.c so that the CGIs building a link apply the same rules before writing it. A URL hgLogin would refuse now becomes an ordinary login link instead of an error page: an over-long or oddly-quoted query string costs the query string, and a host that login.approvedReturn does not cover falls back to the old My Sessions target. Note that with login.approvedReturn set, static-page returns need the bare host added to the list. Also converts the fixed 2 kB buffers in the wikiLink URL builders to dyStrings, since a cgi-encoded return URL can nearly triple in length and safef would have aborted. refs #38192 diff --git src/hg/hgLogin/hgLogin.c src/hg/hgLogin/hgLogin.c index 54a2715a332..ec85fba34cb 100644 --- src/hg/hgLogin/hgLogin.c +++ src/hg/hgLogin/hgLogin.c @@ -325,108 +325,54 @@ return (count >= 1); } struct dyString *getLoginCookieJS(char *userName, uint idx) /* returns javascript statements that set the cookies associated with * logging in as a particular user */ { struct dyString *result = dyStringNew(1024); struct slName *newCookies = loginLoginUser(userName, idx), *sl; for (sl = newCookies; sl != NULL; sl = sl->next) dyStringPrintf(result, " document.cookie = '%s';", sl->name); return result; } -static boolean isValidReturnUrl(char *returnUrl) -/* Verify that returnUrl startswith an hg.conf approved set of hosts. */ -{ -struct slName *approvedHosts = slNameListFromComma(cfgOptionDefault(CFG_APPROVED_HOSTS, NULL)); -slAddHead(&approvedHosts, slNameNew(hLoginHostCgiBinUrl())); -if (approvedHosts) - { - struct slName *approvedStart; - for (approvedStart = approvedHosts; approvedStart != NULL; approvedStart = approvedStart->next) - { - if (startsWith(approvedStart->name, returnUrl)) - return TRUE; - } - } -return FALSE; -} - -static boolean returnUrlSchemeIsSafe(char *returnUrl) -/* Return TRUE unless returnUrl carries a scheme other than http or https. The scheme is the - * text before the first colon, and only when that colon comes before any slash, question mark - * or hash; a colon after one of those belongs to the path or the query, so the URL is relative. - * This is what keeps a javascript: or data: URL out of the href we write. */ -{ -char *colon = strchr(returnUrl, ':'); -if (colon == NULL) - return TRUE; -char *pathStart = strpbrk(returnUrl, "/?#"); -if (pathStart != NULL && pathStart < colon) - return TRUE; -int schemeLen = colon - returnUrl; -return (schemeLen == 4 && startsWithNoCase("http", returnUrl)) - || (schemeLen == 5 && startsWithNoCase("https", returnUrl)); -} - -static boolean returnUrlIsWellFormed(char *returnUrl) -/* Return TRUE if returnUrl looks like a URL we can write into the page. Every CGI parameter - * becomes a cart variable, so returnto holds whatever the visitor's URL said, and it is printed - * into an href attribute and into a javascript location assignment. A quote, an angle bracket, - * a backslash or a control character would end the attribute or the string literal and reflect - * script onto the page. A real URL percent-encodes all of those, so refusing them turns away - * nothing legitimate. */ -{ -char *c; -for (c = returnUrl; *c != 0; c++) - { - unsigned char uc = (unsigned char)*c; - if (uc < ' ' || uc == 127 || strchr("\"'<>\\`", *c) != NULL) - return FALSE; - } -return returnUrlSchemeIsSafe(returnUrl); -} - char *getReturnToURL() /* get URL from cart var returnto; if empty, make URL to hgSession on login host. */ { char *returnURL = cartUsualString(cart, "returnto", ""); -char returnTo[2048]; if (isEmpty(returnURL)) - safef(returnTo, sizeof(returnTo), "%shgSession?hgS_doMainPage=1", hLoginHostCgiBinUrl()); -else { + char returnTo[2048]; + safef(returnTo, sizeof(returnTo), "%shgSession?hgS_doMainPage=1", hLoginHostCgiBinUrl()); + return cloneString(returnTo); + } + /* Check the shape of the URL on every install. login.approvedReturn is optional, and * where it is set it only matches the front of the URL, so the rest of the URL is - * unchecked either way. */ - boolean ok = returnUrlIsWellFormed(returnURL); - if (ok && cfgOptionDefault(CFG_APPROVED_HOSTS, NULL)) - ok = isValidReturnUrl(returnURL); - if (!ok) + * unchecked either way. The check lives in wikiLink.c so that the CGIs building a + * returnto can apply the same rules before they write the link. */ +if (!loginReturnUrlIsAcceptable(returnURL)) { hDumpStackDisallow(); errAbort("Error: Invalid returnto URL. Please send email to genome-www@soe.ucsc.edu " "with the returnto argument from the URL (or just the full URL) so we can " "fix this."); } - safecpy(returnTo, sizeof(returnTo), returnURL); - } -return cloneString(returnTo); +return cloneString(returnURL); } static char *getReturnToUrlForAttr() /* getReturnToURL() escaped for printing inside an href="" attribute. Escaping the ampersand * is the part that matters here: the browser expands an entity in an attribute value, so * javascript:alert(1) would otherwise become a javascript: URL after the checks above * have passed it. */ { return htmlEncode(getReturnToURL()); } void returnToURL(int delay) /* delay for delay mill-seconds then return to the "returnto" URL */ { char *returnURL = javaScriptLiteralEncode(getReturnToURL());