diff --git a/static/js/chart/draw_d3_map.ts b/static/js/chart/draw_d3_map.ts index 784750bfba..4550be3f50 100644 --- a/static/js/chart/draw_d3_map.ts +++ b/static/js/chart/draw_d3_map.ts @@ -222,17 +222,21 @@ function mouseOutAction( hoverClassNames: string[] ): void { const container = d3.select(containerElement); + const hoverClassesToRemove = hoverClassNames.join(" "); + + // Remove global highlight class container.classed(HOVER_HIGHLIGHTED_CLASS_NAME, false); + if (placeDcid) { + // Remove highlight class from the specific place const pathSelection = container.select(`#${getPlacePathId(placeDcid)}`); - for (const className of hoverClassNames) { - pathSelection.classed(className, false); - } + pathSelection.classed(hoverClassesToRemove, false); } else { // If no placeDcid is provided, clear all highlights - for (const className of hoverClassNames) { - container.selectAll("." + className).classed(className, false); - } + const allHighlightsSelector = hoverClassNames.map((c) => `.${c}`).join(","); + container + .selectAll(allHighlightsSelector) + .classed(hoverClassesToRemove, false); } // bring original highlighted region back to the top container.select("." + HIGHLIGHTED_CLASS_NAME).raise(); diff --git a/static/js/shared/util.test.ts b/static/js/shared/util.test.ts index 2d4ca20570..dcbafae90e 100644 --- a/static/js/shared/util.test.ts +++ b/static/js/shared/util.test.ts @@ -55,17 +55,31 @@ test("getCappedStatVarDate", () => { describe("sanitizeSourceUrl", () => { test.each([ // http and https urls should be returned as is - ["https://example.com", "https://example.com"], - ["http://example.com", "http://example.com"], + ["https://example.com", "https://example.com/"], + ["http://example.com", "http://example.com/"], // urls without protocol should be prefixed with https - ["example.com", "https://example.com"], - ["www.example.com", "https://www.example.com"], + ["example.com", "https://example.com/"], + ["www.example.com", "https://www.example.com/"], // whitespace should be handled elegantly - [" example.com ", "https://example.com"], + [" example.com ", "https://example.com/"], // urls with javascript, vbscript, or data should be sanitized ["javascript:alert(1)", ""], ["vbscript:alert(1)", ""], ["data:text/html,", ""], + ["https://javascript:alert(1)", ""], + ["http://vbscript:alert(1)", ""], + ["data:text/html,", ""], + // Valid paths with javascript, vbscript, or data should be preserved + [ + "https://en.wikipedia.org/wiki/JavaScript", + "https://en.wikipedia.org/wiki/JavaScript", + ], + ["http://example.com/vbscript/about", "http://example.com/vbscript/about"], + [ + "https://my-app.com/api?data=dataPayload", + "https://my-app.com/api?data=dataPayload", + ], + ["http://javascript.com", "http://javascript.com/"], // empty string should not result in error ["", ""], ])("should convert %p to %p", (input, expected) => { diff --git a/static/js/shared/util.ts b/static/js/shared/util.ts index e009293d89..3b3c07c825 100644 --- a/static/js/shared/util.ts +++ b/static/js/shared/util.ts @@ -146,25 +146,25 @@ export function sanitizeSourceUrl(url: string): string { if (!url) { return ""; } + const trimmedUrl = url.trim(); - const lowerUrl = trimmedUrl.toLowerCase(); - // If it starts with http:// or https://, return as is - if (lowerUrl.startsWith("http://") || lowerUrl.startsWith("https://")) { - return trimmedUrl; - } + // Ensure we have a protocol for the parser to work + // If the input is missing a valid protocol, we prepend https:// + // Prepending https:// blocks unsafe protocols like javascript:// or vbscript:// + const urlToParse = + trimmedUrl.startsWith("http://") || trimmedUrl.startsWith("https://") + ? trimmedUrl + : "https://" + trimmedUrl; - // Block unsafe protocols - if ( - lowerUrl.startsWith("javascript:") || - lowerUrl.startsWith("vbscript:") || - lowerUrl.startsWith("data:") - ) { + try { + const parsed = new URL(urlToParse); + return parsed.href; + } catch (e) { + // If the URL does not have a valid URL structure, return empty + // This will block urls with scripts like http://javascript:alert(1) return ""; } - - // Otherwise, assume it is relative and needs https:// - return "https://" + trimmedUrl; } /**