d338d5080783d2ac5828658fe17160668bf64cdb chmalee Thu May 7 15:05:24 2026 -0700 Fixes from code review, refs #37500 - Revalidate shared myVariants tracks against hgcentral on every read path (hgTracks, hgc, hgTables); cart-supplied owner/db/project no longer trusted. New myVariantsResolveSharedTrack helper. - Scope shared-track UPDATE statements by share->project/db so a recipient can't edit rows outside the granted scope. - Add hgsid CSRF check to myVariantsJsCommand; pass hgsid in the hgTracks.js highlight Add-Annotation POST. - HTML-escape owner-controlled fields in the canEdit branch of doMyVariantsDetails (Chromosome, Project, project select options, hidden text input). - Validate targetUser against gbMembers when creating a share; return a clear 400 on typos. - Replace the concat(id,' ',name)='%s' lookup with parsed-id + name verification. - Remove cgiMakeColorVar / cgiMakeColorVarWithLabel; the canEdit form uses spectrum.js (already loaded for the create dialog). - Strip _hidden_* columns from hgTables field lists for shared tracks, both the display path and the selected-fields read path. - Make the per-assembly invariant explicit: myVariantsLoadItems and doMyVariantsDetails bail out if share->db != current database. - Memoize myVariantsSharedScopeWhere to avoid per-region hgcentral round-trips on genome-wide hgTables queries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> diff --git src/hg/hgTracks/myVariantsTrack.c src/hg/hgTracks/myVariantsTrack.c index 2ffee273a38..31cf394a06a 100644 --- src/hg/hgTracks/myVariantsTrack.c +++ src/hg/hgTracks/myVariantsTrack.c @@ -1,901 +1,907 @@ /* myVariantsTrack.c - supports tracks of type myVariants. Users can drag to create an item * and click to edit one. */ /* Copyright (C) 2013 The Regents of the University of California * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */ #include "common.h" #include "hash.h" #include "linefile.h" #include "jksql.h" #include "hdb.h" #include "hgTracks.h" #include "bed.h" #include "binRange.h" #include "myVariants.h" #include "myVariantsShare.h" #include "jsonParse.h" #include "sqlNum.h" #include "customFactory.h" #include "hgConfig.h" #include "htmlColor.h" #include "wikiLink.h" #include "hgFind.h" #include "jsonWrite.h" static char *reservedFieldNames[] = { "bin", "chrom", "chromStart", "chromEnd", "name", "score", "strand", "thickStart", "thickEnd", "itemRgb", "description", "db", "ref", "alt", "project", "mouseover", "id", NULL }; static boolean isValidFieldName(char *name) /* Validate that name matches [a-zA-Z_][a-zA-Z0-9_]*, is not a reserved column name, * and does not start with _hidden_ */ { if (isEmpty(name)) return FALSE; if (startsWith("_hidden_", name)) return FALSE; /* Check first char is letter or underscore */ if (!isalpha(name[0]) && name[0] != '_') return FALSE; /* Check remaining chars are alphanumeric or underscore */ int i; for (i = 1; name[i] != '\0'; i++) { if (!isalnum(name[i]) && name[i] != '_') return FALSE; } /* Check against reserved names */ int j; for (j = 0; reservedFieldNames[j] != NULL; j++) { if (sameString(name, reservedFieldNames[j])) return FALSE; } return TRUE; } void myVariantsJsCommand(char *command, struct track *trackList, struct hash *trackHash) /* Execute some command sent to us from the javaScript. All we know for sure is that * the first word of the command is "myVariants." We expect it to be of format: * myVariants <trackName> <jsonData> * where jsonData is a JSON string containing the item details */ { if (!cfgOptionBooleanDefault("doMyVariants", FALSE)) return; char *userName = getUserName(); if (userName == NULL) return; +/* This command runs mysql commands so require the hgsids match to prevent CSRF. */ +char *suppliedHgsid = cgiOptionalString("hgsid"); +char *expectedHgsid = cartSessionId(cart); +if (isEmpty(suppliedHgsid) || isEmpty(expectedHgsid) + || !sameString(suppliedHgsid, expectedHgsid)) + errAbort("session token missing or invalid"); + /* Parse out command into local variables. */ char *words[3]; char *dupeCommand = cloneString(command); /* For parsing. */ int wordCount = chopByWhiteRespectDoubleQuotes(dupeCommand, words, ArraySize(words)); if (wordCount != 3) errAbort("Expecting %d words in jsCommand '%s'", wordCount, command); char *jsonData = words[2]; /* Parse JSON data */ struct jsonElement *json = jsonParse(jsonData); if (json == NULL) errAbort("Invalid JSON data in command: %s", jsonData); /* Handle hideField command: rename column with _hidden_ prefix */ char *hideFieldName = jsonOptionalStringField(json, "hideField", NULL); if (isNotEmpty(hideFieldName)) { if (isValidFieldName(hideFieldName)) { char *tableName = myVariantsTableExists(userName); if (tableName) { struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH); if (sqlFieldIndex(conn, tableName, hideFieldName) >= 0) { struct dyString *alterSql = sqlDyStringCreate( "ALTER TABLE %s CHANGE COLUMN %s _hidden_%s varchar(255) DEFAULT NULL", tableName, hideFieldName, hideFieldName); sqlUpdate(conn, dyStringContents(alterSql)); dyStringFree(&alterSql); } hFreeConn(&conn); } } freez(&dupeCommand); return; } /* Create a new item based on command. */ struct myVariants *item; AllocVar(item); /* Extract values from JSON */ struct jsonElement *hgvsJson = jsonFindNamedField(json, "jsonData", "hgvsInput"); if (hgvsJson) { char *hgvs = jsonStringVal(hgvsJson, "hgvsInput"); if (hgvs) { struct hgPositions *hgp = NULL; AllocVar(hgp); hgp->query = cloneString(hgvs); hgp->database = database; hgp->useAlias = FALSE; if (matchesHgvs(cart, database, hgvs, hgp, measureTiming)) { fixSinglePos(hgp); if (hgp->singlePos) { // fill out an item AND redirect to that location somehow? item->chrom = hgp->singlePos->chrom; item->chromStart = item->thickStart = hgp->singlePos->chromStart; item->chromEnd = item->thickEnd = hgp->singlePos->chromEnd; item->strand[0] = '.'; item->score = 0; item->bin = binFromRange(item->chromStart, item->chromEnd); item->name = hgp->singlePos->name; item->ref = cloneString(""); item->alt = cloneString(""); item->db = database; item->description = cloneString(hgp->singlePos->description); item->project = cloneString(""); item->mouseover = cloneString(""); } } else { /* HGVS didn't match - try hgFind for position/gene resolution */ struct hgPositions *hgpFind = hgPositionsFind(database, hgvs, "", "myVariants", cart, FALSE, FALSE, NULL); if (hgpFind != NULL && hgpFind->posCount == 1) { struct hgPos *pos = hgpFind->singlePos; item->chrom = cloneString(pos->chrom); item->chromStart = item->thickStart = pos->chromStart; item->chromEnd = item->thickEnd = pos->chromEnd; item->strand[0] = '.'; item->score = 0; item->bin = binFromRange(item->chromStart, item->chromEnd); item->name = cloneString(pos->name ? pos->name : hgvs); item->ref = cloneString(""); item->alt = cloneString(""); item->db = database; item->description = cloneString(pos->description ? pos->description : ""); item->project = cloneString(""); item->mouseover = cloneString(""); } else if (hgpFind != NULL && hgpFind->posCount > 1) { warn("Position '%s' matches %d locations - please be more specific", hgvs, hgpFind->posCount); return; } else { warn("Position '%s' not found", hgvs); return; } } } } else { char *chrom = jsonStringVal(jsonFindNamedField(json, "jsonData", "chrom"), "chrom"); int chromStart = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "start"), "start")); int chromEnd = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "end"), "end")); char *name = jsonStringVal(jsonFindNamedField(json, "jsonData", "name"), "name"); int score = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "score"), "score")); char *strand = jsonStringVal(jsonFindNamedField(json, "jsonData", "strand"), "strand"); int thickStart = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "thickStart"), "thickStart")); int thickEnd = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "thickEnd"), "thickEnd")); char *colorCode = jsonStringVal(jsonFindNamedField(json, "jsonData", "color"), "color"); char *description = jsonStringVal(jsonFindNamedField(json, "jsonData", "description"), "description"); char *ref = jsonOptionalStringField(json, "ref", ""); char *alt = jsonOptionalStringField(json, "alt", ""); char *project = jsonOptionalStringField(json, "project", ""); char *mouseover = jsonOptionalStringField(json, "mouseover", ""); unsigned color; htmlColorForCode(colorCode, &color); item->bin = binFromRange(chromStart, chromEnd); item->chrom = cloneString(chrom); item->chromStart = chromStart; item->chromEnd = chromEnd; item->thickStart = thickStart; item->thickEnd = thickEnd; item->name = cloneString(name); item->score = score; item->strand[0] = strand[0]; item->itemRgb = color; item->description = cloneString(description); item->ref = cloneString(ref); item->alt = cloneString(alt); item->db = database; item->project = cloneString(project); item->mouseover = cloneString(mouseover); } if (!item) return; /* Parse custom fields from JSON payload and ALTER TABLE as needed */ char *tableName = myVariantsCreateTable(userName); struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH); struct jsonElement *extraFieldsJson = jsonFindNamedField(json, "jsonData", "extraFields"); if (extraFieldsJson && extraFieldsJson->type == jsonList) { struct slRef *el; for (el = extraFieldsJson->val.jeList; el != NULL; el = el->next) { struct jsonElement *cfObj = el->val; char *cfName = jsonOptionalStringField(cfObj, "name", NULL); char *cfValue = jsonOptionalStringField(cfObj, "value", ""); if (isEmpty(cfName) || !isValidFieldName(cfName)) continue; /* Add column if it doesn't already exist; un-hide if hidden */ if (sqlFieldIndex(conn, tableName, cfName) < 0) { char hiddenName[256]; safef(hiddenName, sizeof(hiddenName), "_hidden_%s", cfName); if (sqlFieldIndex(conn, tableName, hiddenName) >= 0) { /* Un-hide: rename _hidden_X back to X */ struct dyString *alterSql = sqlDyStringCreate( "ALTER TABLE %s CHANGE COLUMN %s %s varchar(255) DEFAULT NULL", tableName, hiddenName, cfName); sqlUpdate(conn, dyStringContents(alterSql)); dyStringFree(&alterSql); } else { struct dyString *alterSql = sqlDyStringCreate( "ALTER TABLE %s ADD COLUMN %s varchar(255) DEFAULT NULL", tableName, cfName); sqlUpdate(conn, dyStringContents(alterSql)); dyStringFree(&alterSql); } } slPairAdd(&item->customFields, cfName, cloneString(cfValue)); } slReverse(&item->customFields); } /* Add item to database. */ myVariantsSaveToDb(conn, item, tableName, 0); /* Trigger CT import by writing a concrete CT file to trash and pointing ctfile_<db> to it */ char *ctFile = myVariantsWriteCtFile(userName, database, cart); if (isNotEmpty(ctFile)) { char varName[256]; safef(varName, sizeof varName, CT_FILE_VAR_PREFIX "%s", database); cartSetString(cart, varName, ctFile); freeMem(ctFile); } /* Output new item coordinates for JavaScript to use for navigation */ jsInlineF("// START newItemPos\n"); jsInlineF("var newItemPos = {\"chrom\": \"%s\", \"start\": %d, \"end\": %d};\n", item->chrom, item->chromStart, item->chromEnd); jsInlineF("// END newItemPos\n"); hFreeConn(&conn); freez(&dupeCommand); } static int myVariantsExtraHeight(struct track *track) /* Return extra height of track. */ { return tl.fontHeight+2; } static void updateTextField(char *trackName, struct sqlConnection *conn, - char *tableName, char *fieldName, int id) -/* Update text valued field with new val. */ + char *tableName, char *fieldName, int id, char *scopeProject, char *scopeDb) +/* Update text valued field with new val. If scopeProject/scopeDb are non-NULL, + * include them in the WHERE clause so a recipient cannot edit rows outside + * the share's authorized project/db. scopeProject of "*" means all projects. */ { char varName[128]; safef(varName, sizeof(varName), "%s_%s", trackName, fieldName); char *newVal = cartOptionalString(cart, varName); if (newVal == NULL) return; if (endsWith(varName, "itemRgb")) { unsigned color; if (htmlColorForCode(newVal, &color)) { - char sql[256]; - sqlSafef(sql, sizeof(sql), "update %s set %s='%d' where id=%d", - tableName, fieldName, color, id); - sqlUpdate(conn, sql); + struct dyString *sql = sqlDyStringCreate( + "update %s set %s='%d' where id=%d", tableName, fieldName, color, id); + if (isNotEmpty(scopeDb)) + sqlDyStringPrintf(sql, " and db='%s'", scopeDb); + if (isNotEmpty(scopeProject) && !sameString(scopeProject, "*")) + sqlDyStringPrintf(sql, " and project='%s'", scopeProject); + sqlUpdate(conn, sql->string); + dyStringFree(&sql); cartRemove(cart, varName); } return; } struct dyString *sql = sqlDyStringCreate("update %s set %s='%s' where id=%d", tableName, fieldName, newVal, id); +if (isNotEmpty(scopeDb)) + sqlDyStringPrintf(sql, " and db='%s'", scopeDb); +if (isNotEmpty(scopeProject) && !sameString(scopeProject, "*")) + sqlDyStringPrintf(sql, " and project='%s'", scopeProject); sqlUpdate(conn, sql->string); dyStringFree(&sql); cartRemove(cart, varName); } static void myVariantsEditOrDelete(char *trackName, struct sqlConnection *conn, char *tableName) /* Troll through cart variables looking for things that indicate user edited item * or deleted it in hgc, and carry out edits. See hgc/myVariantsClick.c. */ { char varName[128]; char sql[256]; safef(varName, sizeof(varName), "%s_%s", trackName, "id"); char *idString = cartOptionalString(cart, varName); if (idString != NULL) { int id = sqlUnsigned(idString); idString = NULL; // Will be no good after cartRemove cartRemove(cart, varName); // Remove so only do edits once. /* Handle cancel. */ safef(varName, sizeof(varName), "%s_%s", trackName, "cancel"); if (cartVarExists(cart, varName)) { cartRemove(cart, varName); // Only want to do cancels once return; } /* Handle delete. */ safef(varName, sizeof(varName), "%s_%s", trackName, "delete"); if (cartVarExists(cart, varName)) { cartRemove(cart, varName); // Especially only want to do deletes once! sqlSafef(sql, sizeof(sql), "delete from %s where id=%d", tableName, id); sqlUpdate(conn, sql); /* Trigger CT refresh */ char *userName = getUserName(); if (userName) { char *ctFile = myVariantsWriteCtFile(userName, database, cart); if (isNotEmpty(ctFile)) { char ctVar[256]; safef(ctVar, sizeof ctVar, CT_FILE_VAR_PREFIX "%s", database); cartSetString(cart, ctVar, ctFile); freeMem(ctFile); } } return; } - /* Handle edits. */ - updateTextField(trackName, conn, tableName, "name", id); - updateTextField(trackName, conn, tableName, "description", id); - updateTextField(trackName, conn, tableName, "itemRgb", id); - updateTextField(trackName, conn, tableName, "chromStart", id); - updateTextField(trackName, conn, tableName, "chromEnd", id); - updateTextField(trackName, conn, tableName, "ref", id); - updateTextField(trackName, conn, tableName, "alt", id); - updateTextField(trackName, conn, tableName, "project", id); - updateTextField(trackName, conn, tableName, "mouseover", id); + /* Handle edits. Owner edits their own table, no project/db scope filter. */ + updateTextField(trackName, conn, tableName, "name", id, NULL, NULL); + updateTextField(trackName, conn, tableName, "description", id, NULL, NULL); + updateTextField(trackName, conn, tableName, "itemRgb", id, NULL, NULL); + updateTextField(trackName, conn, tableName, "chromStart", id, NULL, NULL); + updateTextField(trackName, conn, tableName, "chromEnd", id, NULL, NULL); + updateTextField(trackName, conn, tableName, "ref", id, NULL, NULL); + updateTextField(trackName, conn, tableName, "alt", id, NULL, NULL); + updateTextField(trackName, conn, tableName, "project", id, NULL, NULL); + updateTextField(trackName, conn, tableName, "mouseover", id, NULL, NULL); /* Update any custom fields */ { char *editUserName = getUserName(); struct slName *customCols = myVariantsGetCustomFields(editUserName); struct slName *col; for (col = customCols; col != NULL; col = col->next) - updateTextField(trackName, conn, tableName, col->name, id); + updateTextField(trackName, conn, tableName, col->name, id, NULL, NULL); slFreeList(&customCols); } /* Trigger CT refresh after edits */ { char *userName = getUserName(); if (userName) { char *ctFile = myVariantsWriteCtFile(userName, database, cart); if (isNotEmpty(ctFile)) { char ctVar[256]; safef(ctVar, sizeof ctVar, CT_FILE_VAR_PREFIX "%s", database); cartSetString(cart, ctVar, ctFile); freeMem(ctFile); } } } } } static struct linkedFeatures *loadMyVariantsItems(struct sqlConnection *conn, char *tableName, struct trackDb *tdb, char *whereExtra) /* Query tableName for myVariants items in the current window matching the * whereExtra clause (e.g. "db='hg38' and project='test'") and return them * as a linkedFeatures list. */ { struct linkedFeatures *lf, *lfList = NULL; struct dyString *query = sqlDyStringCreate("select * from %s where ", tableName); hAddBinToQueryGeneral("bin", winStart, winEnd, query); sqlDyStringPrintf(query, " chrom='%s' and chromStart < %d and chromEnd > %d", chromName, winEnd, winStart); if (isNotEmpty(whereExtra)) sqlDyStringPrintf(query, " and (%-s)", whereExtra); struct sqlResult *sr = sqlGetResult(conn, query->string); dyStringFree(&query); char **row; struct dyString *mouseover = dyStringNew(0); while ((row = sqlNextRow(sr)) != NULL) { struct myVariants *item = myVariantsLoad(row); struct bed *bed; AllocVar(bed); char buf[64]; safef(buf, sizeof(buf), "%u %s", item->id, item->name); bed->chrom = item->chrom; bed->chromStart = item->chromStart; bed->chromEnd = item->chromEnd; bed->name = cloneString(buf); bed->score = item->score; bed->strand[0] = item->strand[0]; bed->thickStart = item->thickStart; bed->thickEnd = item->thickEnd; bed->itemRgb = item->itemRgb; lf = bedMungToLinkedFeatures(&bed, tdb, 9, 0, 1000, TRUE); dyStringClear(mouseover); if (item->mouseover && isNotEmpty(item->mouseover)) lf->mouseOver = cloneString(item->mouseover); else { dyStringPrintf(mouseover, "%s", buf); if (item->ref != NULL && isNotEmpty(item->ref)) dyStringPrintf(mouseover, "<br>Ref: %s", item->ref); if (item->alt != NULL && isNotEmpty(item->alt)) dyStringPrintf(mouseover, "<br>Alt: %s", item->alt); lf->mouseOver = cloneString(dyStringContents(mouseover)); } slAddHead(&lfList, lf); } sqlFreeResult(&sr); dyStringFree(&mouseover); return lfList; } void myVariantsLoadItems(struct track *track) /* Load up items in track already. Also make up a pseudo-item that is * where you drag to create an item. */ { struct linkedFeatures *lfList = NULL; boolean isShared = startsWith("myVariants_shared_", track->track); if (isShared) { - /* Extract the token and look up the share info from the cart */ - char *token = track->track + strlen("myVariants_shared_"); - char cartVar[256]; - safef(cartVar, sizeof(cartVar), MYVAR_SHARED_CART_PREFIX "%s", token); - char *cartVal = cartOptionalString(cart, cartVar); - if (isEmpty(cartVal)) + /* Resolve via hgcentral so revoked/downgraded shares stop returning data. */ + struct myVariantsShare *share = myVariantsResolveSharedTrack(track->track, cart); + if (share == NULL) return; - char *owner = NULL, *project = NULL, *db = NULL; - int permission = 0; - if (!myVariantsParseShareCartValue(cartVal, &owner, &project, &db, &permission, NULL)) + /* Shared tracks are per-assembly; only load when the browser's current + * db matches the share's db. */ + if (!sameString(share->db, database)) + { + myVariantsShareFree(&share); return; - char *tableName = myVariantsGetDbTable(owner); + } + char *tableName = myVariantsGetDbTable(share->ownerUser); if (isEmpty(tableName)) { - freeMem(owner); - freeMem(project); - freeMem(db); + myVariantsShareFree(&share); return; } struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH); - struct dyString *whereClause = sqlDyStringCreate("db='%s'", database); - if (!sameString(project, "*")) - sqlDyStringPrintf(whereClause, " and project='%s'", project); + struct dyString *whereClause = sqlDyStringCreate("db='%s'", share->db); + if (!sameString(share->project, "*")) + sqlDyStringPrintf(whereClause, " and project='%s'", share->project); lfList = loadMyVariantsItems(conn, tableName, track->tdb, dyStringCannibalize(&whereClause)); hFreeConn(&conn); - freeMem(owner); - freeMem(project); - freeMem(db); + myVariantsShareFree(&share); } else { /* Load user's own items */ char *userName = getUserName(); char *db = myVariantsGetDatabaseForUser(userName); if (!db) return; char *tableName = myVariantsGetDbTable(userName); struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH); myVariantsEditOrDelete(track->track, conn, tableName); struct dyString *whereClause = sqlDyStringCreate("db='%s'", database); lfList = loadMyVariantsItems(conn, tableName, track->tdb, dyStringCannibalize(&whereClause)); hFreeConn(&conn); } slReverse(&lfList); track->items = lfList; } char *myVariantsName(struct track *track, void *item) /* Return name of one of an item to display on left side. */ { struct linkedFeatures *lf = item; char *name = lf->name; return name; } void myVariantsDrawLeftLabels(struct track *track, int seqStart, int seqEnd, struct hvGfx *hvg, int xOff, int yOff, int width, int height, boolean withCenterLabels, MgFont *font, Color color, enum trackVisibility vis) /* Draw left label - just in dense or full mode. Needed to cope with empty space at top of track. */ { int y = yOff + myVariantsExtraHeight(track); int fontHeight = mgFontLineHeight(font); if (withCenterLabels) y += mgFontLineHeight(font); if (vis == tvDense) hvGfxTextRight(hvg, xOff, y, width-1, fontHeight, color, font, track->shortLabel); else if (vis == tvFull) { struct bed *bed, *bedList = track->items; for (bed = bedList; bed != NULL; bed = bed->next) { if (track->itemLabelColor != NULL) color = track->itemLabelColor(track, bed, hvg); int itemHeight = track->itemHeight(track, bed); hvGfxTextRight(hvg, xOff, y, width - 1, itemHeight, color, font, track->itemName(track, bed)); y += itemHeight; } } /* In pack mode the draw routine takes care of the labels. */ } int myVariantsTotalHeight(struct track *track, enum trackVisibility vis) /* Most fixed height track groups will use this to figure out the height * they use. */ { track->height = tgFixedTotalHeightOptionalOverflow(track, vis, tl.fontHeight+1, tl.fontHeight, FALSE) + myVariantsExtraHeight(track); return track->height; } void myVariantsMethods(struct track *track) /* Set up special methods for myVariants type tracks. */ { linkedFeaturesMethods(track); track->totalHeight = myVariantsTotalHeight; track->drawLeftLabels = myVariantsDrawLeftLabels; track->loadItems = myVariantsLoadItems; track->itemName = myVariantsName; track->nextItemButtonable = TRUE; } static void apiSuccess(struct jsonWrite *jw) /* Send 200 JSON response and exit. */ { printf("Content-Type: application/json\n\n"); puts(jw->dy->string); jsonWriteFree(&jw); exit(0); } static void apiError(int httpStatus, char *message) /* Send error JSON response with given HTTP status and exit. */ { printf("Status: %d\n", httpStatus); printf("Content-Type: application/json\n\n"); struct jsonWrite *jw = jsonWriteNew(); jsonWriteObjectStart(jw, NULL); jsonWriteString(jw, "error", message); jsonWriteObjectEnd(jw); puts(jw->dy->string); jsonWriteFree(&jw); exit(0); } static void shareToJson(struct jsonWrite *jw, struct myVariantsShare *share) /* Write a single share record as a JSON object into jw. */ { jsonWriteObjectStart(jw, NULL); jsonWriteNumber(jw, "id", share->id); jsonWriteString(jw, "ownerUser", share->ownerUser); jsonWriteString(jw, "shareToken", share->shareToken); jsonWriteString(jw, "project", share->project); jsonWriteString(jw, "db", share->db); jsonWriteNumber(jw, "permission", share->permission); jsonWriteString(jw, "targetUser", share->targetUser); jsonWriteString(jw, "label", share->label); jsonWriteString(jw, "createdAt", share->createdAt); jsonWriteObjectEnd(jw); } static boolean isStateChangingShareAction(char *action) /* Returns TRUE for actions that modify the share table. Those require an * hgsid match to defend against CSRF; read-only actions don't, because the * response only goes to the originating cookie-bearing browser. */ { return sameString(action, "createShare") || sameString(action, "revokeShare"); } void myVariantsShareApiHandler(char *action) /* Handle share API requests. Outputs JSON to stdout and calls exit(0). * Called from main() before cartHtmlShell. */ { char *userName = getUserName(); if (userName == NULL) apiError(401, "not logged in"); /* CSRF protection for state-changing actions: require the request to carry * an hgsid matching the user's session. An attacker page can fire requests * with the user's cookies but cannot read or guess the session id, so this * blocks cross-site forgery of share creation/revocation. */ if (isStateChangingShareAction(action)) { char *suppliedHgsid = cgiOptionalString("hgsid"); char *expectedHgsid = cartSessionId(cart); if (isEmpty(suppliedHgsid) || isEmpty(expectedHgsid) || !sameString(suppliedHgsid, expectedHgsid)) apiError(403, "session token missing or invalid"); } char *db = cgiOptionalString("db"); if (db == NULL) apiError(400, "missing required parameter: db"); struct sqlConnection *conn = hConnectCentral(); if (!sqlTableExists(conn, "myVariantsShares")) { hDisconnectCentral(&conn); apiError(500, "myVariantsShares table not found - contact genome-www@ucsc.edu"); } if (sameString(action, "createShare")) { char *project = cgiOptionalString("project"); if (project == NULL) apiError(400, "missing required parameter: project"); int permission = cgiOptionalInt("permission", MYVAR_PERM_READONLY); if (permission != MYVAR_PERM_READONLY && permission != MYVAR_PERM_READWRITE) apiError(400, "permission must be 0 or 1"); char *targetUser = cgiOptionalString("targetUser"); char *label = cgiOptionalString("label"); /* Reject overlong fields before MySQL would (varchar(255)). Use 200 to * leave room for any prefixing/quoting we add downstream. */ if (strlen(project) > 200 || (targetUser && strlen(targetUser) > 200) || (label && strlen(label) > 200)) apiError(400, "project, targetUser, and label must each be <= 200 chars"); /* Project must be "*" (all) or one of the owner's existing project values. * Prevents creating misleading share URLs that filter on a non-existent * project name. */ if (!sameString(project, "*")) { struct slName *projects = myVariantsGetProjects(userName); boolean found = slNameInList(projects, project); slFreeList(&projects); if (!found) apiError(400, "project does not exist for current user"); } + if (isNotEmpty(targetUser)) + { + char gbq[512]; + sqlSafef(gbq, sizeof(gbq), + "select count(*) from gbMembers where userName='%s'", targetUser); + if (sqlQuickNum(conn, gbq) == 0) + apiError(400, "targetUser not found - check the spelling, or omit" + " targetUser to make an 'anyone with link' share"); + } struct myVariantsShare *share = myVariantsCreateShare(conn, userName, project, db, permission, targetUser, label); struct jsonWrite *jw = jsonWriteNew(); jsonWriteObjectStart(jw, NULL); jsonWriteString(jw, "status", "ok"); jsonWriteString(jw, "token", share->shareToken); jsonWriteNumber(jw, "id", share->id); jsonWriteObjectEnd(jw); myVariantsShareFree(&share); hDisconnectCentral(&conn); apiSuccess(jw); } else if (sameString(action, "getShares")) { struct myVariantsShare *shares = myVariantsGetSharesForOwner(conn, userName, db); struct jsonWrite *jw = jsonWriteNew(); jsonWriteObjectStart(jw, NULL); jsonWriteString(jw, "status", "ok"); jsonWriteListStart(jw, "shares"); struct myVariantsShare *share; for (share = shares; share != NULL; share = share->next) shareToJson(jw, share); jsonWriteListEnd(jw); jsonWriteObjectEnd(jw); myVariantsShareFreeList(&shares); hDisconnectCentral(&conn); apiSuccess(jw); } else if (sameString(action, "getSharesForMe")) { struct myVariantsShare *shares = myVariantsGetSharesForUser(conn, userName, db); struct jsonWrite *jw = jsonWriteNew(); jsonWriteObjectStart(jw, NULL); jsonWriteString(jw, "status", "ok"); jsonWriteListStart(jw, "shares"); struct myVariantsShare *share; for (share = shares; share != NULL; share = share->next) shareToJson(jw, share); jsonWriteListEnd(jw); jsonWriteObjectEnd(jw); myVariantsShareFreeList(&shares); hDisconnectCentral(&conn); apiSuccess(jw); } else if (sameString(action, "revokeShare")) { char *token = cgiOptionalString("shareToken"); if (token == NULL) apiError(400, "missing required parameter: shareToken"); boolean revoked = myVariantsRevokeShare(conn, token, userName); hDisconnectCentral(&conn); if (!revoked) apiError(404, "share not found"); struct jsonWrite *jw = jsonWriteNew(); jsonWriteObjectStart(jw, NULL); jsonWriteString(jw, "status", "ok"); jsonWriteObjectEnd(jw); apiSuccess(jw); } else { hDisconnectCentral(&conn); apiError(400, "unknown action"); } } void myVariantsProcessShareParam() /* Check for myVarShare CGI param, validate the share token, and store * the share reference in the cart. Must be called before track loading. */ { char *token = cgiOptionalString(MYVAR_SHARE_CGI_VAR); if (isEmpty(token)) return; if (strlen(token) != MYVAR_TOKEN_LENGTH) { notify("A share link in the URL couldn't be read. The shared track hasn't been added, but the rest of the browser works normally.", "myVarShareInvalidToken"); return; } char *userName = getUserName(); struct sqlConnection *conn = hConnectCentral(); if (!sqlTableExists(conn, "myVariantsShares")) { hDisconnectCentral(&conn); return; } struct myVariantsShare *share = myVariantsGetShareByToken(conn, token); hDisconnectCentral(&conn); if (share == NULL) { notify("A share link is no longer valid (it may have been revoked). The shared track isn't available, but the rest of the browser works normally.", "myVarShareRevoked"); return; } /* Targeted shares require the matching logged-in user. Shares with no targetUser * ("anyone with link") are accessible to anon users too. */ if (isNotEmpty(share->targetUser)) { if (userName == NULL) { notify("A shared track in this view is limited to a specific user. You can keep browsing.", "myVarShareNeedLogin"); myVariantsShareFree(&share); return; } if (!sameString(share->targetUser, userName)) { notify("A shared track in this view wasn't shared with your account. You can keep browsing; the track just won't appear.", "myVarShareWrongUser"); myVariantsShareFree(&share); return; } } /* Store in cart so the shared track loader can find it during track loading */ char cartVar[256]; safef(cartVar, sizeof(cartVar), MYVAR_SHARED_CART_PREFIX "%s", token); char *cartVal = myVariantsShareCartValue(share); cartSetString(cart, cartVar, cartVal); freeMem(cartVal); myVariantsShareFree(&share); } void myVariantsProcessSharedEdits() /* Process pending edits for shared tracks before any track loading. * This ensures edits are committed to the database before both the * owner's track and the shared track query for items. */ { /* Anon users cannot edit shared items, regardless of share permission. */ char *userName = getUserName(); if (userName == NULL) return; struct hashEl *shareVars = cartFindPrefix(cart, MYVAR_SHARED_CART_PREFIX); struct hashEl *el; for (el = shareVars; el != NULL; el = el->next) { char *token = el->name + strlen(MYVAR_SHARED_CART_PREFIX); char trackName[512]; safef(trackName, sizeof(trackName), "myVariants_shared_%s", token); /* Check if there's a pending edit for this shared track */ char varName[256]; safef(varName, sizeof(varName), "%s_%s", trackName, "id"); char *idString = cartOptionalString(cart, varName); if (idString == NULL) continue; - /* Re-validate the share against hgcentral on every edit. The cart-cached - * permission is not authoritative: the owner may have revoked the share - * or downgraded it from read-write to read-only since the recipient's - * cart was last set, and we don't want stale cart contents to grant - * writes that the live share record no longer permits. */ - struct sqlConnection *centralConn = hConnectCentral(); - if (!sqlTableExists(centralConn, "myVariantsShares")) - { - hDisconnectCentral(¢ralConn); - continue; - } - struct myVariantsShare *liveShare = myVariantsGetShareByToken(centralConn, token); - hDisconnectCentral(¢ralConn); + /* Revalidate via hgcentral so revoked/downgraded shares can't write. */ + struct myVariantsShare *liveShare = myVariantsResolveSharedTrack(trackName, cart); if (liveShare == NULL) continue; if (liveShare->permission != MYVAR_PERM_READWRITE) { myVariantsShareFree(&liveShare); continue; } - if (isNotEmpty(liveShare->targetUser) && !sameString(liveShare->targetUser, userName)) - { - myVariantsShareFree(&liveShare); - continue; - } - char *owner = cloneString(liveShare->ownerUser); - myVariantsShareFree(&liveShare); - - char *tableName = myVariantsGetDbTable(owner); + char *tableName = myVariantsGetDbTable(liveShare->ownerUser); if (isEmpty(tableName)) { - freeMem(owner); + myVariantsShareFree(&liveShare); continue; } int id = sqlUnsigned(idString); cartRemove(cart, varName); /* Handle cancel */ safef(varName, sizeof(varName), "%s_%s", trackName, "cancel"); if (cartVarExists(cart, varName)) { cartRemove(cart, varName); - freeMem(owner); + myVariantsShareFree(&liveShare); continue; } - /* Apply field edits - no delete, no project change */ + /* Apply field edits, scoped to the share's project/db so a recipient + * cannot edit rows in projects/assemblies they were not granted. */ + char *sp = liveShare->project; + char *sd = liveShare->db; struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH); - updateTextField(trackName, conn, tableName, "name", id); - updateTextField(trackName, conn, tableName, "description", id); - updateTextField(trackName, conn, tableName, "itemRgb", id); - updateTextField(trackName, conn, tableName, "chromStart", id); - updateTextField(trackName, conn, tableName, "chromEnd", id); - updateTextField(trackName, conn, tableName, "ref", id); - updateTextField(trackName, conn, tableName, "alt", id); - updateTextField(trackName, conn, tableName, "mouseover", id); - struct slName *customCols = myVariantsGetCustomFields(owner); + updateTextField(trackName, conn, tableName, "name", id, sp, sd); + updateTextField(trackName, conn, tableName, "description", id, sp, sd); + updateTextField(trackName, conn, tableName, "itemRgb", id, sp, sd); + updateTextField(trackName, conn, tableName, "chromStart", id, sp, sd); + updateTextField(trackName, conn, tableName, "chromEnd", id, sp, sd); + updateTextField(trackName, conn, tableName, "ref", id, sp, sd); + updateTextField(trackName, conn, tableName, "alt", id, sp, sd); + updateTextField(trackName, conn, tableName, "mouseover", id, sp, sd); + struct slName *customCols = myVariantsGetCustomFields(liveShare->ownerUser); struct slName *col; for (col = customCols; col != NULL; col = col->next) - updateTextField(trackName, conn, tableName, col->name, id); + updateTextField(trackName, conn, tableName, col->name, id, sp, sd); slFreeList(&customCols); hFreeConn(&conn); - freeMem(owner); + myVariantsShareFree(&liveShare); } hashElFreeList(&shareVars); } boolean myVariantsTrackEnabled() /* Return TRUE if the "My Variants" feature is enabled in hg.conf * and the current request comes from a valid user */ { return cfgOptionBooleanDefault("doMyVariants", FALSE) && (getUserName() != NULL); }