Skip to content

Commit 2720ac0

Browse files
authored
fix(duckduckgo): guard out-of-range numeric HTML entities (#96583)
decodeHtmlEntities decoded numeric entities with String.fromCodePoint(parseInt(...)) without a range check, so an out-of-range entity such as � or � threw RangeError and made the whole results page fail to parse. Validate the code point is within 0..0x10FFFF and keep the original entity text otherwise. Add a regression covering decimal and hex out-of-range entities plus a valid astral entity.
1 parent ce15f34 commit 2720ac0

2 files changed

Lines changed: 22 additions & 2 deletions

File tree

extensions/duckduckgo/src/ddg-client.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ type DuckDuckGoResult = {
3636
snippet: string;
3737
};
3838

39+
function isDecodableCodePoint(cp: number): boolean {
40+
return Number.isInteger(cp) && cp >= 0 && cp <= 0x10ffff && (cp < 0xd800 || cp > 0xdfff);
41+
}
42+
3943
function decodeHtmlEntities(text: string): string {
4044
return text.replace(
4145
/&(?: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 {
7276
return "&";
7377
}
7478
if (normalized.startsWith("&#x")) {
75-
return String.fromCodePoint(Number.parseInt(normalized.slice(3, -1), 16));
79+
const codePoint = Number.parseInt(normalized.slice(3, -1), 16);
80+
return isDecodableCodePoint(codePoint) ? String.fromCodePoint(codePoint) : entity;
7681
}
7782
if (normalized.startsWith("&#")) {
78-
return String.fromCodePoint(Number.parseInt(normalized.slice(2, -1), 10));
83+
const codePoint = Number.parseInt(normalized.slice(2, -1), 10);
84+
return isDecodableCodePoint(codePoint) ? String.fromCodePoint(codePoint) : entity;
7985
}
8086
return entity;
8187
},

extensions/duckduckgo/src/ddg-search-provider.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,20 @@ describe("duckduckgo web search provider", () => {
205205
);
206206
});
207207

208+
it("leaves out-of-range numeric html entities intact instead of throwing", () => {
209+
expect(() => ddgClientTesting.decodeHtmlEntities("Result &#99999999; end")).not.toThrow();
210+
expect(ddgClientTesting.decodeHtmlEntities("Result &#99999999; end")).toBe(
211+
"Result &#99999999; end",
212+
);
213+
expect(ddgClientTesting.decodeHtmlEntities("Hex &#x110000; tail")).toBe("Hex &#x110000; tail");
214+
// Surrogate-range entities would decode to lone UTF-16 surrogates; keep them intact.
215+
expect(ddgClientTesting.decodeHtmlEntities("Bad &#55296; end")).toBe("Bad &#55296; end");
216+
expect(ddgClientTesting.decodeHtmlEntities("Bad &#xD800; end")).toBe("Bad &#xD800; end");
217+
expect(ddgClientTesting.decodeHtmlEntities("Bad &#xDFFF; end")).toBe("Bad &#xDFFF; end");
218+
// A valid supplementary-plane entity still decodes.
219+
expect(ddgClientTesting.decodeHtmlEntities("Smile &#128512;")).toBe("Smile 😀");
220+
});
221+
208222
it("does not double-decode escaped entities (decodes &amp; last)", () => {
209223
// A result whose text literally shows "&lt;" arrives double-encoded as
210224
// "&amp;lt;". Decoding &amp; first would re-decode it into "<", corrupting

0 commit comments

Comments
 (0)