Skip to content

Commit fa239f0

Browse files
authored
Allow ADD_ATTR and ADD_TAGS to accept a function (#1150)
* feat: allow ADD_ATTR to accept a function Allow ADD_ATTR configuration option to receive a function with signature (attributeName: string, tagName: string) => boolean, enabling granular control over which attributes are allowed for specific elements. * feat: allow ADD_TAGS to accept a function Allow ADD_TAGS configuration option to receive a function with signature (tagName: string) => boolean, enabling granular control over which tags are allowed.
1 parent e715dde commit fa239f0

File tree

14 files changed

+267
-46
lines changed

14 files changed

+267
-46
lines changed

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ const clean = DOMPurify.sanitize(dirty, {SAFE_FOR_TEMPLATES: true});
175175

176176

177177
// change how e.g. comments containing risky HTML characters are treated.
178-
// be very careful, this setting should only be set to `false` if you really only handle
179-
// HTML and nothing else, no SVG, MathML or the like.
178+
// be very careful, this setting should only be set to `false` if you really only handle
179+
// HTML and nothing else, no SVG, MathML or the like.
180180
// Otherwise, changing from `true` to `false` will lead to XSS in this or some other way.
181181
const clean = DOMPurify.sanitize(dirty, {SAFE_FOR_XML: false});
182182
```
@@ -215,6 +215,24 @@ const clean = DOMPurify.sanitize(dirty, {ADD_TAGS: ['my-tag']});
215215
// extend the existing array of allowed attributes and add my-attr to allow-list
216216
const clean = DOMPurify.sanitize(dirty, {ADD_ATTR: ['my-attr']});
217217

218+
// use functions to control which additional tags and attributes are allowed
219+
const allowlist = {
220+
'one': ['attribute-one'],
221+
'two': ['attribute-two']
222+
};
223+
const clean = DOMPurify.sanitize(
224+
'<one attribute-one="1" attribute-two="2"></one><two attribute-one="1" attribute-two="2"></two>',
225+
{
226+
ADD_TAGS: (tagName) => {
227+
return Object.keys(allowlist).includes(tagName);
228+
},
229+
ADD_ATTR: (attributeName, tagName) => {
230+
231+
return allowlist[tagName]?.includes(attributeName) || false;
232+
}
233+
}
234+
); // <one attribute-one="1"></one><two attribute-two="2"></two>
235+
218236
// prohibit ARIA attributes, leave other safe HTML as is (default is true)
219237
const clean = DOMPurify.sanitize(dirty, {ALLOW_ARIA_ATTR: false});
220238

dist/purify.cjs.d.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,20 @@ import { TrustedTypePolicy, TrustedHTML, TrustedTypesWindow } from 'trusted-type
88
interface Config {
99
/**
1010
* Extend the existing array of allowed attributes.
11+
* Can be an array of attribute names, or a function that receives
12+
* the attribute name and tag name to determine if the attribute is allowed.
1113
*/
12-
ADD_ATTR?: string[] | undefined;
14+
ADD_ATTR?: string[] | ((attributeName: string, tagName: string) => boolean) | undefined;
1315
/**
1416
* Extend the existing array of elements that can use Data URIs.
1517
*/
1618
ADD_DATA_URI_TAGS?: string[] | undefined;
1719
/**
1820
* Extend the existing array of allowed tags.
21+
* Can be an array of tag names, or a function that receives
22+
* the tag name to determine if the tag is allowed.
1923
*/
20-
ADD_TAGS?: string[] | undefined;
24+
ADD_TAGS?: string[] | ((tagName: string) => boolean) | undefined;
2125
/**
2226
* Extend the existing array of elements that are safe for URI-like values (be careful, XSS risk).
2327
*/

dist/purify.cjs.js

Lines changed: 31 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/purify.cjs.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/purify.es.d.mts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,20 @@ import { TrustedTypePolicy, TrustedHTML, TrustedTypesWindow } from 'trusted-type
88
interface Config {
99
/**
1010
* Extend the existing array of allowed attributes.
11+
* Can be an array of attribute names, or a function that receives
12+
* the attribute name and tag name to determine if the attribute is allowed.
1113
*/
12-
ADD_ATTR?: string[] | undefined;
14+
ADD_ATTR?: string[] | ((attributeName: string, tagName: string) => boolean) | undefined;
1315
/**
1416
* Extend the existing array of elements that can use Data URIs.
1517
*/
1618
ADD_DATA_URI_TAGS?: string[] | undefined;
1719
/**
1820
* Extend the existing array of allowed tags.
21+
* Can be an array of tag names, or a function that receives
22+
* the tag name to determine if the tag is allowed.
1923
*/
20-
ADD_TAGS?: string[] | undefined;
24+
ADD_TAGS?: string[] | ((tagName: string) => boolean) | undefined;
2125
/**
2226
* Extend the existing array of elements that are safe for URI-like values (be careful, XSS risk).
2327
*/

dist/purify.es.mjs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,21 @@ function createDOMPurify() {
416416
let FORBID_TAGS = null;
417417
/* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */
418418
let FORBID_ATTR = null;
419+
/* Config object to store ADD_TAGS/ADD_ATTR functions (when used as functions) */
420+
const EXTRA_ELEMENT_HANDLING = Object.seal(create(null, {
421+
tagCheck: {
422+
writable: true,
423+
configurable: false,
424+
enumerable: true,
425+
value: null
426+
},
427+
attributeCheck: {
428+
writable: true,
429+
configurable: false,
430+
enumerable: true,
431+
value: null
432+
}
433+
}));
419434
/* Decide if ARIA attributes are okay */
420435
let ALLOW_ARIA_ATTR = true;
421436
/* Decide if custom data attributes are okay */
@@ -608,16 +623,24 @@ function createDOMPurify() {
608623
}
609624
/* Merge configuration parameters */
610625
if (cfg.ADD_TAGS) {
611-
if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
612-
ALLOWED_TAGS = clone(ALLOWED_TAGS);
626+
if (typeof cfg.ADD_TAGS === 'function') {
627+
EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS;
628+
} else {
629+
if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
630+
ALLOWED_TAGS = clone(ALLOWED_TAGS);
631+
}
632+
addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
613633
}
614-
addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
615634
}
616635
if (cfg.ADD_ATTR) {
617-
if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
618-
ALLOWED_ATTR = clone(ALLOWED_ATTR);
636+
if (typeof cfg.ADD_ATTR === 'function') {
637+
EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR;
638+
} else {
639+
if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
640+
ALLOWED_ATTR = clone(ALLOWED_ATTR);
641+
}
642+
addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
619643
}
620-
addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
621644
}
622645
if (cfg.ADD_URI_SAFE_ATTR) {
623646
addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
@@ -925,7 +948,7 @@ function createDOMPurify() {
925948
return true;
926949
}
927950
/* Remove element if anything forbids its presence */
928-
if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
951+
if (!(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName])) {
929952
/* Check if we have a custom element to handle */
930953
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
931954
if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
@@ -997,7 +1020,7 @@ function createDOMPurify() {
9971020
(https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
9981021
XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
9991022
We don't need to check the value; it's always URI safe. */
1000-
if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
1023+
if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
10011024
if (
10021025
// First condition does a very basic check if a) it's basically a valid custom element tagname AND
10031026
// b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck

dist/purify.es.mjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/purify.js

Lines changed: 31 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/purify.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/purify.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)