sanitize-html 2.3.1 → 2.5.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.
package/CHANGELOG.md CHANGED
@@ -1,7 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.5.0 (2021-09-08):
4
+
5
+ - New `allowedScriptHostnames` option, it enables you to specify which hostnames are allowed in a script tag.
6
+ - New `allowedScriptDomains` option, it enables you to specify which domains are allowed in a script tag. Thank you to [Yorick Girard](https://github.com/yorickgirard) for this and the `allowedScriptHostnames` contribution.
7
+ - Updates whitelist to allowlist.
8
+
9
+ ## 2.4.0 (2021-05-19):
10
+ - Added support for class names with wildcards in `allowedClasses`. Thanks to [zhangbenber](https://github.com/zhangbenber) for the contribution.
11
+
12
+ ## 2.3.3 (2021-03-19):
13
+ - Security fix: `allowedSchemes` and related options did not properly block schemes containing a hyphen, plus sign, period or digit, such as `ms-calculator:`. Thanks to Lukas Euler for pointing out the issue.
14
+ - Added a security note about the known risks associated with using the `parser` option, especially `decodeEntities: false`. See the documentation.
15
+
16
+ ## 2.3.2 (2021-01-26):
17
+
18
+ - Additional fixes for iframe validation exploits. Prevent exploits based on browsers' tolerance of the use of "\" rather than "/" and the presence of whitespace at this point in the URL. Thanks to Ron Masas of [Checkmarx](https://www.checkmarx.com/) for pointing out the issue and writing unit tests.
19
+ - Updates README `yarn add` syntax. Thanks to [Tagir Khadshiev](https://github.com/Aspedm) for the contribution.
20
+
3
21
  ## 2.3.1 (2021-01-22):
4
- - Uses the standard WHATWG URL parser to stop IDNA (Internationalized Domain Name) attacks on the iframe hostname validator. Thanks to Ron Masas of Checkmarx for pointing out the issue and suggesting the use of the WHATWG parser.
22
+ - Uses the standard WHATWG URL parser to stop IDNA (Internationalized Domain Name) attacks on the iframe hostname validator. Thanks to Ron Masas of [Checkmarx](https://www.checkmarx.com/) for pointing out the issue and suggesting the use of the WHATWG parser.
5
23
 
6
24
  ## 2.3.0 (2020-12-16):
7
25
  - Upgrades `htmlparser2` to new major version `^6.0.0`. Thanks to [Bogdan Chadkin](https://github.com/TrySound) for the contribution.
package/README.md CHANGED
@@ -26,9 +26,19 @@ HTML comments are not preserved.
26
26
 
27
27
  sanitize-html is intended for use with Node.js and supports Node 10+. All of its npm dependencies are pure JavaScript. sanitize-html is built on the excellent `htmlparser2` module.
28
28
 
29
- ### Regarding Typescript
29
+ ### Regarding TypeScript
30
30
 
31
- sanitize-html is not written in Typescript and there is no plan to directly support it. There is a community supported implementation, [`@types/sanitize-html`](https://www.npmjs.com/package/@types/sanitize-html), however. Any questions or problems while using that implementation should be directed to its maintainers as directed by that project's contribution guidelines.
31
+ sanitize-html is not written in TypeScript and there is no plan to directly support it. There is a community supported typing definition, [`@types/sanitize-html`](https://www.npmjs.com/package/@types/sanitize-html), however.
32
+ ```bash
33
+ npm install -D @types/sanitize-html
34
+ ```
35
+ If `esModuleInterop=true` is not set in your `tsconfig.json` file, you have to import it with:
36
+
37
+ ```javascript
38
+ import * as sanitizeHtml from 'sanitize-html';
39
+ ```
40
+
41
+ Any questions or problems while using `@types/sanitize-html` should be directed to its maintainers as directed by that project's contribution guidelines.
32
42
 
33
43
  ## How to use
34
44
 
@@ -38,11 +48,14 @@ sanitize-html is not written in Typescript and there is no plan to directly supp
38
48
 
39
49
  But, perhaps you'd like to display sanitized HTML immediately in the browser for preview. Or ask the browser to do the sanitization work on every page load. You can if you want to!
40
50
 
41
- * Clone repository and install via npm
42
- * Run npm install and:
51
+ * Install the package:
43
52
 
44
53
  ```bash
45
- npm install sanitize-html # yarn install sanitize-html
54
+ npm install sanitize-html
55
+ ```
56
+ or
57
+ ```
58
+ yarn add sanitize-html
46
59
  ```
47
60
 
48
61
  The primary change in the 2.x version of sanitize-html is that it no longer includes a build that is ready for browser use. Developers are expected to include sanitize-html in their project builds (e.g., webpack) as they would any other dependency. So while sanitize-html is no longer ready to link to directly in HTML, developers can now more easily process it according to their needs.
@@ -69,7 +82,7 @@ npm install sanitize-html
69
82
 
70
83
  Import the module:
71
84
 
72
- ```bash
85
+ ```js
73
86
  // In ES modules
74
87
  import sanitizeHtml from 'sanitize-html';
75
88
 
@@ -224,16 +237,17 @@ const clean = sanitizeHtml(dirty, {
224
237
  });
225
238
  ```
226
239
 
227
- Similar to `allowedAttributes`, you can use `*` as a tag name, to allow listed classes to be valid for any tag:
240
+ Similar to `allowedAttributes`, you can use `*` to allow classes with a certain prefix, or use `*` as a tag name to allow listed classes to be valid for any tag:
228
241
  ```js
229
242
  allowedClasses: {
243
+ 'code': [ 'language-*', 'lang-*' ],
230
244
  '*': [ 'fancy', 'simple' ]
231
245
  }
232
246
  ```
233
247
 
234
248
  ### Allowed CSS Styles
235
249
 
236
- If you wish to allow specific CSS _styles_ on a particular element, you can do that with the `allowedStyles` option. Simply declare your desired attributes as regular expression options within an array for the given attribute. Specific elements will inherit whitelisted attributes from the global (`*`) attribute. Any other CSS classes are discarded.
250
+ If you wish to allow specific CSS _styles_ on a particular element, you can do that with the `allowedStyles` option. Simply declare your desired attributes as regular expression options within an array for the given attribute. Specific elements will inherit allowlisted attributes from the global (`*`) attribute. Any other CSS classes are discarded.
237
251
 
238
252
  **You must also use `allowedAttributes`** to activate the `style` attribute for the relevant elements. Otherwise this feature will never come into play.
239
253
 
@@ -274,7 +288,9 @@ enforceHtmlBoundary: true
274
288
 
275
289
  ### htmlparser2 Options
276
290
 
277
- sanitize-html is built on `htmlparser2`. By default the only option passed down is `decodeEntities: true` You can set the options to pass by using the parser option.
291
+ sanitize-html is built on `htmlparser2`. By default the only option passed down is `decodeEntities: true`. You can set the options to pass by using the parser option.
292
+
293
+ **Security note: changing the `parser` settings can be risky.** In particular, `decodeEntities: false` has known security concerns and a complete test suite does not exist for every possible combination of settings when used with `sanitize-html`. If security is your goal we recommend you use the defaults rather than changing `parser`, except for the `lowerCaseTags` option.
278
294
 
279
295
  ```javascript
280
296
  const clean = sanitizeHtml(dirty, {
@@ -500,6 +516,32 @@ const clean = sanitizeHtml('<p><iframe src="https://us02web.zoom.us/embed/12345"
500
516
  });
501
517
  ```
502
518
 
519
+ ### Script Filters
520
+
521
+ Similarly to iframes you can allow a script tag on a list of allowlisted domains
522
+
523
+ ```js
524
+ const clean = sanitizeHtml('<script src="https://www.safe.authorized.com/lib.js"></script>', {
525
+ allowedTags: ['script'],
526
+ allowedAttributes: {
527
+ script: ['src']
528
+ },
529
+ allowedScriptDomains: ['authorized.com'],
530
+ })
531
+ ```
532
+
533
+ You can allow a script tag on a list of allowlisted hostnames too
534
+
535
+ ```js
536
+ const clean = sanitizeHtml('<script src="https://www.authorized.com/lib.js"></script>', {
537
+ allowedTags: ['script'],
538
+ allowedAttributes: {
539
+ script: ['src']
540
+ },
541
+ allowedScriptHostnames: [ 'www.authorized.com' ],
542
+ })
543
+ ```
544
+
503
545
  ### Allowed URL schemes
504
546
 
505
547
  By default, we allow the following URL schemes in cases where `href`, `src`, etc. are allowed:
package/index.js CHANGED
@@ -146,10 +146,13 @@ function sanitizeHtml(html, options, _recursing) {
146
146
  allowedAttributesMap[tag].push(obj);
147
147
  }
148
148
  });
149
- allowedAttributesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$');
149
+ if (globRegex.length) {
150
+ allowedAttributesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$');
151
+ }
150
152
  });
151
153
  }
152
154
  const allowedClassesMap = {};
155
+ const allowedClassesGlobMap = {};
153
156
  each(options.allowedClasses, function(classes, tag) {
154
157
  // Implicitly allows the class attribute
155
158
  if (allowedAttributesMap) {
@@ -159,7 +162,18 @@ function sanitizeHtml(html, options, _recursing) {
159
162
  allowedAttributesMap[tag].push('class');
160
163
  }
161
164
 
162
- allowedClassesMap[tag] = classes;
165
+ allowedClassesMap[tag] = [];
166
+ const globRegex = [];
167
+ classes.forEach(function(obj) {
168
+ if (typeof obj === 'string' && obj.indexOf('*') >= 0) {
169
+ globRegex.push(escapeStringRegexp(obj).replace(/\\\*/g, '.*'));
170
+ } else {
171
+ allowedClassesMap[tag].push(obj);
172
+ }
173
+ });
174
+ if (globRegex.length) {
175
+ allowedClassesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$');
176
+ }
163
177
  });
164
178
 
165
179
  const transformTagsMap = {};
@@ -301,9 +315,40 @@ function sanitizeHtml(html, options, _recursing) {
301
315
  return;
302
316
  }
303
317
  }
318
+ if (name === 'script' && a === 'src') {
319
+ let allowed = true;
320
+
321
+ frame.innerText = '';
322
+
323
+ try {
324
+ const parsed = new URL(value);
325
+
326
+ if (options.allowedScriptHostnames || options.allowedScriptDomains) {
327
+ const allowedHostname = (options.allowedScriptHostnames || []).find(function (hostname) {
328
+ return hostname === parsed.hostname;
329
+ });
330
+ const allowedDomain = (options.allowedScriptDomains || []).find(function(domain) {
331
+ return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
332
+ });
333
+ allowed = allowedHostname || allowedDomain;
334
+ }
335
+ } catch (e) {
336
+ allowed = false;
337
+ }
338
+
339
+ if (!allowed) {
340
+ delete frame.attribs[a];
341
+ return;
342
+ }
343
+ }
344
+
304
345
  if (name === 'iframe' && a === 'src') {
305
346
  let allowed = true;
306
347
  try {
348
+ // Chrome accepts \ as a substitute for / in the // at the
349
+ // start of a URL, so rewrite accordingly to prevent exploit.
350
+ // Also drop any whitespace at that point in the URL
351
+ value = value.replace(/^(\w+:)?\s*[\\/]\s*[\\/]/, '$1//');
307
352
  if (value.startsWith('relative:')) {
308
353
  // An attempt to exploit our workaround for base URLs being
309
354
  // mandatory for relative URL validation in the WHATWG
@@ -375,10 +420,17 @@ function sanitizeHtml(html, options, _recursing) {
375
420
  if (a === 'class') {
376
421
  const allowedSpecificClasses = allowedClassesMap[name];
377
422
  const allowedWildcardClasses = allowedClassesMap['*'];
423
+ const allowedSpecificClassesGlob = allowedClassesGlobMap[name];
424
+ const allowedWildcardClassesGlob = allowedClassesGlobMap['*'];
425
+ const allowedClassesGlobs = [ allowedSpecificClassesGlob, allowedWildcardClassesGlob ].filter(
426
+ function(t) {
427
+ return t;
428
+ }
429
+ );
378
430
  if (allowedSpecificClasses && allowedWildcardClasses) {
379
- value = filterClasses(value, deepmerge(allowedSpecificClasses, allowedWildcardClasses));
431
+ value = filterClasses(value, deepmerge(allowedSpecificClasses, allowedWildcardClasses), allowedClassesGlobs);
380
432
  } else {
381
- value = filterClasses(value, allowedSpecificClasses || allowedWildcardClasses);
433
+ value = filterClasses(value, allowedSpecificClasses || allowedWildcardClasses, allowedClassesGlobs);
382
434
  }
383
435
  if (!value.length) {
384
436
  delete frame.attribs[a];
@@ -564,7 +616,9 @@ function sanitizeHtml(html, options, _recursing) {
564
616
  // a javascript: URL to be snuck through
565
617
  href = href.replace(/<!--.*?-->/g, '');
566
618
  // Case insensitive so we don't get faked out by JAVASCRIPT #1
567
- const matches = href.match(/^([a-zA-Z]+):/);
619
+ // Allow more characters after the first so we don't get faked
620
+ // out by certain schemes browsers accept
621
+ const matches = href.match(/^([a-zA-Z][a-zA-Z0-9.\-+]*):/);
568
622
  if (!matches) {
569
623
  // Protocol-relative URL starting with any combination of '/' and '\'
570
624
  if (href.match(/^[/\\]{2}/)) {
@@ -584,7 +638,7 @@ function sanitizeHtml(html, options, _recursing) {
584
638
  }
585
639
 
586
640
  /**
587
- * Filters user input css properties by whitelisted regex attributes.
641
+ * Filters user input css properties by allowlisted regex attributes.
588
642
  *
589
643
  * @param {object} abstractSyntaxTree - Object representation of CSS attributes.
590
644
  * @property {array[Declaration]} abstractSyntaxTree.nodes[0] - Each object cointains prop and value key, i.e { prop: 'color', value: 'red' }.
@@ -637,10 +691,10 @@ function sanitizeHtml(html, options, _recursing) {
637
691
 
638
692
  /**
639
693
  * Filters the existing attributes for the given property. Discards any attributes
640
- * which don't match the whitelist.
694
+ * which don't match the allowlist.
641
695
  *
642
696
  * @param {object} selectedRule - Example: { color: red, font-family: helvetica }
643
- * @param {array} allowedDeclarationsList - List of declarations which pass whitelisting.
697
+ * @param {array} allowedDeclarationsList - List of declarations which pass the allowlist.
644
698
  * @param {object} attributeObject - Object representing the current css property.
645
699
  * @property {string} attributeObject.type - Typically 'declaration'.
646
700
  * @property {string} attributeObject.prop - The CSS property, i.e 'color'.
@@ -649,7 +703,7 @@ function sanitizeHtml(html, options, _recursing) {
649
703
  */
650
704
  function filterDeclarations(selectedRule) {
651
705
  return function (allowedDeclarationsList, attributeObject) {
652
- // If this property is whitelisted...
706
+ // If this property is allowlisted...
653
707
  if (has(selectedRule, attributeObject.prop)) {
654
708
  const matchesRegex = selectedRule[attributeObject.prop].some(function(regularExpression) {
655
709
  return regularExpression.test(attributeObject.value);
@@ -663,14 +717,16 @@ function sanitizeHtml(html, options, _recursing) {
663
717
  };
664
718
  }
665
719
 
666
- function filterClasses(classes, allowed) {
720
+ function filterClasses(classes, allowed, allowedGlobs) {
667
721
  if (!allowed) {
668
722
  // The class attribute is allowed without filtering on this tag
669
723
  return classes;
670
724
  }
671
725
  classes = classes.split(/\s+/);
672
726
  return classes.filter(function(clss) {
673
- return allowed.indexOf(clss) !== -1;
727
+ return allowed.indexOf(clss) !== -1 || allowedGlobs.some(function(glob) {
728
+ return glob.test(clss);
729
+ });
674
730
  }).join(' ');
675
731
  }
676
732
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sanitize-html",
3
- "version": "2.3.1",
4
- "description": "Clean up user-submitted HTML, preserving whitelisted elements and whitelisted attributes on a per-element basis",
3
+ "version": "2.5.0",
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",
7
7
  "files": [