diff --git a/extensions/duckduckgo/src/ddg-client.ts b/extensions/duckduckgo/src/ddg-client.ts index c1e9a60c1215..a05dd40b470f 100644 --- a/extensions/duckduckgo/src/ddg-client.ts +++ b/extensions/duckduckgo/src/ddg-client.ts @@ -36,6 +36,10 @@ type DuckDuckGoResult = { snippet: string; }; +function isDecodableCodePoint(cp: number): boolean { + return Number.isInteger(cp) && cp >= 0 && cp <= 0x10ffff && (cp < 0xd800 || cp > 0xdfff); +} + function decodeHtmlEntities(text: string): string { return text.replace( /&(?:lt|gt|quot|apos|#39|#x27|#x2F|nbsp|ndash|mdash|hellip|amp|#\d+|#x[0-9a-f]+);/gi, @@ -72,10 +76,12 @@ function decodeHtmlEntities(text: string): string { return "&"; } if (normalized.startsWith("&#x")) { - return String.fromCodePoint(Number.parseInt(normalized.slice(3, -1), 16)); + const codePoint = Number.parseInt(normalized.slice(3, -1), 16); + return isDecodableCodePoint(codePoint) ? String.fromCodePoint(codePoint) : entity; } if (normalized.startsWith("&#")) { - return String.fromCodePoint(Number.parseInt(normalized.slice(2, -1), 10)); + const codePoint = Number.parseInt(normalized.slice(2, -1), 10); + return isDecodableCodePoint(codePoint) ? String.fromCodePoint(codePoint) : entity; } return entity; }, diff --git a/extensions/duckduckgo/src/ddg-search-provider.test.ts b/extensions/duckduckgo/src/ddg-search-provider.test.ts index 6640f351d0cd..8c8a0f6ceae7 100644 --- a/extensions/duckduckgo/src/ddg-search-provider.test.ts +++ b/extensions/duckduckgo/src/ddg-search-provider.test.ts @@ -205,6 +205,20 @@ describe("duckduckgo web search provider", () => { ); }); + it("leaves out-of-range numeric html entities intact instead of throwing", () => { + expect(() => ddgClientTesting.decodeHtmlEntities("Result � end")).not.toThrow(); + expect(ddgClientTesting.decodeHtmlEntities("Result � end")).toBe( + "Result � end", + ); + expect(ddgClientTesting.decodeHtmlEntities("Hex � tail")).toBe("Hex � tail"); + // Surrogate-range entities would decode to lone UTF-16 surrogates; keep them intact. + expect(ddgClientTesting.decodeHtmlEntities("Bad � end")).toBe("Bad � end"); + expect(ddgClientTesting.decodeHtmlEntities("Bad � end")).toBe("Bad � end"); + expect(ddgClientTesting.decodeHtmlEntities("Bad � end")).toBe("Bad � end"); + // A valid supplementary-plane entity still decodes. + expect(ddgClientTesting.decodeHtmlEntities("Smile 😀")).toBe("Smile 😀"); + }); + it("does not double-decode escaped entities (decodes & last)", () => { // A result whose text literally shows "<" arrives double-encoded as // "&lt;". Decoding & first would re-decode it into "<", corrupting