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/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "sanitize-html",
3
- "version": "1.16.3",
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
- "prepublish": "make clean && npm run minify",
11
- "test": "npm run prepublish && mocha test/test.js"
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(name) {
94
- if(name.indexOf('*') >= 0) {
95
- globRegex.push(quoteRegexp(name).replace(/\\\*/g, '.*'));
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(name);
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
- (has(allowedAttributesMap, name) && allowedAttributesMap[name].indexOf(a) !== -1 ) ||
200
- (allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1 ) ||
201
- (has(allowedAttributesGlobMap, name) && allowedAttributesGlobMap[name].test(a)) ||
202
- (allowedAttributesGlobMap['*'] && allowedAttributesGlobMap['*'].test(a))) {
203
- if ((a === 'href') || (a === 'src')) {
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
- var parsed = srcset.parse(value);
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
  });