sanitize-html 2.9.0 → 2.11.0

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.
Files changed (3) hide show
  1. package/README.md +68 -1
  2. package/index.js +81 -21
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -8,7 +8,10 @@ sanitize-html provides a simple HTML sanitizer with a clear API.
8
8
 
9
9
  sanitize-html is tolerant. It is well suited for cleaning up HTML fragments such as those created by CKEditor and other rich text editors. It is especially handy for removing unwanted CSS when copying and pasting from Word.
10
10
 
11
- sanitize-html allows you to specify the tags you want to permit, and the permitted attributes for each of those tags.
11
+ sanitize-html allows you to specify the tags you want to permit, and the permitted
12
+ attributes for each of those tags. If an attribute is a known non-boolean value,
13
+ and it is empty, it will be removed. For example `checked` can be empty, but `href`
14
+ cannot.
12
15
 
13
16
  If a tag is not permitted, the contents of the tag are not discarded. There are
14
17
  some exceptions to this, discussed below in the "Discarding the entire contents
@@ -125,6 +128,48 @@ allowedTags: [
125
128
  "small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr", "caption",
126
129
  "col", "colgroup", "table", "tbody", "td", "tfoot", "th", "thead", "tr"
127
130
  ],
131
+ nonBooleanAttributes: [
132
+ 'abbr', 'accept', 'accept-charset', 'accesskey', 'action',
133
+ 'allow', 'alt', 'as', 'autocapitalize', 'autocomplete',
134
+ 'blocking', 'charset', 'cite', 'class', 'color', 'cols',
135
+ 'colspan', 'content', 'contenteditable', 'coords', 'crossorigin',
136
+ 'data', 'datetime', 'decoding', 'dir', 'dirname', 'download',
137
+ 'draggable', 'enctype', 'enterkeyhint', 'fetchpriority', 'for',
138
+ 'form', 'formaction', 'formenctype', 'formmethod', 'formtarget',
139
+ 'headers', 'height', 'hidden', 'high', 'href', 'hreflang',
140
+ 'http-equiv', 'id', 'imagesizes', 'imagesrcset', 'inputmode',
141
+ 'integrity', 'is', 'itemid', 'itemprop', 'itemref', 'itemtype',
142
+ 'kind', 'label', 'lang', 'list', 'loading', 'low', 'max',
143
+ 'maxlength', 'media', 'method', 'min', 'minlength', 'name',
144
+ 'nonce', 'optimum', 'pattern', 'ping', 'placeholder', 'popover',
145
+ 'popovertarget', 'popovertargetaction', 'poster', 'preload',
146
+ 'referrerpolicy', 'rel', 'rows', 'rowspan', 'sandbox', 'scope',
147
+ 'shape', 'size', 'sizes', 'slot', 'span', 'spellcheck', 'src',
148
+ 'srcdoc', 'srclang', 'srcset', 'start', 'step', 'style',
149
+ 'tabindex', 'target', 'title', 'translate', 'type', 'usemap',
150
+ 'value', 'width', 'wrap',
151
+ // Event handlers
152
+ 'onauxclick', 'onafterprint', 'onbeforematch', 'onbeforeprint',
153
+ 'onbeforeunload', 'onbeforetoggle', 'onblur', 'oncancel',
154
+ 'oncanplay', 'oncanplaythrough', 'onchange', 'onclick', 'onclose',
155
+ 'oncontextlost', 'oncontextmenu', 'oncontextrestored', 'oncopy',
156
+ 'oncuechange', 'oncut', 'ondblclick', 'ondrag', 'ondragend',
157
+ 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart',
158
+ 'ondrop', 'ondurationchange', 'onemptied', 'onended',
159
+ 'onerror', 'onfocus', 'onformdata', 'onhashchange', 'oninput',
160
+ 'oninvalid', 'onkeydown', 'onkeypress', 'onkeyup',
161
+ 'onlanguagechange', 'onload', 'onloadeddata', 'onloadedmetadata',
162
+ 'onloadstart', 'onmessage', 'onmessageerror', 'onmousedown',
163
+ 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout',
164
+ 'onmouseover', 'onmouseup', 'onoffline', 'ononline', 'onpagehide',
165
+ 'onpageshow', 'onpaste', 'onpause', 'onplay', 'onplaying',
166
+ 'onpopstate', 'onprogress', 'onratechange', 'onreset', 'onresize',
167
+ 'onrejectionhandled', 'onscroll', 'onscrollend',
168
+ 'onsecuritypolicyviolation', 'onseeked', 'onseeking', 'onselect',
169
+ 'onslotchange', 'onstalled', 'onstorage', 'onsubmit', 'onsuspend',
170
+ 'ontimeupdate', 'ontoggle', 'onunhandledrejection', 'onunload',
171
+ 'onvolumechange', 'onwaiting', 'onwheel'
172
+ ],
128
173
  disallowedTagsMode: 'discard',
