diff --git a/modules/blox/blox/shared/js/components/Icon.jsx b/modules/blox/blox/shared/js/components/Icon.jsx index 45440abb..67395bc1 100644 --- a/modules/blox/blox/shared/js/components/Icon.jsx +++ b/modules/blox/blox/shared/js/components/Icon.jsx @@ -4,7 +4,7 @@ import {h} from "preact"; /** * Icon component * Renders an SVG icon from a raw SVG string passed from Hugo. - * Decodes JSON-escaped sequences (\u003c) and HTML entities (<, ", "). + * Defensively decodes any residual JSON unicode escapes and HTML entities. * * SVG sizing strategy: * - By default, icons size to 1em (matching current font size) — works universally @@ -16,18 +16,19 @@ import {h} from "preact"; export const Icon = ({svg, attributes}) => { if (!svg) return null; - // Clean the SVG string: decode HTML entities and TRIM whitespace + // Decode any residual JSON unicode escapes (e.g. \u003c → <) and HTML entities + // (e.g. < → <) that survive the JSON.parse + DOM read pipeline. + // Uses the browser's built-in HTML parser rather than hand-rolled regexes — + // this is inherently safe against double-unescaping (CWE-116). let decoded = String(svg) - .replace(/\\u003c/gi, "<") - .replace(/\\u003e/gi, ">") - .replace(/</gi, "<") - .replace(/>/gi, ">") - .replace(/"/gi, '"') - .replace(/"/gi, '"') - .replace(/\\u0026/gi, "&") - .replace(/&/gi, "&") + .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))) .trim(); + // Let the browser's HTML parser handle all entity decoding in one safe pass + const _textarea = document.createElement("textarea"); + _textarea.innerHTML = decoded; + decoded = _textarea.value; + const hasWrapper = /]/i.test(decoded); if (hasWrapper) {