sanitize-html 1.16.3 → 1.18.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/.travis.yml +5 -0
- package/README.md +98 -2
- package/dist/index.js +106 -9
- package/dist/sanitize-html.js +1878 -75
- package/dist/sanitize-html.min.js +12 -12
- package/package.json +6 -3
- package/src/index.js +68 -13
- package/test/test.js +75 -0
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sanitize-html",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.2",
|
|
4
4
|
"description": "Clean up user-submitted HTML, preserving whitelisted elements and whitelisted attributes on a per-element basis",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"prepare": "true",
|
|
8
8
|
"build": "make clean && make all && npm run prepare && browserify dist/index.js > dist/sanitize-html.js --standalone 'sanitizeHtml'",
|
|
9
9
|
"minify": "npm run build && uglifyjs dist/sanitize-html.js > dist/sanitize-html.min.js",
|
|
10
|
-
"
|
|
11
|
-
"test": "npm run
|
|
10
|
+
"prepublishOnly": "make clean && npm run minify",
|
|
11
|
+
"test": "npm run prepublishOnly && mocha test/test.js"
|
|
12
12
|
},
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
@@ -25,9 +25,12 @@
|
|
|
25
25
|
"author": "P'unk Avenue LLC",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"chalk": "^2.3.0",
|
|
28
29
|
"htmlparser2": "^3.9.0",
|
|
29
30
|
"lodash.clonedeep": "^4.5.0",
|
|
30
31
|
"lodash.escaperegexp": "^4.1.2",
|
|
32
|
+
"lodash.isplainobject": "^4.0.6",
|
|
33
|
+
"lodash.isstring": "^4.0.1",
|
|
31
34
|
"lodash.mergewith": "^4.6.0",
|
|
32
35
|
"postcss": "^6.0.14",
|
|
33
36
|
"srcset": "^1.0.0",
|
package/src/index.js
CHANGED
|
@@ -3,8 +3,11 @@ var extend = require('xtend');
|
|
|
3
3
|
var quoteRegexp = require('lodash.escaperegexp');
|
|
4
4
|
var cloneDeep = require('lodash.clonedeep');
|
|
5
5
|
var mergeWith = require('lodash.mergewith');
|
|
6
|
+
var isString = require('lodash.isstring');
|
|
7
|
+
var isPlainObject = require('lodash.isplainobject');
|
|
6
8
|
var srcset = require('srcset');
|
|
7
9
|
var postcss = require('postcss');
|
|
10
|
+
var url = require('url');
|
|
8
11
|
|
|
9
12
|
function each(obj, cb) {
|
|
10
13
|
if (obj) Object.keys(obj).forEach(function (key) {
|
|
@@ -90,11 +93,11 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
90
93
|
each(options.allowedAttributes, function(attributes, tag) {
|
|
91
94
|
allowedAttributesMap[tag] = [];
|
|
92
95
|
var globRegex = [];
|
|
93
|
-
attributes.forEach(function(
|
|
94
|
-
if(
|
|
95
|
-
globRegex.push(quoteRegexp(
|
|
96
|
+
attributes.forEach(function(obj) {
|
|
97
|
+
if(isString(obj) && obj.indexOf('*') >= 0) {
|
|
98
|
+
globRegex.push(quoteRegexp(obj).replace(/\\\*/g, '.*'));
|
|
96
99
|
} else {
|
|
97
|
-
allowedAttributesMap[tag].push(
|
|
100
|
+
allowedAttributesMap[tag].push(obj);
|
|
98
101
|
}
|
|
99
102
|
});
|
|
100
103
|
allowedAttributesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$');
|
|
@@ -195,21 +198,73 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
195
198
|
return;
|
|
196
199
|
}
|
|
197
200
|
var parsed;
|
|
201
|
+
// check allowedAttributesMap for the element and attribute and modify the value
|
|
202
|
+
// as necessary if there are specific values defined.
|
|
203
|
+
var passedAllowedAttributesMapCheck = false;
|
|
198
204
|
if (!allowedAttributesMap ||
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
(has(allowedAttributesMap, name) && allowedAttributesMap[name].indexOf(a) !== -1 ) ||
|
|
206
|
+
(allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1 ) ||
|
|
207
|
+
(has(allowedAttributesGlobMap, name) && allowedAttributesGlobMap[name].test(a)) ||
|
|
208
|
+
(allowedAttributesGlobMap['*'] && allowedAttributesGlobMap['*'].test(a))) {
|
|
209
|
+
passedAllowedAttributesMapCheck = true;
|
|
210
|
+
} else if (allowedAttributesMap && allowedAttributesMap[name]) {
|
|
211
|
+
for (const o of allowedAttributesMap[name]) {
|
|
212
|
+
if (isPlainObject(o) && o.name && (o.name === a)) {
|
|
213
|
+
passedAllowedAttributesMapCheck = true;
|
|
214
|
+
var newValue = '';
|
|
215
|
+
if (o.multiple === true) {
|
|
216
|
+
// verify the values that are allowed
|
|
217
|
+
const splitStrArray = value.split(' ');
|
|
218
|
+
for (const s of splitStrArray) {
|
|
219
|
+
if (o.values.indexOf(s) !== -1) {
|
|
220
|
+
if (newValue === '') {
|
|
221
|
+
newValue = s;
|
|
222
|
+
} else {
|
|
223
|
+
newValue += ' ' + s;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} else if (o.values.indexOf(value) >= 0) {
|
|
228
|
+
// verified an allowed value matches the entire attribute value
|
|
229
|
+
newValue = value;
|
|
230
|
+
}
|
|
231
|
+
value = newValue;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (passedAllowedAttributesMapCheck) {
|
|
236
|
+
if (options.allowedSchemesAppliedToAttributes.indexOf(a) !== -1) {
|
|
204
237
|
if (naughtyHref(name, value)) {
|
|
205
238
|
delete frame.attribs[a];
|
|
206
239
|
return;
|
|
207
240
|
}
|
|
208
241
|
}
|
|
209
|
-
|
|
242
|
+
if (name === 'iframe' && a === 'src') {
|
|
243
|
+
//Check if value contains proper hostname prefix
|
|
244
|
+
if (value.substring(0, 2) === '//') {
|
|
245
|
+
var prefix = 'https:';
|
|
246
|
+
value = prefix.concat(value);
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
parsed = url.parse(value);
|
|
250
|
+
if (options.allowedIframeHostnames) {
|
|
251
|
+
var whitelistedHostnames = options.allowedIframeHostnames.find(function(hostname) {
|
|
252
|
+
return hostname === parsed.hostname;
|
|
253
|
+
});
|
|
254
|
+
if (!whitelistedHostnames) {
|
|
255
|
+
delete frame.attribs[a];
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch (e) {
|
|
260
|
+
// Unparseable iframe src
|
|
261
|
+
delete frame.attribs[a];
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
210
265
|
if (a === 'srcset') {
|
|
211
266
|
try {
|
|
212
|
-
|
|
267
|
+
parsed = srcset.parse(value);
|
|
213
268
|
each(parsed, function(value) {
|
|
214
269
|
if (naughtyHref('srcset', value.url)) {
|
|
215
270
|
value.evil = true;
|
|
@@ -233,7 +288,6 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
233
288
|
return;
|
|
234
289
|
}
|
|
235
290
|
}
|
|
236
|
-
|
|
237
291
|
if (a === 'class') {
|
|
238
292
|
value = filterClasses(value, allowedClassesMap[name]);
|
|
239
293
|
if (!value.length) {
|
|
@@ -498,7 +552,7 @@ var htmlParserDefaults = {
|
|
|
498
552
|
sanitizeHtml.defaults = {
|
|
499
553
|
allowedTags: [ 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
|
500
554
|
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
|
501
|
-
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre' ],
|
|
555
|
+
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'iframe' ],
|
|
502
556
|
allowedAttributes: {
|
|
503
557
|
a: [ 'href', 'name', 'target' ],
|
|
504
558
|
// We don't currently allow img itself by default, but this
|
|
@@ -511,6 +565,7 @@ sanitizeHtml.defaults = {
|
|
|
511
565
|
// URL schemes we permit
|
|
512
566
|
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
|
|
513
567
|
allowedSchemesByTag: {},
|
|
568
|
+
allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
|
|
514
569
|
allowProtocolRelative: true
|
|
515
570
|
};
|
|
516
571
|
|
package/test/test.js
CHANGED
|
@@ -647,4 +647,79 @@ describe('sanitizeHtml', function() {
|
|
|
647
647
|
}), '<span style="color:yellow;text-align:center;font-family:helvetica;"></span>'
|
|
648
648
|
);
|
|
649
649
|
});
|
|
650
|
+
it('Should allow only hostnames in an iframe that are whitelisted', function() {
|
|
651
|
+
assert.equal(
|
|
652
|
+
sanitizeHtml('<iframe src="https://www.youtube.com/embed/c2IlcS7AHxM"></iframe>', {
|
|
653
|
+
allowedTags: ['p', 'iframe', 'a', 'img', 'i'],
|
|
654
|
+
allowedAttributes: {'iframe': ['src', 'href'], 'a': ['src', 'href'], 'img': ['src']},
|
|
655
|
+
allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com']
|
|
656
|
+
}), '<iframe src="https://www.youtube.com/embed/c2IlcS7AHxM"></iframe>'
|
|
657
|
+
);
|
|
658
|
+
});
|
|
659
|
+
it('Should remove iframe src urls that are not inlcuded in whitelisted hostnames', function() {
|
|
660
|
+
assert.equal(
|
|
661
|
+
sanitizeHtml('<iframe src="https://www.embed.vevo.com/USUV71704255"></iframe>', {
|
|
662
|
+
allowedTags: ['p', 'iframe', 'a', 'img', 'i'],
|
|
663
|
+
allowedAttributes: {'iframe': ['src', 'href'], 'a': ['src', 'href'], 'img': ['src']},
|
|
664
|
+
allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com']
|
|
665
|
+
}), '<iframe></iframe>'
|
|
666
|
+
);
|
|
667
|
+
});
|
|
668
|
+
it('Should not allow iframe urls that do not have proper hostname', function() {
|
|
669
|
+
assert.equal(
|
|
670
|
+
sanitizeHtml('<iframe src="//www.vimeo.com/embed/c2IlcS7AHxM"></iframe>', {
|
|
671
|
+
allowedTags: ['p', 'iframe', 'a', 'img', 'i'],
|
|
672
|
+
allowedAttributes: {'iframe': ['src', 'href'], 'a': ['src', 'href'], 'img': ['src']},
|
|
673
|
+
allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com']
|
|
674
|
+
}), '<iframe></iframe>'
|
|
675
|
+
);
|
|
676
|
+
});
|
|
677
|
+
it('Should allow iframe through if no hostname option is set', function() {
|
|
678
|
+
assert.equal(
|
|
679
|
+
sanitizeHtml('<iframe src="https://www.vimeo.com/embed/c2IlcS7AHxM"></iframe>', {
|
|
680
|
+
allowedTags: ['p', 'iframe', 'a', 'img', 'i'],
|
|
681
|
+
allowedAttributes: {'iframe': ['src', 'href'], 'a': ['src', 'href'], 'img': ['src']}
|
|
682
|
+
}), '<iframe src="https://www.vimeo.com/embed/c2IlcS7AHxM"></iframe>'
|
|
683
|
+
);
|
|
684
|
+
});
|
|
685
|
+
it('Should only allow attributes to have any combination of specific values', function() {
|
|
686
|
+
assert.equal(
|
|
687
|
+
sanitizeHtml('<iframe name=\"IFRAME\" allowfullscreen=\"true\" sandbox=\"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts allow-top-navigation\"></iframe>',{
|
|
688
|
+
allowedTags: sanitizeHtml.defaults.allowedTags.concat( ['iframe'] ),
|
|
689
|
+
allowedAttributes: {
|
|
690
|
+
iframe: [
|
|
691
|
+
{
|
|
692
|
+
name: 'sandbox',
|
|
693
|
+
multiple: true,
|
|
694
|
+
values: ['allow-popups', 'allow-same-origin', 'allow-scripts']
|
|
695
|
+
},
|
|
696
|
+
'allowfullscreen'
|
|
697
|
+
]
|
|
698
|
+
}
|
|
699
|
+
}), '<iframe allowfullscreen=\"true\" sandbox=\"allow-popups allow-same-origin allow-scripts\"></iframe>');
|
|
700
|
+
});
|
|
701
|
+
it('Should only allow attributes that match a specific value', function() {
|
|
702
|
+
assert.equal(
|
|
703
|
+
sanitizeHtml('<iframe sandbox=\"allow-popups allow-modals\"></iframe><iframe sandbox=\"allow-popups\"></iframe><iframe sandbox=\"allow-scripts\"></iframe>',{
|
|
704
|
+
allowedTags: sanitizeHtml.defaults.allowedTags.concat( ['iframe'] ),
|
|
705
|
+
allowedAttributes: {
|
|
706
|
+
iframe: [
|
|
707
|
+
{
|
|
708
|
+
name: 'sandbox',
|
|
709
|
+
multiple: false,
|
|
710
|
+
values: ['allow-popups', 'allow-same-origin', 'allow-scripts']
|
|
711
|
+
}
|
|
712
|
+
]
|
|
713
|
+
}
|
|
714
|
+
}), '<iframe sandbox></iframe><iframe sandbox=\"allow-popups\"></iframe><iframe sandbox=\"allow-scripts\"></iframe>');
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
it('Should not allow cite urls that do not have an allowed scheme', function() {
|
|
718
|
+
assert.equal(
|
|
719
|
+
sanitizeHtml('<q cite=\"http://www.google.com\">HTTP</q><q cite=\"https://www.google.com\">HTTPS</q><q cite=\"mailto://www.google.com\">MAILTO</q><q cite=\"tel://www.google.com\">TEL</q><q cite=\"ftp://www.google.com\">FTP</q><q cite=\"data://www.google.com\">DATA</q><q cite=\"ldap://www.google.com\">LDAP</q><q cite=\"acrobat://www.google.com\">ACROBAT</q><q cite=\"vbscript://www.google.com\">VBSCRIPT</q><q cite=\"file://www.google.com\">FILE</q><q cite=\"rlogin://www.google.com\">RLOGIN</q><q cite=\"webcal://www.google.com\">WEBCAL</q><q cite=\"javascript://www.google.com\">JAVASCRIPT</q><q cite=\"mms://www.google.com\">MMS</q>',{
|
|
720
|
+
allowedTags: sanitizeHtml.defaults.allowedTags.concat( ['q'] ),
|
|
721
|
+
allowedAttributes: {q: [ 'cite' ]},
|
|
722
|
+
allowedSchemes: sanitizeHtml.defaults.allowedSchemes.concat([ 'tel' ]),
|
|
723
|
+
}), '<q cite=\"http://www.google.com\">HTTP</q><q cite=\"https://www.google.com\">HTTPS</q><q cite=\"mailto://www.google.com\">MAILTO</q><q cite=\"tel://www.google.com\">TEL</q><q cite=\"ftp://www.google.com\">FTP</q><q>DATA</q><q>LDAP</q><q>ACROBAT</q><q>VBSCRIPT</q><q>FILE</q><q>RLOGIN</q><q>WEBCAL</q><q>JAVASCRIPT</q><q>MMS</q>');
|
|
724
|
+
});
|
|
650
725
|
});
|