129
174
  allowedAttributes: {
130
175
  a: [ 'href', 'name', 'target' ],
@@ -167,6 +212,26 @@ allowedTags: false,
167
212
  allowedAttributes: false
168
213
  ```
169
214
 
215
+ #### "What if I want to allow empty attributes, even for cases like href that normally don't make sense?"
216
+
217
+ Very simple! Set `nonBooleanAttributes` to `[]`.
218
+
219
+ ```js
220
+ nonBooleanAttributes: []
221
+ ```
222
+
223
+ #### "What if I want to remove all empty attributes, including valid ones?"
224
+
225
+ Also very simple! Set `nonBooleanAttributes` to `['*']`.
226
+
227
+ **Note**: This will break common valid cases like `checked` and `selected`, so this is
228
+ unlikely to be what you want. For most ordinary HTML use, it is best to avoid making
229
+ this change.
230
+
231
+ ```js
232
+ nonBooleanAttributes: ['*']
233
+ ```
234
+
170
235
  #### "What if I don't want to allow *any* tags?"
171
236
 
172
237
  Also simple! Set `allowedTags` to `[]` and `allowedAttributes` to `{}`.
@@ -255,6 +320,8 @@ allowedClasses: {
255
320
  }
256
321
  ```
257
322
 
323
+ If `allowedClasses` for a certain tag is `false`, all the classes for this tag will be allowed.
324
+
258
325
  > Note: It is advised that your regular expressions always begin with `^` so that you are requiring a known prefix. A regular expression with neither `^` nor `$` just requires that something appear in the middle.
259
326
 
260
327
  ### Allowed CSS Styles
package/index.js CHANGED
@@ -117,12 +117,13 @@ function sanitizeHtml(html, options, _recursing) {
117
117
  options = Object.assign({}, sanitizeHtml.defaults, options);
118
118
  options.parser = Object.assign({}, htmlParserDefaults, options.parser);
119
119
 
120
+ const tagAllowed = function (name) {
121
+ return options.allowedTags === false || (options.allowedTags || []).indexOf(name) > -1;
122
+ };
123
+
120
124
  // vulnerableTags
121
125
  vulnerableTags.forEach(function (tag) {
122
- if (
123
- options.allowedTags !== false && (options.allowedTags || []).indexOf(tag) > -1 &&
124
- !options.allowVulnerableTags
125
- ) {
126
+ if (tagAllowed(tag) && !options.allowVulnerableTags) {
126
127
  console.warn(`\n\n⚠️ Your \`allowedTags\` option includes, \`${tag}\`, which is inherently\nvulnerable to XSS attacks. Please remove it from \`allowedTags\`.\nOr, to disable this warning, add the \`allowVulnerableTags\` option\nand ensure you are accounting for this risk.\n\n`);
127
128
  }
128
129
  });
@@ -169,20 +170,24 @@ function sanitizeHtml(html, options, _recursing) {
169
170
  allowedAttributesMap[tag].push('class');
170
171
  }
171
172
 
172
- allowedClassesMap[tag] = [];
173
- allowedClassesRegexMap[tag] = [];
174
- const globRegex = [];
175
- classes.forEach(function(obj) {
176
- if (typeof obj === 'string' && obj.indexOf('*') >= 0) {
177
- globRegex.push(escapeStringRegexp(obj).replace(/\\\*/g, '.*'));
178
- } else if (obj instanceof RegExp) {
179
- allowedClassesRegexMap[tag].push(obj);
180
- } else {
181
- allowedClassesMap[tag].push(obj);
173
+ allowedClassesMap[tag] = classes;
174
+
175
+ if (Array.isArray(classes)) {
176
+ const globRegex = [];
177
+ allowedClassesMap[tag] = [];
178
+ allowedClassesRegexMap[tag] = [];
179
+ classes.forEach(function(obj) {
180
+ if (typeof obj === 'string' && obj.indexOf('*') >= 0) {
181
+ globRegex.push(escapeStringRegexp(obj).replace(/\\\*/g, '.*'));
182
+ } else if (obj instanceof RegExp) {
183
+ allowedClassesRegexMap[tag].push(obj);
184
+ } else {
185
+ allowedClassesMap[tag].push(obj);
186
+ }
187
+ });
188
+ if (globRegex.length) {
189
+ allowedClassesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$');
182
190
  }
183
- });
184
- if (globRegex.length) {
185
- allowedClassesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$');
186
191
  }
187
192
  });
188
193
 
@@ -254,7 +259,7 @@ function sanitizeHtml(html, options, _recursing) {
254
259
  }
255
260
  }
256
261
 
257
- if ((options.allowedTags !== false && (options.allowedTags || []).indexOf(name) === -1) || (options.disallowedTagsMode === 'recursiveEscape' && !isEmptyObject(skipMap)) || (options.nestingLimit != null && depth >= options.nestingLimit)) {
262
+ if (!tagAllowed(name) || (options.disallowedTagsMode === 'recursiveEscape' && !isEmptyObject(skipMap)) || (options.nestingLimit != null && depth >= options.nestingLimit)) {
258
263
  skip = true;
259
264
  skipMap[depth] = true;
260
265
  if (options.disallowedTagsMode === 'discard') {
@@ -290,6 +295,12 @@ function sanitizeHtml(html, options, _recursing) {
290
295
  delete frame.attribs[a];
291
296
  return;
292
297
  }
298
+ // If the value is empty, and this is a known non-boolean attribute, delete it
299
+ // List taken from https://html.spec.whatwg.org/multipage/indices.html#attributes-3
300
+ if (value === '' && (options.nonBooleanAttributes.includes(a) || options.nonBooleanAttributes.includes('*'))) {
301
+ delete frame.attribs[a];
302
+ return;
303
+ }
293
304
  // check allowedAttributesMap for the element and attribute and modify the value
294
305
  // as necessary if there are specific values defined.
295
306
  let passedAllowedAttributesMapCheck = false;
@@ -450,7 +461,9 @@ function sanitizeHtml(html, options, _recursing) {
450
461
  return;
451
462
  }
452
463
  } catch (e) {
453
- console.warn('Failed to parse "' + name + ' {' + value + '}' + '", If you\'re running this in a browser, we recommend to disable style parsing: options.parseStyleAttributes: false, since this only works in a node environment due to a postcss dependency, More info: https://github.com/apostrophecms/sanitize-html/issues/547');
464
+ if (typeof window !== 'undefined') {
465
+ console.warn('Failed to parse "' + name + ' {' + value + '}' + '", If you\'re running this in a browser, we recommend to disable style parsing: options.parseStyleAttributes: false, since this only works in a node environment due to a postcss dependency, More info: https://github.com/apostrophecms/sanitize-html/issues/547');
466
+ }
454
467
  delete frame.attribs[a];
455
468
  return;
456
469
  }
@@ -513,7 +526,7 @@ function sanitizeHtml(html, options, _recursing) {
513
526
  frame.text += text;
514
527
  }
515
528
  },
516
- onclosetag: function(name) {
529
+ onclosetag: function(name, isImplied) {
517
530
 
518
531
  if (skipText) {
519
532
  skipTextDepth--;
@@ -563,8 +576,12 @@ function sanitizeHtml(html, options, _recursing) {
563
576
  frame.updateParentNodeMediaChildren();
564
577
  frame.updateParentNodeText();
565
578
 
566
- if (options.selfClosing.indexOf(name) !== -1) {
579
+ if (
567
580
  // Already output />
581
+ options.selfClosing.indexOf(name) !== -1 ||
582
+ // Escaped tag, closing tag is implied
583
+ (isImplied && !tagAllowed(name) && [ 'escape', 'recursiveEscape' ].indexOf(options.disallowedTagsMode) >= 0)
584
+ ) {
568
585
  if (skip) {
569
586
  result = tempResult;
570
587
  tempResult = '';
@@ -809,6 +826,49 @@ sanitizeHtml.defaults = {
809
826
  'caption', 'col', 'colgroup', 'table', 'tbody', 'td', 'tfoot', 'th',
810
827
  'thead', 'tr'
811
828
  ],
829
+ // Tags that cannot be boolean
830
+ nonBooleanAttributes: [
831
+ 'abbr', 'accept', 'accept-charset', 'accesskey', 'action',
832
+ 'allow', 'alt', 'as', 'autocapitalize', 'autocomplete',
833
+ 'blocking', 'charset', 'cite', 'class', 'color', 'cols',
834
+ 'colspan', 'content', 'contenteditable', 'coords', 'crossorigin',
835
+ 'data', 'datetime', 'decoding', 'dir', 'dirname', 'download',
836
+ 'draggable', 'enctype', 'enterkeyhint', 'fetchpriority', 'for',
837
+ 'form', 'formaction', 'formenctype', 'formmethod', 'formtarget',
838
+ 'headers', 'height', 'hidden', 'high', 'href', 'hreflang',
839
+ 'http-equiv', 'id', 'imagesizes', 'imagesrcset', 'inputmode',
840
+ 'integrity', 'is', 'itemid', 'itemprop', 'itemref', 'itemtype',
841
+ 'kind', 'label', 'lang', 'list', 'loading', 'low', 'max',
842
+ 'maxlength', 'media', 'method', 'min', 'minlength', 'name',
843
+ 'nonce', 'optimum', 'pattern', 'ping', 'placeholder', 'popover',
844
+ 'popovertarget', 'popovertargetaction', 'poster', 'preload',
845
+ 'referrerpolicy', 'rel', 'rows', 'rowspan', 'sandbox', 'scope',
846
+ 'shape', 'size', 'sizes', 'slot', 'span', 'spellcheck', 'src',
847
+ 'srcdoc', 'srclang', 'srcset', 'start', 'step', 'style',
848
+ 'tabindex', 'target', 'title', 'translate', 'type', 'usemap',
849
+ 'value', 'width', 'wrap',
850
+ // Event handlers
851
+ 'onauxclick', 'onafterprint', 'onbeforematch', 'onbeforeprint',
852
+ 'onbeforeunload', 'onbeforetoggle', 'onblur', 'oncancel',
853
+ 'oncanplay', 'oncanplaythrough', 'onchange', 'onclick', 'onclose',
854
+ 'oncontextlost', 'oncontextmenu', 'oncontextrestored', 'oncopy',
855
+ 'oncuechange', 'oncut', 'ondblclick', 'ondrag', 'ondragend',
856
+ 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart',
857
+ 'ondrop', 'ondurationchange', 'onemptied', 'onended',
858
+ 'onerror', 'onfocus', 'onformdata', 'onhashchange', 'oninput',
859
+ 'oninvalid', 'onkeydown', 'onkeypress', 'onkeyup',
860
+ 'onlanguagechange', 'onload', 'onloadeddata', 'onloadedmetadata',
861
+ 'onloadstart', 'onmessage', 'onmessageerror', 'onmousedown',
862
+ 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout',
863
+ 'onmouseover', 'onmouseup', 'onoffline', 'ononline', 'onpagehide',
864
+ 'onpageshow', 'onpaste', 'onpause', 'onplay', 'onplaying',
865
+ 'onpopstate', 'onprogress', 'onratechange', 'onreset', 'onresize',
866
+ 'onrejectionhandled', 'onscroll', 'onscrollend',
867
+ 'onsecuritypolicyviolation', 'onseeked', 'onseeking', 'onselect',
868
+ 'onslotchange', 'onstalled', 'onstorage', 'onsubmit', 'onsuspend',
869
+ 'ontimeupdate', 'ontoggle', 'onunhandledrejection', 'onunload',
870
+ 'onvolumechange', 'onwaiting', 'onwheel'
871
+ ],
812
872
  disallowedTagsMode: 'discard',
813
873
  allowedAttributes: {
814
874
  a: [ 'href', 'name', 'target' ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanitize-html",
3
- "version": "2.9.0",
3
+ "version": "2.11.0",
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",
@@ -38,7 +38,7 @@
38
38
  "eslint-plugin-node": "^11.1.0",
39
39
  "eslint-plugin-promise": "^4.2.1",
40
40
  "eslint-plugin-standard": "^4.0.1",
41
- "mocha": "^7.0.0",
41
+ "mocha": "^10.2.0",
42
42
  "sinon": "^9.0.2"
43
43
  }
44
44
  }