Skip to content

Commit 8adb60f

Browse files
committed
fix: validate hex colours in hexColor() to prevent XML injection
hexColor() previously did no validation — just stripped '#' and uppercased. If a non-hex string (like gradient XML) was passed, it was silently embedded as an srgbClr val attribute, producing corrupt OOXML that PowerPoint would repair by stripping entire slides. Bug reproduction: LLM passes gradient XML or background XML as a 'color' parameter → hexColor uppercases it → solidFill embeds it as <a:srgbClr val='<P:BG>...'/> → PowerPoint repair strips the slide. Fix: hexColor() now validates input against /^#?[0-9A-Fa-f]{6}$/ and throws a descriptive error on invalid input, matching the same regex used by requireHex(). Error message truncates long strings to avoid dumping XML fragments into the console. Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
1 parent 7ef5d60 commit 8adb60f

3 files changed

Lines changed: 22 additions & 6 deletions

File tree

builtin-modules/doc-core.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
"description": "Format-agnostic document infrastructure — themes, colour validation, contrast utilities, input guards. Used by ha:pptx, ha:pdf, and other format modules.",
44
"author": "system",
55
"mutable": false,
6-
"sourceHash": "sha256:b9119a600839812d",
7-
"dtsHash": "sha256:1f311b99f56fdcbb",
6+
"sourceHash": "sha256:f17eeec053c65a77",
7+
"dtsHash": "sha256:4bbfced3ab780ce8",
88
"importStyle": "named",
99
"hints": {
1010
"overview": "Shared utilities for all document formats. Provides themes, colour validation (WCAG AA contrast), and input guards. You rarely import this directly — ha:pptx and ha:pdf re-use it internally.",

builtin-modules/src/doc-core.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,24 @@
1515
/**
1616
* Convert a hex colour string to normalised format (strip leading #, uppercase).
1717
* This is the **lenient** version — it does NOT throw on bad input.
18-
* Prefer `requireHex()` at public API boundaries; this is kept for
19-
* internal paths where the value has already been validated.
18+
* Normalise and validate a hex colour string.
19+
*
20+
* Throws on invalid input (non-hex strings, XML fragments, named
21+
* colours, rgb() notation, etc.) to prevent malformed OOXML output.
22+
* Prefer `requireHex()` at public API boundaries for more descriptive
23+
* error messages with parameter names.
2024
*
2125
* @param hex - Colour like "#2196F3" or "2196F3"
2226
* @returns Normalised colour like "2196F3"
27+
* @throws {Error} If hex is not a valid 6-character hex colour
2328
*/
2429
export function hexColor(hex: string): string {
30+
if (typeof hex !== "string" || !/^#?[0-9A-Fa-f]{6}$/.test(hex)) {
31+
throw new Error(
32+
`Invalid hex colour: "${typeof hex === "string" && hex.length > 30 ? hex.slice(0, 30) + "..." : hex}". ` +
33+
`Expected a 6-character hex string like "2196F3" or "#FF9800".`,
34+
);
35+
}
2536
return hex.replace(/^#/, "").toUpperCase();
2637
}
2738

builtin-modules/src/types/ha-modules.d.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,16 @@ declare module "ha:doc-core" {
4545
/**
4646
* Convert a hex colour string to normalised format (strip leading #, uppercase).
4747
* This is the **lenient** version — it does NOT throw on bad input.
48-
* Prefer `requireHex()` at public API boundaries; this is kept for
49-
* internal paths where the value has already been validated.
48+
* Normalise and validate a hex colour string.
49+
*
50+
* Throws on invalid input (non-hex strings, XML fragments, named
51+
* colours, rgb() notation, etc.) to prevent malformed OOXML output.
52+
* Prefer `requireHex()` at public API boundaries for more descriptive
53+
* error messages with parameter names.
5054
*
5155
* @param hex - Colour like "#2196F3" or "2196F3"
5256
* @returns Normalised colour like "2196F3"
57+
* @throws {Error} If hex is not a valid 6-character hex colour
5358
*/
5459
export declare function hexColor(hex: string): string;
5560
/**

0 commit comments

Comments
 (0)