diff --git a/src/domain/propertyListKeys.test.ts b/src/domain/propertyListKeys.test.ts
index 759c3e454..e15fcb27e 100644
--- a/src/domain/propertyListKeys.test.ts
+++ b/src/domain/propertyListKeys.test.ts
@@ -49,6 +49,16 @@ it("keeps an entity-encoded key that decodes to the placeholder", () => {
});
});
+it("keeps entity-like text beyond Unicode in comments and CDATA", () => {
+ const text = plist(
+ "raw__proto__x",
+ );
+ expect(parseXmlPropertyList(text)).toEqual({
+ value: { raw: "" },
+ omittedPrototypeKeys: 1,
+ });
+});
+
it("omits own __proto__ entries that JSON decoding creates", () => {
const decoded: unknown = JSON.parse(
'{"__proto__":{"x":1},"a":[{"__proto__":2,"b":3}]}',
diff --git a/src/domain/propertyListKeys.ts b/src/domain/propertyListKeys.ts
index c2192f00b..0007e1e5e 100644
--- a/src/domain/propertyListKeys.ts
+++ b/src/domain/propertyListKeys.ts
@@ -56,13 +56,23 @@ const XML_ENTITIES: Readonly> = {
quot: '"',
};
+/**
+ * Expand XML character and predefined entity references. A reference beyond
+ * Unicode stays as written: comments and CDATA may hold such text literally,
+ * and the XML decoder rejects it anywhere else.
+ */
const decodeXmlText = (text: string): string =>
text.replace(
/&(?:#x([\da-f]+)|#(\d+)|([a-z]+));/giu,
(entity, hex?: string, decimal?: string, name?: string) => {
- if (hex !== undefined) return String.fromCodePoint(parseInt(hex, 16));
- if (decimal !== undefined)
- return String.fromCodePoint(parseInt(decimal, 10));
+ const codePoint =
+ hex !== undefined
+ ? parseInt(hex, 16)
+ : decimal !== undefined
+ ? parseInt(decimal, 10)
+ : undefined;
+ if (codePoint !== undefined)
+ return codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : entity;
return (name === undefined ? undefined : XML_ENTITIES[name]) ?? entity;
},
);
@@ -77,6 +87,7 @@ export const parseXmlPropertyList = (
): PropertyListWithoutPrototypeKeys => {
// The decoded text holds every key the decoder can produce, including
// entity-encoded spellings of the placeholder, so no source key aliases it.
+ // Decoding comment or CDATA text too can only lengthen the placeholder.
const decoded = decodeXmlText(text);
let placeholder = "__rea_prototype_key__";
while (decoded.includes(placeholder)) placeholder = `_${placeholder}`;