sanitize-html 2.17.0 → 2.17.2
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/index.js +89 -39
- package/package.json +15 -14
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
|
|
@@ -301,7 +303,8 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
301
303
|
}
|
|
302
304
|
|
|
303
305
|
const isBeingEscaped = skip && (options.disallowedTagsMode === 'escape' || options.disallowedTagsMode === 'recursiveEscape');
|
|
304
|
-
const shouldPreserveEscapedAttributes = isBeingEscaped &&
|
|
306
|
+
const shouldPreserveEscapedAttributes = isBeingEscaped &&
|
|
307
|
+
options.preserveEscapedAttributes;
|
|
305
308
|
|
|
306
309
|
if (shouldPreserveEscapedAttributes) {
|
|
307
310
|
each(attribs, function(value, a) {
|
|
@@ -315,8 +318,10 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
315
318
|
delete frame.attribs[a];
|
|
316
319
|
return;
|
|
317
320
|
}
|
|
318
|
-
// If the value is empty, check if the attribute is
|
|
319
|
-
//
|
|
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
|
|
320
325
|
// List taken from https://html.spec.whatwg.org/multipage/indices.html#attributes-3
|
|
321
326
|
if (value === '' && (!options.allowedEmptyAttributes.includes(a)) &&
|
|
322
327
|
(options.nonBooleanAttributes.includes(a) || options.nonBooleanAttributes.includes('*'))) {
|
|
@@ -327,9 +332,11 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
327
332
|
// as necessary if there are specific values defined.
|
|
328
333
|
let passedAllowedAttributesMapCheck = false;
|
|
329
334
|
if (!allowedAttributesMap ||
|
|
330
|
-
(has(allowedAttributesMap, name) &&
|
|
335
|
+
(has(allowedAttributesMap, name) &&
|
|
336
|
+
allowedAttributesMap[name].indexOf(a) !== -1) ||
|
|
331
337
|
(allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1) ||
|
|
332
|
-
(has(allowedAttributesGlobMap, name) &&
|
|
338
|
+
(has(allowedAttributesGlobMap, name) &&
|
|
339
|
+
allowedAttributesGlobMap[name].test(a)) ||
|
|
333
340
|
(allowedAttributesGlobMap['*'] && allowedAttributesGlobMap['*'].test(a))) {
|
|
334
341
|
passedAllowedAttributesMapCheck = true;
|
|
335
342
|
} else if (allowedAttributesMap && allowedAttributesMap[name]) {
|
|
@@ -373,12 +380,14 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
373
380
|
const parsed = parseUrl(value);
|
|
374
381
|
|
|
375
382
|
if (options.allowedScriptHostnames || options.allowedScriptDomains) {
|
|
376
|
-
const allowedHostname = (options.allowedScriptHostnames || [])
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
+
});
|
|
382
391
|
allowed = allowedHostname || allowedDomain;
|
|
383
392
|
}
|
|
384
393
|
} catch (e) {
|
|
@@ -402,13 +411,18 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
402
411
|
allowed = has(options, 'allowIframeRelativeUrls')
|
|
403
412
|
? options.allowIframeRelativeUrls
|
|
404
413
|
: (!options.allowedIframeHostnames && !options.allowedIframeDomains);
|
|
405
|
-
} else if (
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
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
|
+
});
|
|
412
426
|
allowed = allowedHostname || allowedDomain;
|
|
413
427
|
}
|
|
414
428
|
} catch (e) {
|
|
@@ -462,9 +476,17 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
462
476
|
return t;
|
|
463
477
|
});
|
|
464
478
|
if (allowedSpecificClasses && allowedWildcardClasses) {
|
|
465
|
-
value = filterClasses(
|
|
479
|
+
value = filterClasses(
|
|
480
|
+
value,
|
|
481
|
+
deepmerge(allowedSpecificClasses, allowedWildcardClasses),
|
|
482
|
+
allowedClassesGlobs
|
|
483
|
+
);
|
|
466
484
|
} else {
|
|
467
|
-
value = filterClasses(
|
|
485
|
+
value = filterClasses(
|
|
486
|
+
value,
|
|
487
|
+
allowedSpecificClasses || allowedWildcardClasses,
|
|
488
|
+
allowedClassesGlobs
|
|
489
|
+
);
|
|
468
490
|
}
|
|
469
491
|
if (!value.length) {
|
|
470
492
|
delete frame.attribs[a];
|
|
@@ -475,7 +497,10 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
475
497
|
if (options.parseStyleAttributes) {
|
|
476
498
|
try {
|
|
477
499
|
const abstractSyntaxTree = postcssParse(name + ' {' + value + '}', { map: false });
|
|
478
|
-
const filteredAST = filterCss(
|
|
500
|
+
const filteredAST = filterCss(
|
|
501
|
+
abstractSyntaxTree,
|
|
502
|
+
options.allowedStyles
|
|
503
|
+
);
|
|
479
504
|
|
|
480
505
|
value = stringifyStyleAttributes(filteredAST);
|
|
481
506
|
|
|
@@ -541,6 +566,11 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
541
566
|
// your concern, don't allow them. The same is essentially true for style tags
|
|
542
567
|
// which have their own collection of XSS vectors.
|
|
543
568
|
result += text;
|
|
569
|
+
} else if ((options.disallowedTagsMode === 'discard' || options.disallowedTagsMode === 'completelyDiscard') && (nonTextTagsArray.indexOf(tag) !== -1)) {
|
|
570
|
+
// htmlparser2 does not decode entities inside raw text elements like
|
|
571
|
+
// textarea and option. The text is already properly encoded, so pass
|
|
572
|
+
// it through without additional escaping to avoid double-encoding.
|
|
573
|
+
result += text;
|
|
544
574
|
} else if (!addedText) {
|
|
545
575
|
const escaped = escapeHtml(text, false);
|
|
546
576
|
if (options.textFilter) {
|
|
@@ -608,7 +638,8 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
608
638
|
tempResult = '';
|
|
609
639
|
}
|
|
610
640
|
// remove the opening tag from the result
|
|
611
|
-
result = result.substring(0, frame.tagPosition) +
|
|
641
|
+
result = result.substring(0, frame.tagPosition) +
|
|
642
|
+
result.substring(frame.tagPosition + frame.openingTagLength);
|
|
612
643
|
return;
|
|
613
644
|
} else if (filterResult) {
|
|
614
645
|
result = result.substring(0, frame.tagPosition);
|
|
@@ -643,6 +674,17 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
643
674
|
parser.write(html);
|
|
644
675
|
parser.end();
|
|
645
676
|
|
|
677
|
+
if (options.disallowedTagsMode === 'escape' || options.disallowedTagsMode === 'recursiveEscape') {
|
|
678
|
+
const lastParsedIndex = parser.endIndex;
|
|
679
|
+
if (lastParsedIndex != null && lastParsedIndex >= 0 &&
|
|
680
|
+
lastParsedIndex < html.length) {
|
|
681
|
+
const unparsed = html.substring(lastParsedIndex);
|
|
682
|
+
result += escapeHtml(unparsed);
|
|
683
|
+
} else if ((lastParsedIndex == null || lastParsedIndex < 0) && html.length > 0 && result === '') {
|
|
684
|
+
result = escapeHtml(html);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
646
688
|
return result;
|
|
647
689
|
|
|
648
690
|
function initializeState() {
|
|
@@ -753,8 +795,10 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
753
795
|
* Modifies the abstractSyntaxTree object.
|
|
754
796
|
*
|
|
755
797
|
* @param {object} abstractSyntaxTree - Object representation of CSS attributes.
|
|
756
|
-
* @property {array[Declaration]} abstractSyntaxTree.nodes[0] -
|
|
757
|
-
*
|
|
798
|
+
* @property {array[Declaration]} abstractSyntaxTree.nodes[0] -
|
|
799
|
+
* Each object contains prop and value key, i.e { prop: 'color', value: 'red' }.
|
|
800
|
+
* @param {object} allowedStyles - Keys are properties (i.e color),
|
|
801
|
+
* value is list of permitted regex rules (i.e /green/i).
|
|
758
802
|
* @return {object} - The modified tree.
|
|
759
803
|
*/
|
|
760
804
|
function filterCss(abstractSyntaxTree, allowedStyles) {
|
|
@@ -776,7 +820,8 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
776
820
|
}
|
|
777
821
|
|
|
778
822
|
if (selectedRule) {
|
|
779
|
-
abstractSyntaxTree.nodes[0].nodes = astRules.nodes
|
|
823
|
+
abstractSyntaxTree.nodes[0].nodes = astRules.nodes
|
|
824
|
+
.reduce(filterDeclarations(selectedRule), []);
|
|
780
825
|
}
|
|
781
826
|
|
|
782
827
|
return abstractSyntaxTree;
|
|
@@ -787,7 +832,8 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
787
832
|
* values in the inline style attribute format.
|
|
788
833
|
*
|
|
789
834
|
* @param {AbstractSyntaxTree} filteredAST
|
|
790
|
-
* @return {string} - Example:
|
|
835
|
+
* @return {string} - Example:
|
|
836
|
+
* "color:yellow;text-align:center !important;font-family:helvetica;"
|
|
791
837
|
*/
|
|
792
838
|
function stringifyStyleAttributes(filteredAST) {
|
|
793
839
|
return filteredAST.nodes[0].nodes
|
|
@@ -804,21 +850,25 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
804
850
|
* Filters the existing attributes for the given property. Discards any attributes
|
|
805
851
|
* which don't match the allowlist.
|
|
806
852
|
*
|
|
807
|
-
* @param {object} selectedRule
|
|
808
|
-
* @param {array} allowedDeclarationsList
|
|
809
|
-
*
|
|
810
|
-
* @
|
|
811
|
-
* @property {string} attributeObject.
|
|
812
|
-
* @property {string} attributeObject.
|
|
813
|
-
* @
|
|
853
|
+
* @param {object} selectedRule - Example: { color: red, font-family: helvetica }
|
|
854
|
+
* @param {array} allowedDeclarationsList - List of declarations
|
|
855
|
+
* which pass the allowlist.
|
|
856
|
+
* @param {object} attributeObject - Object representing the current css property.
|
|
857
|
+
* @property {string} attributeObject.type - Typically 'declaration'.
|
|
858
|
+
* @property {string} attributeObject.prop - The CSS property, i.e 'color'.
|
|
859
|
+
* @property {string} attributeObject.value - The corresponding value to
|
|
860
|
+
* the css property, i.e 'red'.
|
|
861
|
+
* @return {function} - When used in Array.reduce,
|
|
862
|
+
* will return an array of Declaration objects
|
|
814
863
|
*/
|
|
815
864
|
function filterDeclarations(selectedRule) {
|
|
816
865
|
return function (allowedDeclarationsList, attributeObject) {
|
|
817
866
|
// If this property is allowlisted...
|
|
818
867
|
if (has(selectedRule, attributeObject.prop)) {
|
|
819
|
-
const matchesRegex = selectedRule[attributeObject.prop]
|
|
820
|
-
|
|
821
|
-
|
|
868
|
+
const matchesRegex = selectedRule[attributeObject.prop]
|
|
869
|
+
.some(function(regularExpression) {
|
|
870
|
+
return regularExpression.test(attributeObject.value);
|
|
871
|
+
});
|
|
822
872
|
|
|
823
873
|
if (matchesRegex) {
|
|
824
874
|
allowedDeclarationsList.push(attributeObject);
|
|
@@ -950,7 +1000,7 @@ sanitizeHtml.simpleTransform = function(newTagName, newAttribs, merge) {
|
|
|
950
1000
|
|
|
951
1001
|
return {
|
|
952
1002
|
tagName: newTagName,
|
|
953
|
-
attribs
|
|
1003
|
+
attribs
|
|
954
1004
|
};
|
|
955
1005
|
};
|
|
956
1006
|
};
|
package/package.json
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sanitize-html",
|
|
3
|
-
"version": "2.17.
|
|
3
|
+
"version": "2.17.2",
|
|
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",
|
|
@@ -25,20 +24,22 @@
|
|
|
25
24
|
"dependencies": {
|
|
26
25
|
"deepmerge": "^4.2.2",
|
|
27
26
|
"escape-string-regexp": "^4.0.0",
|
|
28
|
-
"htmlparser2": "^
|
|
27
|
+
"htmlparser2": "^10.1.0",
|
|
29
28
|
"is-plain-object": "^5.0.0",
|
|
30
29
|
"parse-srcset": "^1.0.2",
|
|
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
|
}
|