From 3c7931b8938b809a61d8c56d5467e60dc9acbaf3 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sun, 12 Jul 2026 17:53:46 -0400 Subject: [PATCH 1/6] fix: escape title in renderSvg --- src/render/svg.ts | 3 ++- src/utils/params.ts | 3 +-- tests/render/svg.test.ts | 6 ++++++ tests/utils/params.test.ts | 6 +++--- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/render/svg.ts b/src/render/svg.ts index 24847a1..249ea05 100644 --- a/src/render/svg.ts +++ b/src/render/svg.ts @@ -1,5 +1,6 @@ import { TITLE_STYLES } from "../constants/styles.js"; import type { ChartResult } from "../charts/types.js"; +import { sanitize } from "../utils/sanitize.js"; export function renderSvg( width: number, height: number, background: string, @@ -12,7 +13,7 @@ export function renderSvg( text-anchor="middle" fill="${textColour}" font-family="Arial" font-size="${TITLE_STYLES.FONT_SIZE}" > - ${title} + ${sanitize(title)} ` : ''; diff --git a/src/utils/params.ts b/src/utils/params.ts index 59f33e7..f22e0fc 100644 --- a/src/utils/params.ts +++ b/src/utils/params.ts @@ -2,7 +2,6 @@ import { VALID_TYPES } from "../constants/types.js"; import { DEFAULT_CONFIG } from "../constants/config.js"; import { THEMES } from "../constants/themes.js"; import type { ChartType, GapType, Theme } from "../charts/types.js"; -import { sanitize } from "./sanitize.js"; export interface ParsedParams { chartType: ChartType; @@ -62,7 +61,7 @@ export function parseQueryParams(query: QueryParams): ParsedParams { return { chartType, - chartTitle: query["hide_title"] === "true" ? '' : sanitize(query["title"] ?? DEFAULT_CONFIG.TITLE), + chartTitle: query["hide_title"] === "true" ? '' : query["title"] ?? DEFAULT_CONFIG.TITLE, width: Math.max(parseIntSafe(query["width"], DEFAULT_CONFIG.WIDTH), DEFAULT_CONFIG.MIN_WIDTH ), height: Math.max(parseIntSafe(query["height"], DEFAULT_CONFIG.HEIGHT), DEFAULT_CONFIG.MIN_HEIGHT), count: Math.min(Math.max(count, 1), DEFAULT_CONFIG.MAX_COUNT), diff --git a/tests/render/svg.test.ts b/tests/render/svg.test.ts index 10ff2c6..09c32bb 100644 --- a/tests/render/svg.test.ts +++ b/tests/render/svg.test.ts @@ -28,4 +28,10 @@ describe("renderSvg", () => { expect(result).toContain(chart.segments); expect(result).toContain(chart.legend); }); + + it("escapes markup in the title", () => { + const result = renderSvg(600, 400, "#ffffff", chart, ``, "#000000"); + expect(result).toContain("<script>alert("x")</script>"); + expect(result).not.toContain("` }); - expect(params.chartTitle).toBe("<scripts>alert("x")</script>"); + it("returns title raw", () => { + const params = parseQueryParams({ title: `&"'` }); + expect(params.chartTitle).toBe(`&"'`); }); it("clamps count between 1 and MAX_COUNT", () => { From fee3d233f05a0e6388ec4871594dbdc95fa307e2 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sun, 12 Jul 2026 18:03:53 -0400 Subject: [PATCH 2/6] fix: escape language names in createLegend --- src/charts/legend.ts | 3 ++- tests/charts/legend.test.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/charts/legend.ts b/src/charts/legend.ts index 426e112..c8e62bc 100644 --- a/src/charts/legend.ts +++ b/src/charts/legend.ts @@ -1,6 +1,7 @@ import { LEGEND_SHIFT_THRESHOLD, LEGEND_STYLES } from "../constants/styles.js"; import type { Theme, Language, GapType } from "./types.js"; import { formatLegendEntry, resolveColour } from "./helpers.js"; +import { sanitize } from "../utils/sanitize.js"; export function createLegend( languages: Language[], @@ -49,7 +50,7 @@ export function createLegend( font-size="${LEGEND_STYLES.FONT_SIZE}" font-family="Arial" > - ${formatLegendEntry(lang, totalPct, gapType)} + ${sanitize(formatLegendEntry(lang, totalPct, gapType))} `; }).join(''); diff --git a/tests/charts/legend.test.ts b/tests/charts/legend.test.ts index 79c82ad..cbe1ce2 100644 --- a/tests/charts/legend.test.ts +++ b/tests/charts/legend.test.ts @@ -43,6 +43,13 @@ describe("createLegend", () => { expect(result).toContain(`stroke-width="0.5"`); }); + it("escapes markup in language names", () => { + const langs = [{ lang: ``, pct: 100 }]; + const result = createLegend(langs, theme, 300, false, 110, "gap"); + expect(result).toContain("<img src=x onerror=alert(1)> 100.0%"); + expect(result).not.toContain(" { const langs = [ { lang: "C#", pct: 50 }, From 1aacfc956b559017910be0c067ad5acd84d528c6 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Sun, 12 Jul 2026 18:07:40 -0400 Subject: [PATCH 3/6] test: renderError escaping --- tests/render/error.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/render/error.test.ts b/tests/render/error.test.ts index cfe3cd3..d7b5a9a 100644 --- a/tests/render/error.test.ts +++ b/tests/render/error.test.ts @@ -27,4 +27,10 @@ describe("renderError", () => { expect(result).toContain("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA..."); expect(result).not.toContain("A".repeat(50)); }); + + it("escapes markup in the message", () => { + const result = renderError(``, 400, 300); + expect(result).toContain("<script>"x"</script>"); + expect(result).not.toContain("