sanitize-html 1.18.3 → 1.19.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/CHANGELOG.md +170 -0
- package/README.md +12 -146
- package/dist/index.js +30 -10
- package/dist/sanitize-html.js +24319 -0
- package/dist/sanitize-html.min.js +12 -1
- package/package.json +1 -2
- package/src/index.js +32 -10
- package/test/test.js +79 -8
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sanitize-html",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.1",
|
|
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
|
-
"browser": "dist/sanitize-html.js",
|
|
7
6
|
"scripts": {
|
|
8
7
|
"prepare": "true",
|
|
9
8
|
"build": "make clean && make all && npm run prepare && browserify dist/index.js > dist/sanitize-html.js --standalone 'sanitizeHtml'",
|
package/src/index.js
CHANGED
|
@@ -240,21 +240,26 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
if (name === 'iframe' && a === 'src') {
|
|
243
|
+
var allowed = true;
|
|
243
244
|
try {
|
|
244
245
|
// naughtyHref is in charge of whether protocol relative URLs
|
|
245
246
|
// are cool. We should just accept them
|
|
246
247
|
parsed = url.parse(value, false, true);
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
var isRelativeUrl = parsed && parsed.host === null && parsed.protocol === null;
|
|
249
|
+
if (isRelativeUrl) {
|
|
250
|
+
// default value of allowIframeRelativeUrls is true unless allowIframeHostnames specified
|
|
251
|
+
allowed = has(options, "allowIframeRelativeUrls") ?
|
|
252
|
+
options.allowIframeRelativeUrls : !options.allowedIframeHostnames;
|
|
253
|
+
} else if (options.allowedIframeHostnames) {
|
|
254
|
+
allowed = options.allowedIframeHostnames.find(function (hostname) {
|
|
249
255
|
return hostname === parsed.hostname;
|
|
250
256
|
});
|
|
251
|
-
if (!whitelistedHostnames) {
|
|
252
|
-
delete frame.attribs[a];
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
257
|
}
|
|
256
258
|
} catch (e) {
|
|
257
259
|
// Unparseable iframe src
|
|
260
|
+
allowed = false;
|
|
261
|
+
}
|
|
262
|
+
if (!allowed) {
|
|
258
263
|
delete frame.attribs[a];
|
|
259
264
|
return;
|
|
260
265
|
}
|
|
@@ -310,7 +315,7 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
310
315
|
}
|
|
311
316
|
result += ' ' + a;
|
|
312
317
|
if (value.length) {
|
|
313
|
-
result += '="' + escapeHtml(value) + '"';
|
|
318
|
+
result += '="' + escapeHtml(value, true) + '"';
|
|
314
319
|
}
|
|
315
320
|
} else {
|
|
316
321
|
delete frame.attribs[a];
|
|
@@ -346,7 +351,7 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
346
351
|
// which have their own collection of XSS vectors.
|
|
347
352
|
result += text;
|
|
348
353
|
} else {
|
|
349
|
-
var escaped = escapeHtml(text);
|
|
354
|
+
var escaped = escapeHtml(text, false);
|
|
350
355
|
if (options.textFilter) {
|
|
351
356
|
result += options.textFilter(escaped);
|
|
352
357
|
} else {
|
|
@@ -407,11 +412,28 @@ function sanitizeHtml(html, options, _recursing) {
|
|
|
407
412
|
|
|
408
413
|
return result;
|
|
409
414
|
|
|
410
|
-
function escapeHtml(s) {
|
|
415
|
+
function escapeHtml(s, quote) {
|
|
411
416
|
if (typeof(s) !== 'string') {
|
|
412
417
|
s = s + '';
|
|
413
418
|
}
|
|
414
|
-
|
|
419
|
+
if (options.parser.decodeEntities) {
|
|
420
|
+
s = s.replace(/&/g, '&').replace(/</g, '<').replace(/\>/g, '>');
|
|
421
|
+
if (quote) {
|
|
422
|
+
s = s.replace(/\"/g, '"');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// TODO: this is inadequate because it will pass `&0;`. This approach
|
|
426
|
+
// will not work, each & must be considered with regard to whether it
|
|
427
|
+
// is followed by a 100% syntactically valid entity or not, and escaped
|
|
428
|
+
// if it is not. If this bothers you, don't set parser.decodeEntities
|
|
429
|
+
// to false. (The default is true.)
|
|
430
|
+
s = s.replace(/&(?![a-zA-Z0-9#]{1,20};)/g, '&') // Match ampersands not part of existing HTML entity
|
|
431
|
+
.replace(/</g, '<')
|
|
432
|
+
.replace(/\>/g, '>');
|
|
433
|
+
if (quote) {
|
|
434
|
+
s = s.replace(/\"/g, '"');
|
|
435
|
+
}
|
|
436
|
+
return s;
|
|
415
437
|
}
|
|
416
438
|
|
|
417
439
|
function naughtyHref(name, href) {
|
package/test/test.js
CHANGED
|
@@ -141,7 +141,7 @@ describe('sanitizeHtml', function() {
|
|
|
141
141
|
}
|
|
142
142
|
}}, textFilter: function (text) {
|
|
143
143
|
return text.replace(/\s/g, '_');
|
|
144
|
-
}}), '<a href="http://somelink">some_text_need
|
|
144
|
+
}}), '<a href="http://somelink">some_text_need"to<be>filtered</a>');
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
it('should add new text when not initially set and replace attributes when they are changed by transforming function', function () {
|
|
@@ -422,12 +422,12 @@ describe('sanitizeHtml', function() {
|
|
|
422
422
|
assert.equal(
|
|
423
423
|
sanitizeHtml('<<img src="javascript:evil"/>img src="javascript:evil"/>'
|
|
424
424
|
),
|
|
425
|
-
'<img src
|
|
425
|
+
'<img src="javascript:evil"/>'
|
|
426
426
|
);
|
|
427
427
|
assert.equal(
|
|
428
428
|
sanitizeHtml('<<a href="javascript:evil"/>a href="javascript:evil"/>'
|
|
429
429
|
),
|
|
430
|
-
'<<a>a href
|
|
430
|
+
'<<a>a href="javascript:evil"/></a>'
|
|
431
431
|
);
|
|
432
432
|
});
|
|
433
433
|
it('should allow attributes to be specified as globs', function() {
|
|
@@ -456,12 +456,12 @@ describe('sanitizeHtml', function() {
|
|
|
456
456
|
assert.equal(
|
|
457
457
|
sanitizeHtml('<div>"normal text"</div><script>"this is code"</script>', {
|
|
458
458
|
allowedTags: [ 'script' ]
|
|
459
|
-
}), '
|
|
459
|
+
}), '"normal text"<script>"this is code"</script>'
|
|
460
460
|
);
|
|
461
461
|
assert.equal(
|
|
462
462
|
sanitizeHtml('<div>"normal text"</div><style>body { background-image: url("image.test"); }</style>', {
|
|
463
463
|
allowedTags: [ 'style' ]
|
|
464
|
-
}), '
|
|
464
|
+
}), '"normal text"<style>body { background-image: url("image.test"); }</style>'
|
|
465
465
|
);
|
|
466
466
|
});
|
|
467
467
|
it('should not unescape escapes found inside script tags', function() {
|
|
@@ -480,7 +480,7 @@ describe('sanitizeHtml', function() {
|
|
|
480
480
|
textFilter: function(text) {
|
|
481
481
|
return text.replace(' this should be removed', '');
|
|
482
482
|
}
|
|
483
|
-
}), '
|
|
483
|
+
}), '"normal text"'
|
|
484
484
|
);
|
|
485
485
|
});
|
|
486
486
|
it('should respect htmlparser2 options when passed in', function() {
|
|
@@ -656,7 +656,7 @@ describe('sanitizeHtml', function() {
|
|
|
656
656
|
}), '<iframe src="https://www.youtube.com/embed/c2IlcS7AHxM"></iframe>'
|
|
657
657
|
);
|
|
658
658
|
});
|
|
659
|
-
it('Should remove iframe src urls that are not
|
|
659
|
+
it('Should remove iframe src urls that are not included in whitelisted hostnames', function() {
|
|
660
660
|
assert.equal(
|
|
661
661
|
sanitizeHtml('<iframe src="https://www.embed.vevo.com/USUV71704255"></iframe>', {
|
|
662
662
|
allowedTags: ['p', 'iframe', 'a', 'img', 'i'],
|
|
@@ -682,7 +682,7 @@ describe('sanitizeHtml', function() {
|
|
|
682
682
|
}), '<iframe src="https://www.vimeo.com/embed/c2IlcS7AHxM"></iframe>'
|
|
683
683
|
);
|
|
684
684
|
});
|
|
685
|
-
it('Should allow relative URLs for iframes', function() {
|
|
685
|
+
it('Should allow relative URLs for iframes by default', function() {
|
|
686
686
|
assert.equal(
|
|
687
687
|
sanitizeHtml('<iframe src="/foo"></iframe>', {
|
|
688
688
|
allowedTags: ['p', 'iframe', 'a', 'img', 'i'],
|
|
@@ -690,6 +690,43 @@ describe('sanitizeHtml', function() {
|
|
|
690
690
|
}), '<iframe src="/foo"></iframe>'
|
|
691
691
|
);
|
|
692
692
|
});
|
|
693
|
+
it('Should allow relative URLs for iframes', function() {
|
|
694
|
+
assert.equal(
|
|
695
|
+
sanitizeHtml('<iframe src="/foo"></iframe>', {
|
|
696
|
+
allowedTags: ['p', 'iframe', 'a', 'img', 'i'],
|
|
697
|
+
allowedAttributes: {'iframe': ['src', 'href'], 'a': ['src', 'href'], 'img': ['src']},
|
|
698
|
+
allowIframeRelativeUrls: true,
|
|
699
|
+
}), '<iframe src="/foo"></iframe>'
|
|
700
|
+
);
|
|
701
|
+
});
|
|
702
|
+
it('Should remove relative URLs for iframes', function() {
|
|
703
|
+
assert.equal(
|
|
704
|
+
sanitizeHtml('<iframe src="/foo"></iframe>', {
|
|
705
|
+
allowedTags: ['p', 'iframe', 'a', 'img', 'i'],
|
|
706
|
+
allowedAttributes: {'iframe': ['src', 'href'], 'a': ['src', 'href'], 'img': ['src']},
|
|
707
|
+
allowIframeRelativeUrls: false,
|
|
708
|
+
}), '<iframe></iframe>'
|
|
709
|
+
);
|
|
710
|
+
});
|
|
711
|
+
it('Should remove relative URLs for iframes when whitelisted hostnames specified', function() {
|
|
712
|
+
assert.equal(
|
|
713
|
+
sanitizeHtml('<iframe src="/foo"></iframe><iframe src="https://www.youtube.com/embed/c2IlcS7AHxM"></iframe>', {
|
|
714
|
+
allowedTags: ['p', 'iframe', 'a', 'img', 'i'],
|
|
715
|
+
allowedAttributes: {'iframe': ['src', 'href'], 'a': ['src', 'href'], 'img': ['src']},
|
|
716
|
+
allowedIframeHostnames: ['www.youtube.com']
|
|
717
|
+
}), '<iframe></iframe><iframe src="https://www.youtube.com/embed/c2IlcS7AHxM"></iframe>'
|
|
718
|
+
);
|
|
719
|
+
});
|
|
720
|
+
it('Should allow relative and whitelisted hostname URLs for iframes', function() {
|
|
721
|
+
assert.equal(
|
|
722
|
+
sanitizeHtml('<iframe src="/foo"></iframe><iframe src="https://www.youtube.com/embed/c2IlcS7AHxM"></iframe>', {
|
|
723
|
+
allowedTags: ['p', 'iframe', 'a', 'img', 'i'],
|
|
724
|
+
allowedAttributes: {'iframe': ['src', 'href'], 'a': ['src', 'href'], 'img': ['src']},
|
|
725
|
+
allowIframeRelativeUrls: true,
|
|
726
|
+
allowedIframeHostnames: ['www.youtube.com']
|
|
727
|
+
}), '<iframe src="/foo"></iframe><iframe src="https://www.youtube.com/embed/c2IlcS7AHxM"></iframe>'
|
|
728
|
+
);
|
|
729
|
+
});
|
|
693
730
|
it('Should allow protocol-relative URLs for the right domain for iframes', function() {
|
|
694
731
|
assert.equal(
|
|
695
732
|
sanitizeHtml('<iframe src="//www.youtube.com/embed/c2IlcS7AHxM"></iframe>', {
|
|
@@ -748,4 +785,38 @@ describe('sanitizeHtml', function() {
|
|
|
748
785
|
allowedSchemes: sanitizeHtml.defaults.allowedSchemes.concat([ 'tel' ]),
|
|
749
786
|
}), '<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>');
|
|
750
787
|
});
|
|
788
|
+
it('Should encode &, <, > and where necessary, "', function() {
|
|
789
|
+
assert.equal(sanitizeHtml('"< & >" <span class=""test"">cool</span>', {
|
|
790
|
+
allowedTags: [ 'span' ],
|
|
791
|
+
allowedAttributes: {
|
|
792
|
+
span: [ 'class' ]
|
|
793
|
+
}
|
|
794
|
+
}), '"< & >" <span class=""test"">cool</span>');
|
|
795
|
+
});
|
|
796
|
+
it('Should not pass through &0; unescaped if decodeEntities is true (the default)', function() {
|
|
797
|
+
assert.equal(sanitizeHtml('<img src="<0&0;0.2&" />', {allowedTags: ['img']}), '<img src="<0&0;0.2&" />');
|
|
798
|
+
});
|
|
799
|
+
it('Should not double encode ampersands on HTML entities if decodeEntities is false (TODO more tests, this is too loose to rely upon)', function() {
|
|
800
|
+
var textIn = 'This & & that ® 
 ± OK?';
|
|
801
|
+
var expectedResult = 'This & & that ® 
 ± OK?';
|
|
802
|
+
var sanitizeHtmlOptions = {
|
|
803
|
+
parser: {
|
|
804
|
+
decodeEntities: false
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
assert.equal(sanitizeHtml(textIn, sanitizeHtmlOptions), expectedResult);
|
|
808
|
+
});
|
|
809
|
+
// TODO: make this test and similar tests for entities that are not
|
|
810
|
+
// strictly valid pass, at which point decodeEntities: false is safe
|
|
811
|
+
// to use.
|
|
812
|
+
//
|
|
813
|
+
// it('Should not pass through &0; (a bogus entity) unescaped if decodeEntities is false', function() {
|
|
814
|
+
// assert.equal(sanitizeHtml(
|
|
815
|
+
// '<img src="<0&0;0.2&" />', {
|
|
816
|
+
// allowedTags: ['img'],
|
|
817
|
+
// parser: {
|
|
818
|
+
// decodeEntities: false
|
|
819
|
+
// }
|
|
820
|
+
// }), '<img src="<0&0;0.2&" />');
|
|
821
|
+
// });
|
|
751
822
|
});
|