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/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "sanitize-html",
3
- "version": "1.18.3",
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
- if (options.allowedIframeHostnames) {
248
- var whitelistedHostnames = options.allowedIframeHostnames.find(function(hostname) {
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
- return s.replace(/\&/g, '&amp;').replace(/</g, '&lt;').replace(/\>/g, '&gt;').replace(/\"/g, '&quot;');
419
+ if (options.parser.decodeEntities) {
420
+ s = s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\>/g, '&gt;');
421
+ if (quote) {
422
+ s = s.replace(/\"/g, '&quot;');
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, '&amp;') // Match ampersands not part of existing HTML entity
431
+ .replace(/</g, '&lt;')
432
+ .replace(/\>/g, '&gt;');
433
+ if (quote) {
434
+ s = s.replace(/\"/g, '&quot;');
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&quot;to&lt;be&gt;filtered</a>');
144
+ }}), '<a href="http://somelink">some_text_need"to&lt;be&gt;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
- '&lt;img src=&quot;javascript:evil&quot;/&gt;'
425
+ '&lt;img src="javascript:evil"/&gt;'
426
426
  );
427
427
  assert.equal(
428
428
  sanitizeHtml('<<a href="javascript:evil"/>a href="javascript:evil"/>'
429
429
  ),
430
- '&lt;<a>a href=&quot;javascript:evil&quot;/&gt;</a>'
430
+ '&lt;<a>a href="javascript:evil"/&gt;</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
- }), '&quot;normal text&quot;<script>"this is code"</script>'
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
- }), '&quot;normal text&quot;<style>body { background-image: url("image.test"); }</style>'
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
- }), '&quot;normal text&quot;'
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 inlcuded in whitelisted hostnames', function() {
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="&#34;test&#34;">cool</span>', {
790
+ allowedTags: [ 'span' ],
791
+ allowedAttributes: {
792
+ span: [ 'class' ]
793
+ }
794
+ }), '"&lt; &amp; &gt;" <span class="&quot;test&quot;">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="&lt;0&amp;0;0.2&amp;" />');
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 &amp; & that &reg; &#x0000A; &#10; &plusmn; OK?';
801
+ var expectedResult = 'This &amp; &amp; that &reg; &#x0000A; &#10; &plusmn; 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="&lt;0&amp;0;0.2&amp;" />');
821
+ // });
751
822
  });