From 595b7e2fef2f8901a3236a996ac2d1f05036e3aa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 06:16:41 +0000 Subject: [PATCH] fix(frontend): render map relay tooltips as text The relay marker tooltip was passed to Leaflet as a HTML string. Nicknames are limited to [0-9a-zA-Z]{1,19} by the descriptor parser today, so this is not exploitable, but THREAT_MODEL.md forbids string tooltips for relay-derived fields. Use a text node like the country usage layer. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_015Cf2Sci2amhHihBa9yt91x --- frontend/src/util/layer-construction.test.ts | 15 +++++++++++---- frontend/src/util/layer-construction.ts | 5 ++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/frontend/src/util/layer-construction.test.ts b/frontend/src/util/layer-construction.test.ts index 2ee7a8c6..dee47938 100644 --- a/frontend/src/util/layer-construction.test.ts +++ b/frontend/src/util/layer-construction.test.ts @@ -125,16 +125,23 @@ describe("buildRelayLayer", () => { it("lists up to three nicknames", () => { const relays = ["a", "b", "c"].map(nickname => makeRelay({nickname})) - expect(tooltipAfterHover(relays).tooltip?.getContent()).toBe("a, b, c") + expect(tooltipAfterHover(relays).tooltip?.getContent()).toHaveProperty("textContent", "a, b, c") }) it("summarizes more than three relays", () => { - expect(tooltipAfterHover(makeRelays(4)).tooltip?.getContent()).toBe("4 relays") + expect(tooltipAfterHover(makeRelays(4)).tooltip?.getContent()).toHaveProperty("textContent", "4 relays") }) it("summarizes relays without nicknames", () => { const relays = [makeRelay({nickname: ""}), makeRelay({nickname: ""})] - expect(tooltipAfterHover(relays).tooltip?.getContent()).toBe("2 relays") + expect(tooltipAfterHover(relays).tooltip?.getContent()).toHaveProperty("textContent", "2 relays") + }) + + it("renders relay text as text, not as markup", () => { + const relays = [makeRelay({nickname: ""})] + const content = tooltipAfterHover(relays).tooltip?.getContent() as HTMLElement + expect(content.querySelector("img")).toBeNull() + expect(content.textContent).toBe("") }) it("removes the tooltip on mouseout", () => { @@ -329,7 +336,7 @@ describe("buildRelayLayer sized by consensus weight", () => { ] const layer = markers(buildRelayLayer(buildRelayCoordinatesMap(relays), false, colors, vi.fn(), true, topRelayCount)) layer[1].fire("mouseover") - return layer[0].getTooltip()?.getContent() + return (layer[0].getTooltip()?.getContent() as HTMLElement | undefined)?.textContent } it("is relative to the consensus weight of the shown relays", () => { diff --git a/frontend/src/util/layer-construction.ts b/frontend/src/util/layer-construction.ts index e26ebda2..754580fd 100644 --- a/frontend/src/util/layer-construction.ts +++ b/frontend/src/util/layer-construction.ts @@ -150,7 +150,10 @@ function addRelayNicknameTooltip(relays: RelayLocationDto[], mostImportantRelay: }, ) .on('mouseover', async function (this: L.Marker) { - marker.bindTooltip(tooltip, {permanent: false, direction: 'top'}).openTooltip(); + // A text node instead of a HTML string, so that relay-derived text is never interpreted as markup + const content = document.createElement('div') + content.textContent = tooltip + marker.bindTooltip(content, {permanent: false, direction: 'top'}).openTooltip(); }) .on('mouseout', function (this: L.Marker) { marker.unbindTooltip();