0cfbb0ae0beabd4e52ccad30dd876d4d42795cba max Tue Sep 29 23:32:19 2026 -0700 hgTracks: html-encode the values the track download dialog builds into its markup The dialog assembles its html by string concatenation, and several of the values that go in are not written by the code: the track names and short labels come from trackDb, which for a hub means from the hub, the assembly name can carry a hub's own naming, and the region line is whatever the position box holds. They now go through htmlEncode() from utils.js, the shared helper, which covers both element text and single-quoted attribute values, so a label or a name comes out as the text it is meant to be. refs #38226 diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js index ee62c57660f..ba08d958c70 100644 --- src/hg/js/hgTracks.js +++ src/hg/js/hgTracks.js @@ -8090,83 +8090,86 @@ // pane rather than in the dialog body, because the body scrolls once the // track list is long and a message the user has to scroll to find is no // better than no message. The Download button gets an id so that it can be // disabled while the file is being built. dialogWrap.find(".ui-dialog-buttonpane button").first().attr("id", "downloadTracksGo"); dialogWrap.find(".ui-dialog-buttonset").after( "<span id='downloadTracksStatus' style='display: none; padding-left: 10px'>" + "Preparing the file, this can take a while for a large region...</span>"); } // the strand the browser is showing, which the Reverse button flips let strandStr = hgTracks.revCmplDisp ? "(- strand)" : "(+ strand)"; htmlStr = "<p>Use this selection window to download track data for the current region:" + // the position is data, not prose, so it gets the monospace treatment and a // line of its own "<br><span style=\"font-family: 'Roboto Mono', 'Courier New', monospace; " + - "font-weight: bold\">" + genomePos.get() + " " + strandStr + "</span><br>" + + "font-weight: bold\">" + htmlEncode(genomePos.get()) + " " + strandStr + + "</span><br>" + "Large regions may be slow to download.</p>"; // the output format comes first: it decides which tracks can be downloaded at all htmlStr += "<div>"; htmlStr += "<label style='padding-right: 10px' for='outputFormat'>Choose an output format</label>"; htmlStr += "<select name='outputFormat' id='outputFormat'>"; htmlStr += "<option selected value='json'>JSON</option>"; htmlStr += "<option value='csv'>CSV</option>"; htmlStr += "<option value='tsv'>TSV</option>"; // the GenBank output is under hg.conf control for now let withGenbank = typeof showGenbankDownload !== 'undefined' && showGenbankDownload; if (withGenbank) { htmlStr += "<option value='gb'>GenBank</option>"; } htmlStr += "</select>"; // an option of the format, so it sits with it and disappears with it htmlStr += "<div id='downloadHeaderOpt'>"; htmlStr += "<label style='padding-right: 10px' for='downloadTrackHeaders'>Include track column headers</label>"; htmlStr += "<input type='checkbox' checked id='downloadTrackHeaders'></input>"; htmlStr += "</div>"; // its own block, so that it does not move up next to the format select when the // column header option above it is hidden htmlStr += "<div>"; htmlStr += "<label style='padding-right: 10px' for='downloadFileName'>Enter an output file name</label>"; // undecoratedTrack strips the hub_<id>_ that a hub assembly's name carries, the // hub id is this browser's cart detail and means nothing in a file name htmlStr += "<input type=text size=30 class='downloadFileName' id='downloadFileName'" + - " value='" + undecoratedTrack(getDb()) + ".tracks'></input>"; + " value='" + htmlEncode(undecoratedTrack(getDb())) + ".tracks'></input>"; htmlStr += "</div>"; htmlStr += "</div>"; htmlStr += "<div style='margin-top: 12px'>"; _.each(hgTracks.trackDb, function(track, trackName) { showDisabledMsg = false; if (!trackName.includes("Squish") && trackName !== "ruler" && track.visibility > 0) { - htmlStr += "<input type=checkbox class='downloadTrackName' id='" + trackName + "'"; + htmlStr += "<input type=checkbox class='downloadTrackName' id='" + + htmlEncode(trackName) + "'"; // the first word of the type is all the output formats need. A hub writes // its own type strings and this one goes into an html attribute, so keep // it to the characters a type name can legitimately have htmlStr += " data-track-type='" + (track.type || "").split(" ")[0].replace(/[^A-Za-z0-9]/g, "") + "'"; if (trackName.startsWith("ct_") || trackName === "hgPcrResult" || track.type === "mathWig" || !tdbIsLeaf(track)) { showDisabledMsg = true; // disabled whatever the output format is, so the format must not undo it htmlStr += " data-always-disabled='1' disabled "; } else { htmlStr += " checked "; } htmlStr += ">"; - htmlStr += "<label>" + track.shortLabel + "</label>"; + htmlStr += "<label>" + htmlEncode(track.shortLabel) + "</label>"; htmlStr += "</input>"; if (showDisabledMsg) { - htmlStr += " <span id='" + trackName + "Tooltip'><a href='#'>(?)</a></span>"; + htmlStr += " <span id='" + htmlEncode(trackName) + + "Tooltip'><a href='#'>(?)</a></span>"; } htmlStr += "<br>"; } }); htmlStr += "</div>"; htmlStr += "<div><button id='checkAllDownloadTracks'>Check All</button>" + " " + "<button id='uncheckAllDownloadTracks'>Clear All</button>" + "</div>"; if (withGenbank) { htmlStr += "<div id='downloadGenbankNote' " + "style='display: none; margin-top: 12px; font-size: 90%'>The GenBank file also " + "contains the DNA sequence of the region, always on the forward strand, even " + "when the browser is showing the reverse complement. Tracks with numerical " + "data, e.g. bigWigs, have no GenBank equivalent and are greyed out." +