sanitize-html 2.16.0 → 2.17.1
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 +10 -1
- package/index.js +92 -40
- package/package.json +14 -13
package/README.md
CHANGED
|
@@ -745,7 +745,7 @@ This will transform `<disallowed>content <allowed>content</allowed> </disallowed
|
|
|
745
745
|
|
|
746
746
|
#### Escape the disallowed tag and all its children even for allowed tags.
|
|
747
747
|
|
|
748
|
-
if you set `disallowedTagsMode` to `recursiveEscape`, disallowed
|
|
748
|
+
if you set `disallowedTagsMode` to `recursiveEscape`, disallowed tags and their children will be escaped even for allowed tags:
|
|
749
749
|
|
|
750
750
|
```js
|
|
751
751
|
disallowedTagsMode: `recursiveEscape`
|
|
@@ -753,6 +753,15 @@ disallowedTagsMode: `recursiveEscape`
|
|
|
753
753
|
|
|
754
754
|
This will transform `<disallowed>hello<p>world</p></disallowed>` to `<disallowed>hello<p>world</p></disallowed>`
|
|
755
755
|
|
|
756
|
+
#### Escape the disallowed tag, including all its attributes.
|
|
757
|
+
|
|
758
|
+
By default, attributes are not preserved when tags are escaped. You can set `preserveEscapedAttributes` to `true` to
|
|
759
|
+
keep the attributes, which will also be escaped and therefore have no effect on the browser.
|
|
760
|
+
|
|
761
|
+
```js
|
|
762
|
+
preserveEscapedAttributes: true
|
|
763
|
+
```
|
|
764
|
+
|
|
756
765
|
### Ignore style attribute contents
|
|
757
766
|
|
|
758
767
|
Instead of discarding faulty style attributes, you can allow them by disabling the parsing of style attributes:
|
package/index.js
CHANGED
|
@@ -88,7 +88,8 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
let result = '';
|
|
91
|
-
// Used for hot swapping the result variable with an empty string
|
|
91
|
+
// Used for hot swapping the result variable with an empty string
|
|
92
|
+
// in order to "capture" the text written to it.
|
|
92
93
|
let tempResult = '';
|
|
93
94
|
|
|
94
95
|
function Frame(tag, attribs) {
|
|
@@ -119,7 +120,8 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
119
120
|
options.parser = Object.assign({}, htmlParserDefaults, options.parser);
|
|
120
121
|
|
|
121
122
|
const tagAllowed = function (name) {
|
|
122
|
-
return options.allowedTags === false ||
|
|
123
|
+
return options.allowedTags === false ||
|
|
124
|
+
(options.allowedTags || []).indexOf(name) > -1;
|
|
123
125
|
};
|
|
124
126
|
|
|
125
127
|
// vulnerableTags
|
|
@@ -300,7 +302,15 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
300
302
|
}
|
|
301
303
|
}
|
|
302
304
|
|
|
303
|
-
|
|
305
|
+
const isBeingEscaped = skip && (options.disallowedTagsMode === 'escape' || options.disallowedTagsMode === 'recursiveEscape');
|
|
306
|
+
const shouldPreserveEscapedAttributes = isBeingEscaped &&
|
|
307
|
+
options.preserveEscapedAttributes;
|
|
308
|
+
|
|
309
|
+
if (shouldPreserveEscapedAttributes) {
|
|
310
|
+
each(attribs, function(value, a) {
|
|
311
|
+
result += ' ' + a + '="' + escapeHtml((value || ''), true) + '"';
|
|
312
|
+
});
|
|
313
|
+
} else if (!allowedAttributesMap || has(allowedAttributesMap, name) || allowedAttributesMap['*']) {
|
|
304
314
|
each(attribs, function(value, a) {
|
|
305
315
|
if (!VALID_HTML_ATTRIBUTE_NAME.test(a)) {
|
|
306
316
|
// This prevents part of an attribute name in the output from being
|
|
@@ -308,8 +318,10 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
308
318
|
delete frame.attribs[a];
|
|
309
319
|
return;
|
|
310
320
|
}
|
|
311
|
-
// If the value is empty, check if the attribute is
|
|
312
|
-
//
|
|
321
|
+
// If the value is empty, check if the attribute is
|
|
322
|
+
// in the allowedEmptyAttributes array.
|
|
323
|
+
// If it is not in the allowedEmptyAttributes array,
|
|
324
|
+
// and it is a known non-boolean attribute, delete it
|
|
313
325
|
// List taken from https://html.spec.whatwg.org/multipage/indices.html#attributes-3
|
|
314
326
|
if (value === '' && (!options.allowedEmptyAttributes.includes(a)) &&
|
|
315
327
|
(options.nonBooleanAttributes.includes(a) || options.nonBooleanAttributes.includes('*'))) {
|
|
@@ -320,9 +332,11 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
320
332
|
// as necessary if there are specific values defined.
|
|
321
333
|
let passedAllowedAttributesMapCheck = false;
|
|
322
334
|
if (!allowedAttributesMap ||
|
|
323
|
-
(has(allowedAttributesMap, name) &&
|
|
335
|
+
(has(allowedAttributesMap, name) &&
|
|
336
|
+
allowedAttributesMap[name].indexOf(a) !== -1) ||
|
|
324
337
|
(allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1) ||
|
|
325
|
-
(has(allowedAttributesGlobMap, name) &&
|
|
338
|
+
(has(allowedAttributesGlobMap, name) &&
|
|
339
|
+
allowedAttributesGlobMap[name].test(a)) ||
|
|
326
340
|
(allowedAttributesGlobMap['*'] && allowedAttributesGlobMap['*'].test(a))) {
|
|
327
341
|
passedAllowedAttributesMapCheck = true;
|
|
328
342
|
} else if (allowedAttributesMap && allowedAttributesMap[name]) {
|
|
@@ -366,12 +380,14 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
366
380
|
const parsed = parseUrl(value);
|
|
367
381
|
|
|
368
382
|
if (options.allowedScriptHostnames || options.allowedScriptDomains) {
|
|
369
|
-
const allowedHostname = (options.allowedScriptHostnames || [])
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
383
|
+
const allowedHostname = (options.allowedScriptHostnames || [])
|
|
384
|
+
.find(function (hostname) {
|
|
385
|
+
return hostname === parsed.url.hostname;
|
|
386
|
+
});
|
|
387
|
+
const allowedDomain = (options.allowedScriptDomains || [])
|
|
388
|
+
.find(function(domain) {
|
|
389
|
+
return parsed.url.hostname === domain || parsed.url.hostname.endsWith(`.${domain}`);
|
|
390
|
+
});
|
|
375
391
|
allowed = allowedHostname || allowedDomain;
|
|
376
392
|
}
|
|
377
393
|
} catch (e) {
|
|
@@ -395,13 +411,18 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
395
411
|
allowed = has(options, 'allowIframeRelativeUrls')
|
|
396
412
|
? options.allowIframeRelativeUrls
|
|
397
413
|
: (!options.allowedIframeHostnames && !options.allowedIframeDomains);
|
|
398
|
-
} else if (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
414
|
+
} else if (
|
|
415
|
+
options.allowedIframeHostnames ||
|
|
416
|
+
options.allowedIframeDomains
|
|
417
|
+
) {
|
|
418
|
+
const allowedHostname = (options.allowedIframeHostnames || [])
|
|
419
|
+
.find(function (hostname) {
|
|
420
|
+
return hostname === parsed.url.hostname;
|
|
421
|
+
});
|
|
422
|
+
const allowedDomain = (options.allowedIframeDomains || [])
|
|
423
|
+
.find(function(domain) {
|
|
424
|
+
return parsed.url.hostname === domain || parsed.url.hostname.endsWith(`.${domain}`);
|
|
425
|
+
});
|
|
405
426
|
allowed = allowedHostname || allowedDomain;
|
|
406
427
|
}
|
|
407
428
|
} catch (e) {
|
|
@@ -455,9 +476,17 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
455
476
|
return t;
|
|
456
477
|
});
|
|
457
478
|
if (allowedSpecificClasses && allowedWildcardClasses) {
|
|
458
|
-
value = filterClasses(
|
|
479
|
+
value = filterClasses(
|
|
480
|
+
value,
|
|
481
|
+
deepmerge(allowedSpecificClasses, allowedWildcardClasses),
|
|
482
|
+
allowedClassesGlobs
|
|
483
|
+
);
|
|
459
484
|
} else {
|
|
460
|
-
value = filterClasses(
|
|
485
|
+
value = filterClasses(
|
|
486
|
+
value,
|
|
487
|
+
allowedSpecificClasses || allowedWildcardClasses,
|
|
488
|
+
allowedClassesGlobs
|
|
489
|
+
);
|
|
461
490
|
}
|
|
462
491
|
if (!value.length) {
|
|
463
492
|
delete frame.attribs[a];
|
|
@@ -468,7 +497,10 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
468
497
|
if (options.parseStyleAttributes) {
|
|
469
498
|
try {
|
|
470
499
|
const abstractSyntaxTree = postcssParse(name + ' {' + value + '}', { map: false });
|
|
471
|
-
const filteredAST = filterCss(
|
|
500
|
+
const filteredAST = filterCss(
|
|
501
|
+
abstractSyntaxTree,
|
|
502
|
+
options.allowedStyles
|
|
503
|
+
);
|
|
472
504
|
|
|
473
505
|
value = stringifyStyleAttributes(filteredAST);
|
|
474
506
|
|
|
@@ -601,7 +633,8 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
601
633
|
tempResult = '';
|
|
602
634
|
}
|
|
603
635
|
// remove the opening tag from the result
|
|
604
|
-
result = result.substring(0, frame.tagPosition) +
|
|
636
|
+
result = result.substring(0, frame.tagPosition) +
|
|
637
|
+
result.substring(frame.tagPosition + frame.openingTagLength);
|
|
605
638
|
return;
|
|
606
639
|
} else if (filterResult) {
|
|
607
640
|
result = result.substring(0, frame.tagPosition);
|
|
@@ -636,6 +669,16 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
636
669
|
parser.write(html);
|
|
637
670
|
parser.end();
|
|
638
671
|
|
|
672
|
+
if (options.disallowedTagsMode === 'escape' || options.disallowedTagsMode === 'recursiveEscape') {
|
|
673
|
+
const lastParsedIndex = parser.endIndex;
|
|
674
|
+
if (lastParsedIndex != null && lastParsedIndex >= 0 && lastParsedIndex < html.length) {
|
|
675
|
+
const unparsed = html.substring(lastParsedIndex);
|
|
676
|
+
result += escapeHtml(unparsed);
|
|
677
|
+
} else if ((lastParsedIndex == null || lastParsedIndex < 0) && html.length > 0 && result === '') {
|
|
678
|
+
result = escapeHtml(html);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
639
682
|
return result;
|
|
640
683
|
|
|
641
684
|
function initializeState() {
|
|
@@ -746,8 +789,10 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
746
789
|
* Modifies the abstractSyntaxTree object.
|
|
747
790
|
*
|
|
748
791
|
* @param {object} abstractSyntaxTree - Object representation of CSS attributes.
|
|
749
|
-
* @property {array[Declaration]} abstractSyntaxTree.nodes[0] -
|
|
750
|
-
*
|
|
792
|
+
* @property {array[Declaration]} abstractSyntaxTree.nodes[0] -
|
|
793
|
+
* Each object contains prop and value key, i.e { prop: 'color', value: 'red' }.
|
|
794
|
+
* @param {object} allowedStyles - Keys are properties (i.e color),
|
|
795
|
+
* value is list of permitted regex rules (i.e /green/i).
|
|
751
796
|
* @return {object} - The modified tree.
|
|
752
797
|
*/
|
|
753
798
|
function filterCss(abstractSyntaxTree, allowedStyles) {
|
|
@@ -769,7 +814,8 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
769
814
|
}
|
|
770
815
|
|
|
771
816
|
if (selectedRule) {
|
|
772
|
-
abstractSyntaxTree.nodes[0].nodes = astRules.nodes
|
|
817
|
+
abstractSyntaxTree.nodes[0].nodes = astRules.nodes
|
|
818
|
+
.reduce(filterDeclarations(selectedRule), []);
|
|
773
819
|
}
|
|
774
820
|
|
|
775
821
|
return abstractSyntaxTree;
|
|
@@ -780,7 +826,8 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
780
826
|
* values in the inline style attribute format.
|
|
781
827
|
*
|
|
782
828
|
* @param {AbstractSyntaxTree} filteredAST
|
|
783
|
-
* @return {string} - Example:
|
|
829
|
+
* @return {string} - Example:
|
|
830
|
+
* "color:yellow;text-align:center !important;font-family:helvetica;"
|
|
784
831
|
*/
|
|
785
832
|
function stringifyStyleAttributes(filteredAST) {
|
|
786
833
|
return filteredAST.nodes[0].nodes
|
|
@@ -797,21 +844,25 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
797
844
|
* Filters the existing attributes for the given property. Discards any attributes
|
|
798
845
|
* which don't match the allowlist.
|
|
799
846
|
*
|
|
800
|
-
* @param {object} selectedRule
|
|
801
|
-
* @param {array} allowedDeclarationsList
|
|
802
|
-
*
|
|
803
|
-
* @
|
|
804
|
-
* @property {string} attributeObject.
|
|
805
|
-
* @property {string} attributeObject.
|
|
806
|
-
* @
|
|
847
|
+
* @param {object} selectedRule - Example: { color: red, font-family: helvetica }
|
|
848
|
+
* @param {array} allowedDeclarationsList - List of declarations
|
|
849
|
+
* which pass the allowlist.
|
|
850
|
+
* @param {object} attributeObject - Object representing the current css property.
|
|
851
|
+
* @property {string} attributeObject.type - Typically 'declaration'.
|
|
852
|
+
* @property {string} attributeObject.prop - The CSS property, i.e 'color'.
|
|
853
|
+
* @property {string} attributeObject.value - The corresponding value to
|
|
854
|
+
* the css property, i.e 'red'.
|
|
855
|
+
* @return {function} - When used in Array.reduce,
|
|
856
|
+
* will return an array of Declaration objects
|
|
807
857
|
*/
|
|
808
858
|
function filterDeclarations(selectedRule) {
|
|
809
859
|
return function (allowedDeclarationsList, attributeObject) {
|
|
810
860
|
// If this property is allowlisted...
|
|
811
861
|
if (has(selectedRule, attributeObject.prop)) {
|
|
812
|
-
const matchesRegex = selectedRule[attributeObject.prop]
|
|
813
|
-
|
|
814
|
-
|
|
862
|
+
const matchesRegex = selectedRule[attributeObject.prop]
|
|
863
|
+
.some(function(regularExpression) {
|
|
864
|
+
return regularExpression.test(attributeObject.value);
|
|
865
|
+
});
|
|
815
866
|
|
|
816
867
|
if (matchesRegex) {
|
|
817
868
|
allowedDeclarationsList.push(attributeObject);
|
|
@@ -923,7 +974,8 @@ sanitizeHtml.defaults = {
|
|
|
923
974
|
allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
|
|
924
975
|
allowProtocolRelative: true,
|
|
925
976
|
enforceHtmlBoundary: false,
|
|
926
|
-
parseStyleAttributes: true
|
|
977
|
+
parseStyleAttributes: true,
|
|
978
|
+
preserveEscapedAttributes: false
|
|
927
979
|
};
|
|
928
980
|
|
|
929
981
|
sanitizeHtml.simpleTransform = function(newTagName, newAttribs, merge) {
|
|
@@ -942,7 +994,7 @@ sanitizeHtml.simpleTransform = function(newTagName, newAttribs, merge) {
|
|
|
942
994
|
|
|
943
995
|
return {
|
|
944
996
|
tagName: newTagName,
|
|
945
|
-
attribs
|
|
997
|
+
attribs
|
|
946
998
|
};
|
|
947
999
|
};
|
|
948
1000
|
};
|
package/package.json
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sanitize-html",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.1",
|
|
4
4
|
"description": "Clean up user-submitted HTML, preserving allowlisted elements and allowlisted attributes on a per-element basis",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"index.js"
|
|
9
9
|
],
|
|
10
|
-
"scripts": {
|
|
11
|
-
"test": "npx eslint . && mocha test/test.js"
|
|
12
|
-
},
|
|
13
10
|
"repository": {
|
|
14
11
|
"type": "git",
|
|
15
|
-
"url": "https://github.com/apostrophecms/
|
|
12
|
+
"url": "https://github.com/apostrophecms/apostrophe.git",
|
|
13
|
+
"directory": "packages/sanitize-html"
|
|
16
14
|
},
|
|
15
|
+
"homepage": "https://github.com/apostrophecms/apostrophe/tree/main/packages/sanitize-html#readme",
|
|
17
16
|
"keywords": [
|
|
18
17
|
"html",
|
|
19
18
|
"parser",
|
|
@@ -31,14 +30,16 @@
|
|
|
31
30
|
"postcss": "^8.3.11"
|
|
32
31
|
},
|
|
33
32
|
"devDependencies": {
|
|
34
|
-
"eslint": "^
|
|
35
|
-
"eslint-config-apostrophe": "^3.4.0",
|
|
36
|
-
"eslint-config-standard": "^14.1.1",
|
|
37
|
-
"eslint-plugin-import": "^2.25.2",
|
|
38
|
-
"eslint-plugin-node": "^11.1.0",
|
|
39
|
-
"eslint-plugin-promise": "^4.2.1",
|
|
40
|
-
"eslint-plugin-standard": "^4.0.1",
|
|
33
|
+
"eslint": "^9.39.1",
|
|
41
34
|
"mocha": "^10.2.0",
|
|
42
|
-
"sinon": "^9.0.2"
|
|
35
|
+
"sinon": "^9.0.2",
|
|
36
|
+
"eslint-config-apostrophe": "^6.0.2"
|
|
37
|
+
},
|
|
38
|
+
"apostropheTestConfig": {
|
|
39
|
+
"requiresMongo": false
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "npm run lint && mocha",
|
|
43
|
+
"lint": "eslint ."
|
|
43
44
|
}
|
|
44
45
|
}
|