dompurify 3.4.15 → 3.4.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -7
- package/dist/purify.cjs.d.ts +398 -405
- package/dist/purify.cjs.js +2459 -2642
- package/dist/purify.cjs.js.map +1 -1
- package/dist/purify.es.d.mts +399 -404
- package/dist/purify.es.mjs +2458 -2640
- package/dist/purify.es.mjs.map +1 -1
- package/dist/purify.js +2543 -2729
- package/dist/purify.js.map +1 -1
- package/dist/purify.min.js +7 -3
- package/dist/purify.min.js.map +1 -1
- package/package.json +22 -25
- package/src/purify.ts +506 -347
- package/src/utils.ts +36 -0
package/src/purify.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
arrayLastIndexOf,
|
|
15
15
|
arrayPop,
|
|
16
16
|
arrayPush,
|
|
17
|
+
arraySlice,
|
|
17
18
|
arraySplice,
|
|
18
19
|
stringMatch,
|
|
19
20
|
stringReplace,
|
|
@@ -22,8 +23,12 @@ import {
|
|
|
22
23
|
stringIndexOf,
|
|
23
24
|
stringTrim,
|
|
24
25
|
regExpTest,
|
|
26
|
+
setAdd,
|
|
27
|
+
setCreate,
|
|
28
|
+
setHas,
|
|
25
29
|
isRegex,
|
|
26
30
|
typeErrorCreate,
|
|
31
|
+
lookupAccessor,
|
|
27
32
|
lookupGetter,
|
|
28
33
|
create,
|
|
29
34
|
objectHasOwnProperty,
|
|
@@ -75,10 +80,11 @@ const NODE_TYPE = {
|
|
|
75
80
|
`<style>...</style><img onerror=x>` built as a node, which the literal
|
|
76
81
|
serializer emits verbatim for the HTML parser to re-open.
|
|
77
82
|
Shape (a) is handled by the firstElementChild branch in _isUnsafeNode; shape
|
|
78
|
-
(b)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
innerHTML
|
|
83
|
+
(b), and any other markup in the text, by the ELEMENT_MARKUP_PROBE branch next
|
|
84
|
+
to it. Both read textContent (the raw-serialized form for these elements)
|
|
85
|
+
rather than innerHTML, because an XML/XHTML working document serializes
|
|
86
|
+
innerHTML with `<` escaped, which silently blinds the innerHTML-based probes
|
|
87
|
+
(rule 1's second probe and FALLBACK_TAG_CLOSE) there.
|
|
82
88
|
`script` is never allow-listed, but is kept here so the guard matches the
|
|
83
89
|
serializer's own literal-text list exactly. */
|
|
84
90
|
const LITERAL_TEXT_ELEMENT_NAMES = [
|
|
@@ -93,20 +99,6 @@ const LITERAL_TEXT_ELEMENT_NAMES = [
|
|
|
93
99
|
];
|
|
94
100
|
const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, LITERAL_TEXT_ELEMENT_NAMES));
|
|
95
101
|
|
|
96
|
-
/* Per-element end-tag matcher. On an HTML reparse the ONLY token that
|
|
97
|
-
terminates a literal-text element's raw content is its own end tag; a foreign
|
|
98
|
-
literal-text close (e.g. `</xmp>` sitting inside `<style>`) does not break
|
|
99
|
-
out, so matching is per-element, not a shared alternation. The lookahead
|
|
100
|
-
requires an HTML tag-name terminator (whitespace, `/` or `>`) so a longer
|
|
101
|
-
name such as `</styles` is not mistaken for `</style`. */
|
|
102
|
-
const LITERAL_TEXT_CLOSE = (function (): Record<string, RegExp> {
|
|
103
|
-
const map: Record<string, RegExp> = {};
|
|
104
|
-
arrayForEach(LITERAL_TEXT_ELEMENT_NAMES, (name) => {
|
|
105
|
-
map[name] = seal(new RegExp('</' + name + '(?=[\\t\\n\\f\\r />])', 'i'));
|
|
106
|
-
});
|
|
107
|
-
return freeze(map);
|
|
108
|
-
})();
|
|
109
|
-
|
|
110
102
|
const getGlobal = function (): WindowLike {
|
|
111
103
|
return typeof window === 'undefined' ? null : window;
|
|
112
104
|
};
|
|
@@ -303,6 +295,17 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
303
295
|
return getNodeName ? getNodeName(node) : (node as any).nodeName;
|
|
304
296
|
};
|
|
305
297
|
|
|
298
|
+
/* Element namespace through the cached Element.prototype getter, so a live
|
|
299
|
+
node's own-property override (Object.defineProperty) cannot steer
|
|
300
|
+
namespace decisions; direct read where the getter does not exist. Only
|
|
301
|
+
call this on Elements - the getter throws on other node types. */
|
|
302
|
+
const getNamespaceURI = lookupAccessor(ElementPrototype, 'namespaceURI');
|
|
303
|
+
const _readNamespaceURI = function (element: Element): string | null {
|
|
304
|
+
return getNamespaceURI
|
|
305
|
+
? getNamespaceURI(element)
|
|
306
|
+
: (element as any).namespaceURI;
|
|
307
|
+
};
|
|
308
|
+
|
|
306
309
|
// As per issue #47, the web-components registry is inherited by a
|
|
307
310
|
// new document created via createHTMLDocument. As per the spec
|
|
308
311
|
// (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
|
|
@@ -576,6 +579,9 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
576
579
|
* of importing it into a new Document and returning a sanitized copy */
|
|
577
580
|
let IN_PLACE = false;
|
|
578
581
|
|
|
582
|
+
/* IN_PLACE: nodes processed during the current call */
|
|
583
|
+
let IN_PLACE_VISITED: Set<unknown> | null = null;
|
|
584
|
+
|
|
579
585
|
/* Allow usage of profiles like html, svg and mathMl */
|
|
580
586
|
let USE_PROFILES: UseProfilesConfig | false = {};
|
|
581
587
|
|
|
@@ -1152,40 +1158,50 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1152
1158
|
* return. Return true otherwise.
|
|
1153
1159
|
*/
|
|
1154
1160
|
const _checkValidNamespace = function (element: Element): boolean {
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1161
|
+
/* Read names and namespaces through the cached prototype getters, never
|
|
1162
|
+
the instance properties. A live IN_PLACE node can carry own-property
|
|
1163
|
+
overrides of tagName / namespaceURI (Object.defineProperty) that would
|
|
1164
|
+
otherwise steer the classification below: a namespace-confused node
|
|
1165
|
+
posing as a plain HTML element, or a parent posing as an integration
|
|
1166
|
+
point. Same root cause as GHSA-x4vx-rjvf-j5p4 (nodeName); for elements
|
|
1167
|
+
nodeName is the tagName. */
|
|
1168
|
+
const elementNamespace = _readNamespaceURI(element);
|
|
1169
|
+
const tagName = stringToLowerCase(_readNodeName(element));
|
|
1170
|
+
|
|
1171
|
+
// A missing parent (JSDOM inside shadow DOM) or a non-element parent
|
|
1172
|
+
// (DocumentFragment, ShadowRoot, Document) is simulated as a <template>
|
|
1173
|
+
// in the configured namespace, as before.
|
|
1174
|
+
const parentNode = getParentNode(element);
|
|
1175
|
+
let parent: { namespaceURI: string | null };
|
|
1176
|
+
let parentTagName: string;
|
|
1177
|
+
if (parentNode && _readNodeType(parentNode) === NODE_TYPE.element) {
|
|
1178
|
+
parent = { namespaceURI: _readNamespaceURI(parentNode) };
|
|
1179
|
+
parentTagName = stringToLowerCase(_readNodeName(parentNode));
|
|
1180
|
+
} else {
|
|
1181
|
+
parent = { namespaceURI: NAMESPACE };
|
|
1182
|
+
parentTagName = 'template';
|
|
1164
1183
|
}
|
|
1165
1184
|
|
|
1166
|
-
|
|
1167
|
-
const parentTagName = stringToLowerCase(parent.tagName);
|
|
1168
|
-
|
|
1169
|
-
if (!ALLOWED_NAMESPACES[element.namespaceURI]) {
|
|
1185
|
+
if (!ALLOWED_NAMESPACES[elementNamespace]) {
|
|
1170
1186
|
return false;
|
|
1171
1187
|
}
|
|
1172
1188
|
|
|
1173
|
-
if (
|
|
1189
|
+
if (elementNamespace === SVG_NAMESPACE) {
|
|
1174
1190
|
return _checkSvgNamespace(tagName, parent, parentTagName);
|
|
1175
1191
|
}
|
|
1176
1192
|
|
|
1177
|
-
if (
|
|
1193
|
+
if (elementNamespace === MATHML_NAMESPACE) {
|
|
1178
1194
|
return _checkMathMlNamespace(tagName, parent, parentTagName);
|
|
1179
1195
|
}
|
|
1180
1196
|
|
|
1181
|
-
if (
|
|
1197
|
+
if (elementNamespace === HTML_NAMESPACE) {
|
|
1182
1198
|
return _checkHtmlNamespace(tagName, parent, parentTagName);
|
|
1183
1199
|
}
|
|
1184
1200
|
|
|
1185
1201
|
// For XHTML and XML documents that support custom namespaces
|
|
1186
1202
|
if (
|
|
1187
1203
|
PARSER_MEDIA_TYPE === 'application/xhtml+xml' &&
|
|
1188
|
-
ALLOWED_NAMESPACES[
|
|
1204
|
+
ALLOWED_NAMESPACES[elementNamespace]
|
|
1189
1205
|
) {
|
|
1190
1206
|
return true;
|
|
1191
1207
|
}
|
|
@@ -1271,46 +1287,29 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1271
1287
|
};
|
|
1272
1288
|
|
|
1273
1289
|
/**
|
|
1274
|
-
*
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1277
|
-
* (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
|
|
1278
|
-
* custom element's reaction detaches a node so `_forceRemove`'s deliberate
|
|
1279
|
-
* parentless guard throws, or any other re-entrant engine mutation — would
|
|
1280
|
-
* otherwise leave the caller's *live* tree half-sanitized, with everything
|
|
1281
|
-
* after the abort point still carrying its handlers. There is no safe way
|
|
1282
|
-
* to resume the walk (the tree mutated under us), so we strip the root bare:
|
|
1283
|
-
* remove every child and every attribute, then let the caller's catch see
|
|
1284
|
-
* the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
|
|
1285
|
-
* getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
|
|
1290
|
+
* Fail-closed teardown of an in-place root: strip every disallowed attribute
|
|
1291
|
+
* in the subtree first, so nothing armed is left behind, then remove all
|
|
1292
|
+
* children and the root's own attributes.
|
|
1286
1293
|
*
|
|
1287
1294
|
* @param root the in-place root to empty
|
|
1288
1295
|
*/
|
|
1289
1296
|
const _neutralizeRoot = function (root: Node): void {
|
|
1290
|
-
/* Strip every disallowed attribute (on* handlers included) off the whole
|
|
1291
|
-
subtree BEFORE detaching anything. Detaching first would hand back
|
|
1292
|
-
handler-bearing originals (e.g. an already-loading `<img onerror>`)
|
|
1293
|
-
whose queued resource event still fires in page scope after we throw.
|
|
1294
|
-
Clobber-safe reads; a doomed clobbered node's own attributes are
|
|
1295
|
-
irrelevant while its non-clobbered descendants are reached and scrubbed. */
|
|
1296
1297
|
_neutralizeSubtree(root);
|
|
1297
1298
|
|
|
1298
1299
|
const childNodes = getChildNodes(root);
|
|
1299
1300
|
if (childNodes) {
|
|
1300
|
-
|
|
1301
|
-
arrayForEach(childNodes, (child) => {
|
|
1302
|
-
arrayPush(snapshot, child);
|
|
1303
|
-
});
|
|
1304
|
-
arrayForEach(snapshot, (child) => {
|
|
1301
|
+
arrayForEach(arraySlice(childNodes), (child: Node) => {
|
|
1305
1302
|
try {
|
|
1306
1303
|
remove(child);
|
|
1307
1304
|
} catch (_) {
|
|
1308
|
-
/* Best
|
|
1305
|
+
/* Best effort */
|
|
1309
1306
|
}
|
|
1310
1307
|
});
|
|
1311
1308
|
}
|
|
1312
1309
|
|
|
1313
|
-
|
|
1310
|
+
// Only elements have attributes; the root may be a rejected fragment
|
|
1311
|
+
const attributes =
|
|
1312
|
+
_readNodeType(root) === NODE_TYPE.element ? getAttributes(root) : null;
|
|
1314
1313
|
if (attributes) {
|
|
1315
1314
|
for (let i = attributes.length - 1; i >= 0; --i) {
|
|
1316
1315
|
const attribute = attributes[i];
|
|
@@ -1323,18 +1322,9 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1323
1322
|
};
|
|
1324
1323
|
|
|
1325
1324
|
/**
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1328
|
-
*
|
|
1329
|
-
* lookup key for HTML elements in an HTML document, so they silently miss an
|
|
1330
|
-
* attribute whose stored qualified name still contains uppercase ASCII
|
|
1331
|
-
* letters. That happens when the node came from a case-preserving source
|
|
1332
|
-
* (an XML/XHTML document imported via importNode(), or createAttributeNS()),
|
|
1333
|
-
* where e.g. `ONERROR` survives the walk: the policy check lowercases to
|
|
1334
|
-
* `onerror` and rejects it, but `removeAttribute('ONERROR')` looks up
|
|
1335
|
-
* `onerror` and finds nothing. Remove the exact Attr node instead, which is
|
|
1336
|
-
* case- and namespace-exact, and fall back to name-based removal only when
|
|
1337
|
-
* the caller could not supply the node.
|
|
1325
|
+
* Removes an attribute, preferably as its exact Attr node: name-based
|
|
1326
|
+
* removal lowercases the name in HTML documents and misses case-preserved
|
|
1327
|
+
* names, e.g. from imported XHTML.
|
|
1338
1328
|
*
|
|
1339
1329
|
* @param name an Attribute name
|
|
1340
1330
|
* @param element a DOM node
|
|
@@ -1365,13 +1355,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1365
1355
|
element.removeAttribute(name);
|
|
1366
1356
|
}
|
|
1367
1357
|
} catch (_) {
|
|
1368
|
-
/* Clobbered or
|
|
1369
|
-
name-based removal so the "is" handling below still runs. Note this
|
|
1370
|
-
fallback ASCII-lowercases its lookup key in an HTML document and so
|
|
1371
|
-
cannot reach a case-preserved attribute name; the cached
|
|
1372
|
-
removeAttributeNode() above is what removes those, and a clobbered
|
|
1373
|
-
form is already removed wholesale by _isClobbered() before reaching
|
|
1374
|
-
here. */
|
|
1358
|
+
/* Clobbered or detached node: fall back to name-based removal */
|
|
1375
1359
|
try {
|
|
1376
1360
|
element.removeAttribute(name);
|
|
1377
1361
|
} catch (_) {}
|
|
@@ -1392,12 +1376,8 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1392
1376
|
};
|
|
1393
1377
|
|
|
1394
1378
|
/**
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1397
|
-
* Removes every attribute the active configuration does not allow from a
|
|
1398
|
-
* single element, using the same allowlist as the main attribute pass (so
|
|
1399
|
-
* `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to
|
|
1400
|
-
* neutralise nodes that are being discarded from an in-place tree.
|
|
1379
|
+
* Removes every attribute the configuration does not allow. Used to
|
|
1380
|
+
* neutralize nodes discarded from an in-place tree.
|
|
1401
1381
|
*
|
|
1402
1382
|
* @param element the element to strip
|
|
1403
1383
|
*/
|
|
@@ -1419,36 +1399,33 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1419
1399
|
};
|
|
1420
1400
|
|
|
1421
1401
|
/**
|
|
1422
|
-
*
|
|
1402
|
+
* Iterative depth-first walk, so deep trees cannot overflow the call stack,
|
|
1403
|
+
* reading through the cached getters, so clobbering cannot hide children.
|
|
1404
|
+
* When `visit` returns false, the node's children are skipped. With
|
|
1405
|
+
* `intoShadow`, open shadow roots are walked after their host's children.
|
|
1423
1406
|
*
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
* all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path
|
|
1428
|
-
* those dropped nodes are detached from the caller's LIVE tree but a
|
|
1429
|
-
* handler-bearing original among them (an `<img onerror>`/`<video>` that was
|
|
1430
|
-
* loading) keeps its queued resource event, which fires in page scope after
|
|
1431
|
-
* sanitize returns. This walks a removed subtree and strips every attribute
|
|
1432
|
-
* the active configuration does not allow — so `on*` handlers are cancelled
|
|
1433
|
-
* through the SAME allowlist that governs kept nodes, not a separate `/^on/`
|
|
1434
|
-
* blocklist. Run synchronously before sanitize returns, i.e. before any
|
|
1435
|
-
* queued event can fire. Hook-free by design: these nodes leave the output,
|
|
1436
|
-
* so firing attribute hooks for them would be surprising. Clobber-safe reads;
|
|
1437
|
-
* a doomed clobbered node may shadow `removeAttribute` (its own attributes are
|
|
1438
|
-
* irrelevant — it is discarded — while its non-clobbered descendants, e.g.
|
|
1439
|
-
* the `<img>`, are reached and scrubbed).
|
|
1440
|
-
*
|
|
1441
|
-
* @param root the root of a removed subtree to neutralise
|
|
1407
|
+
* @param root where to start
|
|
1408
|
+
* @param visit called for every node
|
|
1409
|
+
* @param intoShadow whether to enter open shadow roots
|
|
1442
1410
|
*/
|
|
1443
|
-
const
|
|
1411
|
+
const _walkTree = function (
|
|
1412
|
+
root: Node,
|
|
1413
|
+
visit: (node: Node) => boolean | void,
|
|
1414
|
+
intoShadow = false
|
|
1415
|
+
): void {
|
|
1444
1416
|
const stack: Node[] = [root];
|
|
1445
1417
|
|
|
1446
1418
|
while (stack.length > 0) {
|
|
1447
1419
|
const node = stack.pop();
|
|
1448
|
-
|
|
1420
|
+
if (visit(node) === false) {
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1449
1423
|
|
|
1450
|
-
if (
|
|
1451
|
-
|
|
1424
|
+
if (intoShadow && _readNodeType(node) === NODE_TYPE.element) {
|
|
1425
|
+
const shadowRoot = getShadowRoot(node);
|
|
1426
|
+
if (_isDocumentFragment(shadowRoot)) {
|
|
1427
|
+
stack.push(shadowRoot);
|
|
1428
|
+
}
|
|
1452
1429
|
}
|
|
1453
1430
|
|
|
1454
1431
|
const childNodes = getChildNodes(node);
|
|
@@ -1461,46 +1438,191 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1461
1438
|
};
|
|
1462
1439
|
|
|
1463
1440
|
/**
|
|
1464
|
-
*
|
|
1441
|
+
* Strips disallowed attributes from every element of a discarded subtree,
|
|
1442
|
+
* so a handler that is already queued (a loading image) cannot fire.
|
|
1465
1443
|
*
|
|
1466
|
-
*
|
|
1467
|
-
|
|
1444
|
+
* @param root the root of a removed subtree
|
|
1445
|
+
*/
|
|
1446
|
+
const _neutralizeSubtree = function (root: Node): void {
|
|
1447
|
+
_walkTree(root, (node) => {
|
|
1448
|
+
if (_readNodeType(node) === NODE_TYPE.element) {
|
|
1449
|
+
_stripDisallowedAttributes(node as Element);
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* All nodes under `root`, root first, including open shadow roots. Template
|
|
1456
|
+
* content is not entered, it is inert.
|
|
1468
1457
|
*
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1458
|
+
* @param root the in-place root
|
|
1459
|
+
* @return the nodes
|
|
1460
|
+
*/
|
|
1461
|
+
const _collectLiveNodes = function (root: Node): Node[] {
|
|
1462
|
+
const nodes: Node[] = [];
|
|
1463
|
+
_walkTree(
|
|
1464
|
+
root,
|
|
1465
|
+
(node) => {
|
|
1466
|
+
nodes.push(node);
|
|
1467
|
+
},
|
|
1468
|
+
true
|
|
1469
|
+
);
|
|
1470
|
+
return nodes;
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* Strips disallowed attributes from every element that was under the
|
|
1475
|
+
* in-place root when the call started and no longer is, whatever removed
|
|
1476
|
+
* or moved it.
|
|
1479
1477
|
*
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1478
|
+
* @param root the in-place root
|
|
1479
|
+
* @param before the root's nodes, collected before the walk
|
|
1480
|
+
*/
|
|
1481
|
+
const _neutralizeEscapedElements = function (
|
|
1482
|
+
root: Node,
|
|
1483
|
+
before: Node[]
|
|
1484
|
+
): void {
|
|
1485
|
+
const reachable = setCreate();
|
|
1486
|
+
arrayForEach(_collectLiveNodes(root), (node: Node) => {
|
|
1487
|
+
if (_readNodeType(node) === NODE_TYPE.element) {
|
|
1488
|
+
setAdd(reachable, node);
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
arrayForEach(before, (node: Node) => {
|
|
1492
|
+
if (
|
|
1493
|
+
_readNodeType(node) === NODE_TYPE.element &&
|
|
1494
|
+
!setHas(reachable, node)
|
|
1495
|
+
) {
|
|
1496
|
+
_stripDisallowedAttributes(node as Element);
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* Neutralizes everything that left the in-place tree during the call: what
|
|
1503
|
+
* DOMPurify removed, and what hooks or custom elements removed or moved out.
|
|
1504
|
+
*
|
|
1505
|
+
* @param root the in-place root
|
|
1506
|
+
* @param before the root's nodes, collected before the walk
|
|
1507
|
+
* @return true if the root itself was removed
|
|
1508
|
+
*/
|
|
1509
|
+
const _neutralizeDiscarded = function (root: Node, before: Node[]): boolean {
|
|
1510
|
+
let rootRemoved = false;
|
|
1511
|
+
arrayForEach(DOMPurify.removed, (entry) => {
|
|
1512
|
+
if (entry.element) {
|
|
1513
|
+
rootRemoved = rootRemoved || entry.element === root;
|
|
1514
|
+
_neutralizeSubtree(entry.element as Node);
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
_neutralizeEscapedElements(root, before);
|
|
1518
|
+
return rootRemoved;
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1521
|
+
const MAX_UNVISITED_ROUNDS = 8;
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* The walk only moves forward, so a node moved to a position it already
|
|
1525
|
+
* passed is never visited. Give every original node that is still in the
|
|
1526
|
+
* tree but unvisited the normal treatment, hooks included. Nodes created
|
|
1527
|
+
* during the call are left alone. Fails closed if the tree keeps changing.
|
|
1485
1528
|
*
|
|
1486
|
-
*
|
|
1487
|
-
*
|
|
1529
|
+
* @param root the in-place root
|
|
1530
|
+
* @param before the root's nodes, collected before the walk
|
|
1531
|
+
*/
|
|
1532
|
+
const _sanitizeUnvisitedNodes = function (root: Node, before: Node[]): void {
|
|
1533
|
+
const visited = IN_PLACE_VISITED;
|
|
1534
|
+
/* Built lazily: usually every live node was visited */
|
|
1535
|
+
let original: Set<unknown> | null = null;
|
|
1536
|
+
const getOriginal = (): Set<unknown> => {
|
|
1537
|
+
if (!original) {
|
|
1538
|
+
original = setCreate();
|
|
1539
|
+
arrayForEach(before, (beforeNode: Node) => {
|
|
1540
|
+
setAdd(original, beforeNode);
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
return original;
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
for (let round = 0; ; round++) {
|
|
1548
|
+
const live = _collectLiveNodes(root);
|
|
1549
|
+
const containers: Node[] = [root];
|
|
1550
|
+
let pending = false;
|
|
1551
|
+
|
|
1552
|
+
arrayForEach(live, (node: Node) => {
|
|
1553
|
+
if (_readNodeType(node) === NODE_TYPE.documentFragment) {
|
|
1554
|
+
/* An open shadow root under the root */
|
|
1555
|
+
arrayPush(containers, node);
|
|
1556
|
+
} else if (!pending && !setHas(visited, node)) {
|
|
1557
|
+
pending = setHas(getOriginal(), node);
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
if (!pending) {
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
if (round >= MAX_UNVISITED_ROUNDS) {
|
|
1566
|
+
throw typeErrorCreate(
|
|
1567
|
+
'the tree kept changing during in-place sanitization; ' +
|
|
1568
|
+
'refusing to return it'
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
arrayForEach(containers, (container: Node, index: number) => {
|
|
1573
|
+
_sanitizeUnvisitedIn(container, index > 0, root, getOriginal());
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* One _sanitizeUnvisitedNodes pass over the root or an open shadow root.
|
|
1488
1580
|
*
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1491
|
-
*
|
|
1492
|
-
*
|
|
1493
|
-
|
|
1494
|
-
|
|
1581
|
+
* @param container the root or an open shadow root
|
|
1582
|
+
* @param inShadow whether container is a shadow root
|
|
1583
|
+
* @param root the in-place root
|
|
1584
|
+
* @param original the root's nodes collected before the walk, as a set
|
|
1585
|
+
*/
|
|
1586
|
+
const _sanitizeUnvisitedIn = function (
|
|
1587
|
+
container: Node,
|
|
1588
|
+
inShadow: boolean,
|
|
1589
|
+
root: Node,
|
|
1590
|
+
original: Set<unknown>
|
|
1591
|
+
): void {
|
|
1592
|
+
const iterator = _createNodeIterator(container);
|
|
1593
|
+
let node: Node | null;
|
|
1594
|
+
while ((node = iterator.nextNode())) {
|
|
1595
|
+
if (!setHas(original, node) || setHas(IN_PLACE_VISITED, node)) {
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (inShadow) {
|
|
1600
|
+
_executeHooks(hooks.uponSanitizeShadowNode, node, null);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
_sanitizeElements(node, root);
|
|
1604
|
+
_sanitizeAttributes(node as Element, root);
|
|
1605
|
+
if (_isDocumentFragment((node as HTMLTemplateElement).content)) {
|
|
1606
|
+
_sanitizeShadowDOM((node as HTMLTemplateElement).content);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
/**
|
|
1612
|
+
* Records that _sanitizeElements processed `node`. No-op outside IN_PLACE.
|
|
1495
1613
|
*
|
|
1496
|
-
* @param
|
|
1614
|
+
* @param node the node being processed
|
|
1497
1615
|
*/
|
|
1616
|
+
const _markVisited = function (node: Node): void {
|
|
1617
|
+
if (IN_PLACE_VISITED) {
|
|
1618
|
+
setAdd(IN_PLACE_VISITED, node);
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1498
1622
|
/**
|
|
1499
|
-
*
|
|
1500
|
-
*
|
|
1501
|
-
*
|
|
1502
|
-
* everywhere except on <label>/<output>, and the whole policy is gated on
|
|
1503
|
-
* SAFE_FOR_XML (see the rationale block in _isValidAttribute).
|
|
1623
|
+
* Patch-linkage attributes (declarative partial updates): `patchsrc`
|
|
1624
|
+
* always, `for` everywhere except on <label> and <output>. Gated on
|
|
1625
|
+
* SAFE_FOR_XML. Shared by the IN_PLACE pre-pass and _isValidAttribute.
|
|
1504
1626
|
*
|
|
1505
1627
|
* @param lcName the transformCaseFunc'd attribute name
|
|
1506
1628
|
* @param lcTag the transformCaseFunc'd tag name of the carrying element
|
|
@@ -1521,18 +1643,26 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1521
1643
|
return lcName === 'for' && lcTag !== 'label' && lcTag !== 'output';
|
|
1522
1644
|
};
|
|
1523
1645
|
|
|
1646
|
+
/**
|
|
1647
|
+
* IN_PLACE pre-pass: sever all patch linkage before the walk, so no patch
|
|
1648
|
+
* can fire mid-walk into a part of the tree the walker already passed.
|
|
1649
|
+
* Removes range markers (processing instructions, comments with markup)
|
|
1650
|
+
* and strips linkage attributes. It cannot undo a patch that already fired.
|
|
1651
|
+
*
|
|
1652
|
+
* TODO: if the shipped feature can patch through `for` on <label>/<output>,
|
|
1653
|
+
* drop that pair here too (see test/declarative-patch-probe.html).
|
|
1654
|
+
*
|
|
1655
|
+
* @param root the in-place root to sweep
|
|
1656
|
+
*/
|
|
1524
1657
|
const _neutralizePatchLinkage = function (root: Node): void {
|
|
1525
1658
|
if (!SAFE_FOR_XML) {
|
|
1526
1659
|
return;
|
|
1527
1660
|
}
|
|
1528
1661
|
|
|
1529
|
-
|
|
1530
|
-
while (stack.length > 0) {
|
|
1531
|
-
const node = stack.pop();
|
|
1662
|
+
_walkTree(root, (node) => {
|
|
1532
1663
|
const nodeType = _readNodeType(node);
|
|
1533
1664
|
|
|
1534
|
-
|
|
1535
|
-
processing instruction, and any markup-bearing comment. */
|
|
1665
|
+
// Range markers: processing instructions, comments with markup
|
|
1536
1666
|
if (
|
|
1537
1667
|
nodeType === NODE_TYPE.processingInstruction ||
|
|
1538
1668
|
(nodeType === NODE_TYPE.comment &&
|
|
@@ -1541,13 +1671,13 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1541
1671
|
try {
|
|
1542
1672
|
remove(node);
|
|
1543
1673
|
} catch (_) {
|
|
1544
|
-
/* Best
|
|
1674
|
+
/* Best effort */
|
|
1545
1675
|
}
|
|
1546
1676
|
|
|
1547
|
-
|
|
1677
|
+
return false;
|
|
1548
1678
|
}
|
|
1549
1679
|
|
|
1550
|
-
|
|
1680
|
+
// Linkage attributes
|
|
1551
1681
|
if (nodeType === NODE_TYPE.element) {
|
|
1552
1682
|
const element = node as Element;
|
|
1553
1683
|
const lcTag = transformCaseFunc(_readNodeName(node));
|
|
@@ -1564,17 +1694,12 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1564
1694
|
element.removeAttribute('for');
|
|
1565
1695
|
}
|
|
1566
1696
|
} catch (_) {
|
|
1567
|
-
/* Clobbered
|
|
1697
|
+
/* Clobbered methods on a doomed node */
|
|
1568
1698
|
}
|
|
1569
1699
|
}
|
|
1570
1700
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
for (let i = childNodes.length - 1; i >= 0; --i) {
|
|
1574
|
-
stack.push(childNodes[i]);
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1701
|
+
return true;
|
|
1702
|
+
});
|
|
1578
1703
|
};
|
|
1579
1704
|
|
|
1580
1705
|
/**
|
|
@@ -1715,6 +1840,36 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1715
1840
|
* @param node The root element whose character data should be scrubbed.
|
|
1716
1841
|
*/
|
|
1717
1842
|
const _scrubTemplateExpressions = function (node: Element): void {
|
|
1843
|
+
_scrubOwnCharacterData(node);
|
|
1844
|
+
|
|
1845
|
+
// NodeIterator does not descend into <template>.content per the DOM spec,
|
|
1846
|
+
// so we must explicitly recurse into each template's content fragment,
|
|
1847
|
+
// mirroring the approach used by _sanitizeShadowDOM.
|
|
1848
|
+
const templates = node.querySelectorAll?.('template');
|
|
1849
|
+
if (templates) {
|
|
1850
|
+
arrayForEach(templates, (tmpl: HTMLTemplateElement) => {
|
|
1851
|
+
if (_isDocumentFragment(tmpl.content)) {
|
|
1852
|
+
_scrubTemplateExpressions(tmpl.content as unknown as Element);
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
};
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* _scrubOwnCharacterData
|
|
1860
|
+
*
|
|
1861
|
+
* Non-recursive core of _scrubTemplateExpressions: normalize `node` and
|
|
1862
|
+
* strip template expressions from its own character data only. Does not
|
|
1863
|
+
* enter <template>.content or attached shadow roots (NodeIterator never
|
|
1864
|
+
* does). Used by _sanitizeShadowDOM, where every nested template content
|
|
1865
|
+
* and shadow root is already walked - and therefore scrubbed - by its own
|
|
1866
|
+
* recursive _sanitizeShadowDOM call; recursing here as well would rescrub
|
|
1867
|
+
* each nested fragment once per ancestor, which is quadratic in nesting
|
|
1868
|
+
* depth.
|
|
1869
|
+
*
|
|
1870
|
+
* @param node The root whose own character data should be scrubbed.
|
|
1871
|
+
*/
|
|
1872
|
+
const _scrubOwnCharacterData = function (node: Element): void {
|
|
1718
1873
|
node.normalize();
|
|
1719
1874
|
/* Clobber-safe ownerDocument read, same reasoning as _createNodeIterator:
|
|
1720
1875
|
under SAFE_FOR_TEMPLATES this runs on the live IN_PLACE root, which may
|
|
@@ -1736,18 +1891,6 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1736
1891
|
currentNode.data = _stripTemplateExpressions(currentNode.data);
|
|
1737
1892
|
currentNode = walker.nextNode() as CharacterData | null;
|
|
1738
1893
|
}
|
|
1739
|
-
|
|
1740
|
-
// NodeIterator does not descend into <template>.content per the DOM spec,
|
|
1741
|
-
// so we must explicitly recurse into each template's content fragment,
|
|
1742
|
-
// mirroring the approach used by _sanitizeShadowDOM.
|
|
1743
|
-
const templates = node.querySelectorAll?.('template');
|
|
1744
|
-
if (templates) {
|
|
1745
|
-
arrayForEach(templates, (tmpl: HTMLTemplateElement) => {
|
|
1746
|
-
if (_isDocumentFragment(tmpl.content)) {
|
|
1747
|
-
_scrubTemplateExpressions(tmpl.content as unknown as Element);
|
|
1748
|
-
}
|
|
1749
|
-
});
|
|
1750
|
-
}
|
|
1751
1894
|
};
|
|
1752
1895
|
|
|
1753
1896
|
/**
|
|
@@ -1914,14 +2057,31 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1914
2057
|
is why both probes here read textContent instead. Previously only
|
|
1915
2058
|
`style`-with-element-child was covered; every element in
|
|
1916
2059
|
LITERAL_TEXT_ELEMENTS shares this literal serialization and is
|
|
1917
|
-
equally affected.
|
|
2060
|
+
equally affected.
|
|
2061
|
+
|
|
2062
|
+
The text probe is rule 1's ELEMENT_MARKUP_PROBE, not just the element's
|
|
2063
|
+
own end tag. For these elements textContent is exactly what an HTML
|
|
2064
|
+
serializer emits, so in an HTML working document this is the same
|
|
2065
|
+
decision rule 1 already makes there (markup-like text is removed). Only
|
|
2066
|
+
matching the own end tag let an XML/XHTML-sourced literal-text element
|
|
2067
|
+
keep markup such as `</style><img onerror>` that the same input loses in
|
|
2068
|
+
HTML mode, and that breaks out once the output is placed in another
|
|
2069
|
+
raw-text context.
|
|
2070
|
+
|
|
2071
|
+
Namespace through the cached getter (see _checkValidNamespace), and only
|
|
2072
|
+
for Elements: a processing instruction's nodeName is its target, which
|
|
2073
|
+
can collide with a name in LITERAL_TEXT_ELEMENTS. */
|
|
1918
2074
|
if (
|
|
1919
2075
|
SAFE_FOR_XML &&
|
|
1920
|
-
currentNode.namespaceURI === HTML_NAMESPACE &&
|
|
1921
2076
|
LITERAL_TEXT_ELEMENTS[tagName] &&
|
|
2077
|
+
_readNodeType(currentNode) === NODE_TYPE.element &&
|
|
2078
|
+
_readNamespaceURI(currentNode) === HTML_NAMESPACE &&
|
|
1922
2079
|
(_isNode(currentNode.firstElementChild) ||
|
|
1923
2080
|
(typeof currentNode.textContent === 'string' &&
|
|
1924
|
-
regExpTest(
|
|
2081
|
+
regExpTest(
|
|
2082
|
+
EXPRESSIONS.ELEMENT_MARKUP_PROBE,
|
|
2083
|
+
currentNode.textContent
|
|
2084
|
+
)))
|
|
1925
2085
|
) {
|
|
1926
2086
|
return true;
|
|
1927
2087
|
}
|
|
@@ -2010,7 +2170,16 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2010
2170
|
path was dead in safe cases and a clobbering surface in unsafe
|
|
2011
2171
|
ones. Falsy cached results stay falsy; the `if (childNodes &&
|
|
2012
2172
|
parentNode)` check already gates correctly. */
|
|
2013
|
-
if (
|
|
2173
|
+
if (
|
|
2174
|
+
KEEP_CONTENT &&
|
|
2175
|
+
!FORBID_CONTENTS[tagName] &&
|
|
2176
|
+
/* The default list is lowercase, but in application/xhtml+xml mode
|
|
2177
|
+
tagName keeps its case, so `foreignObject` would miss its
|
|
2178
|
+
`foreignobject` entry and have its content hoisted instead of
|
|
2179
|
+
dropped, unlike in HTML mode. Also consult the lowercased name; this
|
|
2180
|
+
can only drop more content, never keep more. */
|
|
2181
|
+
!FORBID_CONTENTS[stringToLowerCase(tagName)]
|
|
2182
|
+
) {
|
|
2014
2183
|
const parentNode = getParentNode(currentNode);
|
|
2015
2184
|
const childNodes = getChildNodes(currentNode);
|
|
2016
2185
|
|
|
@@ -2079,35 +2248,13 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2079
2248
|
};
|
|
2080
2249
|
|
|
2081
2250
|
/**
|
|
2082
|
-
*
|
|
2083
|
-
*
|
|
2084
|
-
*
|
|
2085
|
-
* filtering). Per the cached, unclobberable parentNode getter the node is
|
|
2086
|
-
* genuinely out of the tree, so it can reach neither the serialized
|
|
2087
|
-
* output nor an IN_PLACE live tree; treat it as removed and stop
|
|
2088
|
-
* processing it. Without this guard, the unsafe-node / namespace checks
|
|
2089
|
-
* would call _forceRemove on a parentless node and hit the REPORT-3
|
|
2090
|
-
* fail-closed throw — which exists for nodes DOMPurify wants gone but
|
|
2091
|
-
* *cannot* detach (clobbered / parentless roots), the opposite of a node
|
|
2092
|
-
* that is already safely gone. The walk root is exempt: a detached
|
|
2093
|
-
* IN_PLACE root is legitimate input and must still be fully sanitized,
|
|
2094
|
-
* and a kill-decision on it must keep hitting the REPORT-3 throw.
|
|
2095
|
-
*
|
|
2096
|
-
* Nodes detached by hooks stay the hook's responsibility for placement:
|
|
2097
|
-
* they are not recorded in DOMPurify.removed, so the post-walk IN_PLACE
|
|
2098
|
-
* pass (which iterates DOMPurify.removed) does not reach them. But a
|
|
2099
|
-
* hook-detached subtree can still hold a queued resource-event handler -
|
|
2100
|
-
* e.g. an <img onload> that began loading when the caller built the live
|
|
2101
|
-
* tree - which fires in page scope after sanitize returns even though the
|
|
2102
|
-
* handler never reached the returned tree. That is the audit-5 F1 hazard,
|
|
2103
|
-
* and the documented node.remove() hook pattern walks straight into it.
|
|
2104
|
-
* So on the IN_PLACE path we neutralize the detached subtree inline,
|
|
2105
|
-
* stripping its non-allow-listed attributes before returning, exactly as
|
|
2106
|
-
* the post-walk pass does for _forceRemove'd subtrees.
|
|
2251
|
+
* A hook may have detached the node it was called for. If so, treat it as
|
|
2252
|
+
* removed and stop processing it; the walk root is exempt. On the IN_PLACE
|
|
2253
|
+
* path the detached subtree is neutralized right away.
|
|
2107
2254
|
*
|
|
2108
2255
|
* @param currentNode the node a hook may have detached
|
|
2109
2256
|
* @param root the current walk root
|
|
2110
|
-
* @return true if the node
|
|
2257
|
+
* @return true if the node was detached
|
|
2111
2258
|
*/
|
|
2112
2259
|
const _handleHookDetachedNode = function (
|
|
2113
2260
|
currentNode: Node,
|
|
@@ -2134,6 +2281,9 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2134
2281
|
* @return true if node was killed, false if left alive
|
|
2135
2282
|
*/
|
|
2136
2283
|
const _sanitizeElements = function (currentNode: any, root: Node): boolean {
|
|
2284
|
+
/* IN_PLACE: remember the node was processed (see _sanitizeUnvisitedNodes). */
|
|
2285
|
+
_markVisited(currentNode);
|
|
2286
|
+
|
|
2137
2287
|
/* Execute a hook if present */
|
|
2138
2288
|
_executeHooks(hooks.beforeSanitizeElements, currentNode, null);
|
|
2139
2289
|
|
|
@@ -2206,6 +2356,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2206
2356
|
skipped; they are removal decisions, not the hook contract. */
|
|
2207
2357
|
if (removed === false) {
|
|
2208
2358
|
_executeHooks(hooks.afterSanitizeElements, currentNode, null);
|
|
2359
|
+
|
|
2360
|
+
/* The hook may have detached the kept custom element. Same
|
|
2361
|
+
IN_PLACE hazard as the before/upon sites: neutralize the
|
|
2362
|
+
detached subtree before the walker skips past it (see
|
|
2363
|
+
_handleHookDetachedNode). */
|
|
2364
|
+
if (_handleHookDetachedNode(currentNode, root)) {
|
|
2365
|
+
return true;
|
|
2366
|
+
}
|
|
2209
2367
|
}
|
|
2210
2368
|
|
|
2211
2369
|
return removed;
|
|
@@ -2248,7 +2406,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2248
2406
|
/* Execute a hook if present */
|
|
2249
2407
|
_executeHooks(hooks.afterSanitizeElements, currentNode, null);
|
|
2250
2408
|
|
|
2251
|
-
|
|
2409
|
+
/* The afterSanitizeElements hook is the documented place for
|
|
2410
|
+
node.remove() policies, and a detach here is invisible to every later
|
|
2411
|
+
pass: the iterator advances past the detached subtree, so its
|
|
2412
|
+
descendants are never visited, and hook-detached nodes are not in
|
|
2413
|
+
DOMPurify.removed, so the post-walk IN_PLACE pass does not reach them
|
|
2414
|
+
either. Apply the same detach-neutralization invariant as the
|
|
2415
|
+
before/upon sites: true (removed) if the hook detached the node. */
|
|
2416
|
+
return _handleHookDetachedNode(currentNode, root);
|
|
2252
2417
|
};
|
|
2253
2418
|
|
|
2254
2419
|
/**
|
|
@@ -2508,6 +2673,47 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2508
2673
|
}
|
|
2509
2674
|
};
|
|
2510
2675
|
|
|
2676
|
+
/**
|
|
2677
|
+
* _isUnsafeAttributeValue
|
|
2678
|
+
*
|
|
2679
|
+
* Value-based attribute checks that the attribute walk applies before any
|
|
2680
|
+
* hook can force-keep an attribute, shared with the public
|
|
2681
|
+
* isValidAttribute() so that it never approves a value sanitize() would
|
|
2682
|
+
* strip.
|
|
2683
|
+
*
|
|
2684
|
+
* @param name the attribute name as found on the node (case preserved)
|
|
2685
|
+
* @param value the attribute value
|
|
2686
|
+
* @returns true if the attribute must be removed because of its value
|
|
2687
|
+
*/
|
|
2688
|
+
const _isUnsafeAttributeValue = function (
|
|
2689
|
+
name: string,
|
|
2690
|
+
value: string
|
|
2691
|
+
): boolean {
|
|
2692
|
+
/* Work around a security issue with comments inside attributes, and
|
|
2693
|
+
raw-text closers that re-contextualize the value on a later reparse */
|
|
2694
|
+
if (
|
|
2695
|
+
SAFE_FOR_XML &&
|
|
2696
|
+
regExpTest(
|
|
2697
|
+
/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,
|
|
2698
|
+
value
|
|
2699
|
+
)
|
|
2700
|
+
) {
|
|
2701
|
+
return true;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
/* Make sure we cannot easily use animated hrefs, even if animations are
|
|
2705
|
+
allowed. Compared case-insensitively on purpose: in
|
|
2706
|
+
application/xhtml+xml mode names keep their case, and the real SVG
|
|
2707
|
+
attribute is spelled attributeName, which 'attributename' would
|
|
2708
|
+
never match (GHSA-c6gr-qvv4-5v3f). In XHTML a lowercase
|
|
2709
|
+
`attributename` is not the SMIL attribute, so also stripping it
|
|
2710
|
+
there costs nothing. */
|
|
2711
|
+
return (
|
|
2712
|
+
stringToLowerCase(name) === 'attributename' &&
|
|
2713
|
+
stringMatch(value, 'href') !== null
|
|
2714
|
+
);
|
|
2715
|
+
};
|
|
2716
|
+
|
|
2511
2717
|
/**
|
|
2512
2718
|
* _sanitizeAttributes
|
|
2513
2719
|
*
|
|
@@ -2517,12 +2723,23 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2517
2723
|
* @protect setAttribute
|
|
2518
2724
|
*
|
|
2519
2725
|
* @param currentNode to sanitize
|
|
2726
|
+
* @param root the current walk root
|
|
2520
2727
|
*/
|
|
2521
|
-
|
|
2522
|
-
|
|
2728
|
+
const _sanitizeAttributes = function (
|
|
2729
|
+
currentNode: Element,
|
|
2730
|
+
root: Node
|
|
2731
|
+
): void {
|
|
2523
2732
|
/* Execute a hook if present */
|
|
2524
2733
|
_executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
|
|
2525
2734
|
|
|
2735
|
+
/* A hook may have detached the node - the attribute hooks can detach
|
|
2736
|
+
exactly like the element hooks can, and the walker will not revisit
|
|
2737
|
+
the detached subtree. Treat it as removed and, on the IN_PLACE path,
|
|
2738
|
+
neutralize it (see _handleHookDetachedNode for the full rationale). */
|
|
2739
|
+
if (_handleHookDetachedNode(currentNode, root)) {
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2526
2743
|
const { attributes } = currentNode;
|
|
2527
2744
|
|
|
2528
2745
|
/* Check if we have attributes; if not we might have a text node */
|
|
@@ -2595,20 +2812,9 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2595
2812
|
// Else: already prefixed, leave the attribute alone — the prefix is
|
|
2596
2813
|
// itself the clobbering protection, and re-applying it is incorrect.
|
|
2597
2814
|
|
|
2598
|
-
/*
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
regExpTest(
|
|
2602
|
-
/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,
|
|
2603
|
-
value
|
|
2604
|
-
)
|
|
2605
|
-
) {
|
|
2606
|
-
_removeAttribute(name, currentNode, attr);
|
|
2607
|
-
continue;
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
|
-
/* Make sure we cannot easily use animated hrefs, even if animations are allowed */
|
|
2611
|
-
if (lcName === 'attributename' && stringMatch(value, 'href')) {
|
|
2815
|
+
/* Value checks that apply regardless of hooks (comment / raw-text
|
|
2816
|
+
closers in the value, animated hrefs); see _isUnsafeAttributeValue. */
|
|
2817
|
+
if (_isUnsafeAttributeValue(name, value)) {
|
|
2612
2818
|
_removeAttribute(name, currentNode, attr);
|
|
2613
2819
|
continue;
|
|
2614
2820
|
}
|
|
@@ -2668,6 +2874,11 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2668
2874
|
|
|
2669
2875
|
/* Execute a hook if present */
|
|
2670
2876
|
_executeHooks(hooks.afterSanitizeAttributes, currentNode, null);
|
|
2877
|
+
|
|
2878
|
+
/* Same detach re-check as after beforeSanitizeAttributes: this is the
|
|
2879
|
+
last hook that runs for the node, so a detach here would otherwise
|
|
2880
|
+
hand the caller a live, handler-bearing subtree. */
|
|
2881
|
+
_handleHookDetachedNode(currentNode, root);
|
|
2671
2882
|
};
|
|
2672
2883
|
|
|
2673
2884
|
/**
|
|
@@ -2690,7 +2901,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2690
2901
|
_sanitizeElements(shadowNode, fragment);
|
|
2691
2902
|
|
|
2692
2903
|
/* Check attributes next */
|
|
2693
|
-
_sanitizeAttributes(shadowNode);
|
|
2904
|
+
_sanitizeAttributes(shadowNode, fragment);
|
|
2694
2905
|
|
|
2695
2906
|
/* Deep shadow DOM detected.
|
|
2696
2907
|
Realm-safe check (GHSA-hpcv-96wg-7vj8): use nodeType against the
|
|
@@ -2719,53 +2930,50 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2719
2930
|
}
|
|
2720
2931
|
}
|
|
2721
2932
|
|
|
2933
|
+
/* Post-walk template-expression safety net, mirroring the light-DOM
|
|
2934
|
+
passes on `dirty` / `body`. The per-node strip above (via
|
|
2935
|
+
_sanitizeElements) only sees each text node in isolation, so an
|
|
2936
|
+
expression split across text siblings by a disallowed separator element
|
|
2937
|
+
(e.g. `$` <object> `{alert(1)}`) survives: each fragment matches no
|
|
2938
|
+
delimiter regex on its own, and once the separator is removed the
|
|
2939
|
+
fragments become adjacent and serialize as one complete expression.
|
|
2940
|
+
The light-DOM net catches exactly this, but NodeIterator does not
|
|
2941
|
+
descend into shadow trees and node.normalize() does not merge text
|
|
2942
|
+
across the shadow boundary, so the fragments were never re-examined as
|
|
2943
|
+
a merged unit. normalize() here (inside _scrubTemplateExpressions)
|
|
2944
|
+
merges them and re-strips. Deliberately the non-recursive variant: each
|
|
2945
|
+
nested shadow root and template content is walked by its own
|
|
2946
|
+
_sanitizeShadowDOM call above and scrubbed there, so every fragment is
|
|
2947
|
+
scrubbed exactly once (the recursive variant would make this quadratic
|
|
2948
|
+
in nesting depth). Gated on SAFE_FOR_TEMPLATES like the light-DOM
|
|
2949
|
+
passes, and placed before the afterSanitizeShadowDOM hook so that hook
|
|
2950
|
+
observes the finalized fragment. */
|
|
2951
|
+
if (SAFE_FOR_TEMPLATES) {
|
|
2952
|
+
_scrubOwnCharacterData(fragment as unknown as Element);
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2722
2955
|
/* Execute a hook if present */
|
|
2723
2956
|
_executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
|
|
2724
2957
|
};
|
|
2725
2958
|
|
|
2726
2959
|
/**
|
|
2727
|
-
*
|
|
2728
|
-
*
|
|
2729
|
-
*
|
|
2730
|
-
* the existing _sanitizeShadowDOM pipeline. The default node iterator
|
|
2731
|
-
* does not descend into shadow trees, so nodes inside an attached
|
|
2732
|
-
* shadow root would otherwise be skipped entirely.
|
|
2733
|
-
*
|
|
2734
|
-
* Two real input paths put attached shadow roots in front of us:
|
|
2735
|
-
* 1. IN_PLACE on a DOM node that already has shadow roots attached.
|
|
2736
|
-
* 2. DOM-node input where importNode(dirty, true) deep-clones the
|
|
2737
|
-
* shadow root because it was created with `clonable: true`.
|
|
2738
|
-
*
|
|
2739
|
-
* This pass runs once, up front, so the main iteration loop (and the
|
|
2740
|
-
* existing _sanitizeShadowDOM template-content recursion) stay
|
|
2741
|
-
* untouched — string-input paths are not affected.
|
|
2960
|
+
* Sanitizes every attached shadow root under `root`, since the node
|
|
2961
|
+
* iterator does not enter shadow trees. Needed for IN_PLACE input and for
|
|
2962
|
+
* DOM input whose clonable shadow roots were copied by importNode().
|
|
2742
2963
|
*
|
|
2743
2964
|
* @param root the subtree root to walk for attached shadow roots
|
|
2744
2965
|
*/
|
|
2745
2966
|
const _sanitizeAttachedShadowRoots = function (root: Node): void {
|
|
2746
|
-
/*
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
throw at the IN_PLACE entry pre-pass, before a single node is
|
|
2751
|
-
sanitized, leaving the caller's live tree untouched (fail-open). See
|
|
2752
|
-
campaign-3 F4. A heap stack keeps depth off the call stack.
|
|
2753
|
-
|
|
2754
|
-
Each work item is either a node to descend into, or a deferred
|
|
2755
|
-
`_sanitizeShadowDOM` for an already-walked shadow root. The deferred
|
|
2756
|
-
form preserves the original post-order discipline: a shadow root's
|
|
2757
|
-
nested shadow roots are discovered before the outer shadow is
|
|
2758
|
-
sanitized (which may remove hosts). Pushes are in reverse of the
|
|
2759
|
-
desired processing order (LIFO): template content, then children, then
|
|
2760
|
-
the shadow-sanitize, then the shadow walk — so the order matches the
|
|
2761
|
-
previous recursion exactly. */
|
|
2967
|
+
/* Explicit stack, so deep trees cannot overflow the call stack. An item is
|
|
2968
|
+
a node to walk or a shadow root to sanitize. Items are processed in
|
|
2969
|
+
reverse push order: a shadow root is walked first (finding nested
|
|
2970
|
+
roots), then sanitized, then template content, then the children. */
|
|
2762
2971
|
const stack: Array<{ node: Node | null; shadow: DocumentFragment | null }> =
|
|
2763
2972
|
[{ node: root, shadow: null }];
|
|
2764
2973
|
|
|
2765
2974
|
while (stack.length > 0) {
|
|
2766
2975
|
const item = stack.pop();
|
|
2767
2976
|
|
|
2768
|
-
/* Deferred shadow-DOM sanitisation: runs after its subtree was walked. */
|
|
2769
2977
|
if (item.shadow) {
|
|
2770
2978
|
_sanitizeShadowDOM(item.shadow);
|
|
2771
2979
|
continue;
|
|
@@ -2775,9 +2983,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2775
2983
|
const nodeType = _readNodeType(node);
|
|
2776
2984
|
const isElement = nodeType === NODE_TYPE.element;
|
|
2777
2985
|
|
|
2778
|
-
|
|
2779
|
-
the first child is processed first. Snapshotting matters because a
|
|
2780
|
-
hook may detach siblings mid-walk. */
|
|
2986
|
+
// Children, copied now since hooks may detach siblings mid-walk
|
|
2781
2987
|
const childNodes = getChildNodes(node);
|
|
2782
2988
|
if (childNodes) {
|
|
2783
2989
|
for (let i = childNodes.length - 1; i >= 0; --i) {
|
|
@@ -2785,9 +2991,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2785
2991
|
}
|
|
2786
2992
|
}
|
|
2787
2993
|
|
|
2788
|
-
|
|
2789
|
-
"template content last" order) When the node is a <template>,
|
|
2790
|
-
descend into its content. */
|
|
2994
|
+
// Template content
|
|
2791
2995
|
if (isElement) {
|
|
2792
2996
|
const rootName = getNodeName ? getNodeName(node) : null;
|
|
2793
2997
|
if (
|
|
@@ -2801,17 +3005,10 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2801
3005
|
}
|
|
2802
3006
|
}
|
|
2803
3007
|
|
|
2804
|
-
|
|
2805
|
-
Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
|
|
2806
|
-
rather than `instanceof DocumentFragment`, which is realm-bound and
|
|
2807
|
-
silently skipped foreign-realm shadow roots (e.g.
|
|
2808
|
-
iframe.contentDocument attachShadow). */
|
|
3008
|
+
// Shadow root: walk it, then sanitize it (realm-safe nodeType check)
|
|
2809
3009
|
if (isElement) {
|
|
2810
3010
|
const sr = getShadowRoot(node);
|
|
2811
3011
|
if (_isDocumentFragment(sr)) {
|
|
2812
|
-
/* Push the deferred sanitise first so it pops after the shadow
|
|
2813
|
-
walk we push next, i.e. nested shadow roots are discovered
|
|
2814
|
-
before this one is sanitised. */
|
|
2815
3012
|
stack.push({ node: null, shadow: sr }, { node: sr, shadow: null });
|
|
2816
3013
|
}
|
|
2817
3014
|
}
|
|
@@ -2881,73 +3078,40 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2881
3078
|
/* Clean up removed elements */
|
|
2882
3079
|
DOMPurify.removed = [];
|
|
2883
3080
|
|
|
2884
|
-
/*
|
|
2885
|
-
|
|
2886
|
-
where _parseConfig is skipped on later calls: a single string call would
|
|
2887
|
-
disable in-place mode for every subsequent node call, returning a
|
|
2888
|
-
sanitized copy while leaving the caller's node — which in-place callers
|
|
2889
|
-
keep using and whose return value they ignore — unsanitized. REPORT-2. */
|
|
3081
|
+
/* Resolved per call: setConfig() skips _parseConfig, so writing IN_PLACE
|
|
3082
|
+
here would leak into later calls (REPORT-2). */
|
|
2890
3083
|
const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
|
|
3084
|
+
/* IN_PLACE: the caller's nodes before any hook or custom element can run,
|
|
3085
|
+
and the visit tracking of an outer call this one may be nested in */
|
|
3086
|
+
let inPlaceNodes: Node[] = [];
|
|
3087
|
+
const outerVisited = IN_PLACE_VISITED;
|
|
2891
3088
|
|
|
2892
3089
|
if (inPlace) {
|
|
2893
|
-
/* Declarative-partial-updates / streaming pre-pass: sever every patch
|
|
2894
|
-
linkage across the live tree BEFORE the walk, so no patch can fire
|
|
2895
|
-
mid-walk and inject into an already-processed region. Runs first, so
|
|
2896
|
-
it also covers the forbidden/clobbered roots that throw below. */
|
|
2897
3090
|
_neutralizePatchLinkage(dirty as Node);
|
|
2898
3091
|
|
|
2899
|
-
/*
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
the property and let this check skip the root-allowlist
|
|
2903
|
-
validation entirely. */
|
|
3092
|
+
/* The root must be an allowed tag and not clobbered: a parentless root
|
|
3093
|
+
cannot be removed, so it would come back unsanitized. Its name is read
|
|
3094
|
+
through the cached getter, a clobbering child could shadow it. */
|
|
2904
3095
|
const nn = _readNodeName(dirty as Node);
|
|
2905
3096
|
if (typeof nn === 'string') {
|
|
2906
3097
|
const tagName = transformCaseFunc(nn);
|
|
2907
3098
|
if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
|
|
2908
|
-
/* Fail closed on a live root: neutralize handlers/children before
|
|
2909
|
-
throwing, exactly as the mid-walk abort path does. */
|
|
2910
3099
|
_neutralizeRoot(dirty as Node);
|
|
2911
|
-
|
|
2912
3100
|
throw typeErrorCreate(
|
|
2913
3101
|
'root node is forbidden and cannot be sanitized in-place'
|
|
2914
3102
|
);
|
|
2915
3103
|
}
|
|
2916
3104
|
}
|
|
2917
3105
|
|
|
2918
|
-
/* Pre-flight the root through _isClobbered. The iterator-driven
|
|
2919
|
-
removal path can not detach a parent-less root: _forceRemove
|
|
2920
|
-
falls through to Element.prototype.remove(), which per spec
|
|
2921
|
-
is a no-op on a node with no parent. A clobbered root would
|
|
2922
|
-
then survive the main loop with its attributes uninspected,
|
|
2923
|
-
because _sanitizeAttributes early-returns on _isClobbered. The
|
|
2924
|
-
result would be an attacker-controlled form, complete with any
|
|
2925
|
-
event-handler attributes the caller passed in, handed back to
|
|
2926
|
-
the application unsanitized. Refuse to sanitize such a root
|
|
2927
|
-
the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
|
|
2928
3106
|
if (_isClobbered(dirty as Element)) {
|
|
2929
|
-
/* Fail closed on a live clobbered root before throwing.
|
|
2930
|
-
_neutralizeRoot's reads are clobber-safe (cached getters); the
|
|
2931
|
-
form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
|
|
2932
3107
|
_neutralizeRoot(dirty as Node);
|
|
2933
|
-
|
|
2934
3108
|
throw typeErrorCreate(
|
|
2935
3109
|
'root node is clobbered and cannot be sanitized in-place'
|
|
2936
3110
|
);
|
|
2937
3111
|
}
|
|
2938
3112
|
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
barrier as the main walk (campaign-3 F2): a custom-element reaction
|
|
2942
|
-
inside a shadow root could abort this pre-pass before the walk runs,
|
|
2943
|
-
which would otherwise leave the entire live tree unsanitized. */
|
|
2944
|
-
try {
|
|
2945
|
-
_sanitizeAttachedShadowRoots(dirty as Node);
|
|
2946
|
-
} catch (error) {
|
|
2947
|
-
_neutralizeRoot(dirty as Node);
|
|
2948
|
-
|
|
2949
|
-
throw error;
|
|
2950
|
-
}
|
|
3113
|
+
inPlaceNodes = _collectLiveNodes(dirty as Node);
|
|
3114
|
+
IN_PLACE_VISITED = setCreate();
|
|
2951
3115
|
} else if (_isNode(dirty)) {
|
|
2952
3116
|
/* If dirty is a DOM element, append to an empty document to avoid
|
|
2953
3117
|
elements being stripped by the parser */
|
|
@@ -3011,65 +3175,46 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
3011
3175
|
/* Get node iterator */
|
|
3012
3176
|
const walkRoot: Node = inPlace ? (dirty as Node) : body;
|
|
3013
3177
|
|
|
3014
|
-
/*
|
|
3015
|
-
|
|
3016
|
-
engine/custom-element mutation can detach a node mid-walk so
|
|
3017
|
-
`_forceRemove`'s parentless guard throws, aborting the loop. Without the
|
|
3018
|
-
barrier the caller's in-place tree would be left half-sanitized with the
|
|
3019
|
-
unvisited tail still armed. _createNodeIterator itself is inside the
|
|
3020
|
-
barrier too: constructing the iterator dereferences the root's document,
|
|
3021
|
-
and any failure there (e.g. an exotic/clobbered root) must still fail
|
|
3022
|
-
closed rather than skip the neutralize. On any throw we fail closed -
|
|
3023
|
-
strip the in-place root bare - then rethrow so the existing throw
|
|
3024
|
-
contract is preserved. (String/DOM-copy paths never return the partial
|
|
3025
|
-
body, so the propagating throw is already fail-closed there.) */
|
|
3178
|
+
/* If anything throws, an in-place tree is stripped bare before the error
|
|
3179
|
+
propagates, never left half-sanitized. */
|
|
3026
3180
|
try {
|
|
3181
|
+
if (inPlace) {
|
|
3182
|
+
_sanitizeAttachedShadowRoots(dirty as Node);
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3027
3185
|
const nodeIterator = _createNodeIterator(walkRoot);
|
|
3028
3186
|
while ((currentNode = nodeIterator.nextNode())) {
|
|
3029
|
-
/* Sanitize tags and elements */
|
|
3030
3187
|
_sanitizeElements(currentNode, walkRoot);
|
|
3188
|
+
_sanitizeAttributes(currentNode, walkRoot);
|
|
3031
3189
|
|
|
3032
|
-
|
|
3033
|
-
_sanitizeAttributes(currentNode);
|
|
3034
|
-
|
|
3035
|
-
/* Shadow DOM detected, sanitize it.
|
|
3036
|
-
Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
|
|
3037
|
-
instead of instanceof, so foreign-realm <template>.content is
|
|
3038
|
-
walked correctly. */
|
|
3190
|
+
// Template content (realm-safe nodeType check)
|
|
3039
3191
|
if (_isDocumentFragment(currentNode.content)) {
|
|
3040
3192
|
_sanitizeShadowDOM(currentNode.content);
|
|
3041
3193
|
}
|
|
3042
3194
|
}
|
|
3195
|
+
|
|
3196
|
+
if (inPlace) {
|
|
3197
|
+
_sanitizeUnvisitedNodes(dirty as Node, inPlaceNodes);
|
|
3198
|
+
}
|
|
3043
3199
|
} catch (error) {
|
|
3044
3200
|
if (inPlace) {
|
|
3045
3201
|
_neutralizeRoot(dirty as Node);
|
|
3046
|
-
|
|
3047
|
-
detached from the root, so _neutralizeRoot's subtree pass does not
|
|
3048
|
-
reach them. Defuse them too, mirroring the success-path loop below. */
|
|
3049
|
-
arrayForEach(DOMPurify.removed, (entry) => {
|
|
3050
|
-
if (entry.element) {
|
|
3051
|
-
_neutralizeSubtree(entry.element as Node);
|
|
3052
|
-
}
|
|
3053
|
-
});
|
|
3202
|
+
_neutralizeDiscarded(dirty as Node, inPlaceNodes);
|
|
3054
3203
|
}
|
|
3055
3204
|
|
|
3056
3205
|
throw error;
|
|
3206
|
+
} finally {
|
|
3207
|
+
IN_PLACE_VISITED = outerVisited;
|
|
3057
3208
|
}
|
|
3058
3209
|
|
|
3059
|
-
/* If we sanitized `dirty` in-place, return it. */
|
|
3060
3210
|
if (inPlace) {
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
arrayForEach(DOMPurify.removed, (entry) => {
|
|
3069
|
-
if (entry.element) {
|
|
3070
|
-
_neutralizeSubtree(entry.element as Node);
|
|
3071
|
-
}
|
|
3072
|
-
});
|
|
3211
|
+
// A removed root has no safe state to return
|
|
3212
|
+
if (_neutralizeDiscarded(dirty as Node, inPlaceNodes)) {
|
|
3213
|
+
throw typeErrorCreate(
|
|
3214
|
+
'a node selected for removal could not be safely returned; ' +
|
|
3215
|
+
'refusing to sanitize in place'
|
|
3216
|
+
);
|
|
3217
|
+
}
|
|
3073
3218
|
|
|
3074
3219
|
if (SAFE_FOR_TEMPLATES) {
|
|
3075
3220
|
_scrubTemplateExpressions(dirty as Element);
|
|
@@ -3163,6 +3308,20 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
3163
3308
|
|
|
3164
3309
|
const lcTag = transformCaseFunc(tag);
|
|
3165
3310
|
const lcName = transformCaseFunc(attr);
|
|
3311
|
+
|
|
3312
|
+
/* Apply the same value checks sanitize() applies to an attribute when no
|
|
3313
|
+
hook intervenes, so a value approved here is one sanitize() would also
|
|
3314
|
+
keep: comment / raw-text closers, animated hrefs, and the jQuery 3.0
|
|
3315
|
+
self-closing-tag pattern. */
|
|
3316
|
+
if (
|
|
3317
|
+
typeof value === 'string' &&
|
|
3318
|
+
(_isUnsafeAttributeValue(attr, value) ||
|
|
3319
|
+
(!ALLOW_SELF_CLOSE_IN_ATTR &&
|
|
3320
|
+
regExpTest(EXPRESSIONS.SELF_CLOSING_TAG, value)))
|
|
3321
|
+
) {
|
|
3322
|
+
return false;
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3166
3325
|
return _isValidAttribute(lcTag, lcName, value);
|
|
3167
3326
|
};
|
|
3168
3327
|
|