sanitize-html 2.17.2 → 2.17.4

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 +2 -2
  2. package/index.js +24 -44
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -695,13 +695,13 @@ Normally, with a few exceptions, if a tag is not allowed, all of the text within
695
695
 
696
696
  The exceptions are:
697
697
 
698
- `style`, `script`, `textarea`, `option`
698
+ `style`, `script`, `textarea`, `option`, `xmp`
699
699
 
700
700
  If you wish to replace this list, for instance to discard whatever is found
701
701
  inside a `noscript` tag, use the `nonTextTags` option:
702
702
 
703
703
  ```js
704
- nonTextTags: [ 'style', 'script', 'textarea', 'option', 'noscript' ]
704
+ nonTextTags: [ 'style', 'script', 'textarea', 'option', 'xmp', 'noscript' ]
705
705
  ```
706
706
 
707
707
  Note that if you use this option you are responsible for stating the entire list. This gives you the power to retain the content of `textarea`, if you want to.
package/index.js CHANGED
@@ -4,6 +4,7 @@ const { isPlainObject } = require('is-plain-object');
4
4
  const deepmerge = require('deepmerge');
5
5
  const parseSrcset = require('parse-srcset');
6
6
  const { parse: postcssParse } = require('postcss');
7
+ const { naughtyHref: launderNaughtyHref } = require('launder');
7
8
  // Tags that can conceivably represent stand-alone media.
8
9
  const mediaTags = [
9
10
  'img', 'audio', 'video', 'picture', 'svg',
@@ -135,11 +136,16 @@ function sanitizeHtml(html, options, _recursing) {
135
136
  // the text when the tag is disallowed makes sense for other reasons.
136
137
  // If we are not allowing these tags, we should drop their content too.
137
138
  // For other tags you would drop the tag but keep its content.
139
+ // `xmp` is included because htmlparser2 treats it as a raw-text element,
140
+ // so markup inside is parsed as text on input but would otherwise be
141
+ // re-emitted unescaped via the `ontext` branch below, allowing XSS bypass
142
+ // when `xmp` is disallowed (which is the default).
138
143
  const nonTextTagsArray = options.nonTextTags || [
139
144
  'script',
140
145
  'style',
141
146
  'textarea',
142
- 'option'
147
+ 'option',
148
+ 'xmp'
143
149
  ];
144
150
  let allowedAttributesMap;
145
151
  let allowedAttributesGlobMap;
@@ -566,10 +572,14 @@ function sanitizeHtml(html, options, _recursing) {
566
572
  // your concern, don't allow them. The same is essentially true for style tags
567
573
  // which have their own collection of XSS vectors.
568
574
  result += text;
569
- } else if ((options.disallowedTagsMode === 'discard' || options.disallowedTagsMode === 'completelyDiscard') && (nonTextTagsArray.indexOf(tag) !== -1)) {
570
- // htmlparser2 does not decode entities inside raw text elements like
571
- // textarea and option. The text is already properly encoded, so pass
572
- // it through without additional escaping to avoid double-encoding.
575
+ } else if ((options.disallowedTagsMode === 'discard' || options.disallowedTagsMode === 'completelyDiscard') && (tag === 'textarea' || tag === 'xmp')) {
576
+ // htmlparser2 treats <textarea> and <xmp> as raw text elements and
577
+ // does NOT decode entities inside them. The text is already properly
578
+ // encoded, so pass it through without additional escaping to avoid
579
+ // double-encoding. Other "nonTextTags" like <option> are not raw text
580
+ // elements in htmlparser2, so their contents are decoded and must be
581
+ // escaped below like any other text (important to prevent XSS via
582
+ // entity-encoded payloads such as <option>&lt;script&gt;...&lt;/script&gt;</option>).
573
583
  result += text;
574
584
  } else if (!addedText) {
575
585
  const escaped = escapeHtml(text, false);
@@ -722,45 +732,15 @@ function sanitizeHtml(html, options, _recursing) {
722
732
  }
723
733
 
724
734
  function naughtyHref(name, href) {
725
- // Browsers ignore character codes of 32 (space) and below in a surprising
726
- // number of situations. Start reading here:
727
- // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Embedded_tab
728
- // eslint-disable-next-line no-control-regex
729
- href = href.replace(/[\x00-\x20]+/g, '');
730
- // Clobber any comments in URLs, which the browser might
731
- // interpret inside an XML data island, allowing
732
- // a javascript: URL to be snuck through
733
- while (true) {
734
- const firstIndex = href.indexOf('<!--');
735
- if (firstIndex === -1) {
736
- break;
737
- }
738
- const lastIndex = href.indexOf('-->', firstIndex + 4);
739
- if (lastIndex === -1) {
740
- break;
741
- }
742
- href = href.substring(0, firstIndex) + href.substring(lastIndex + 3);
743
- }
744
- // Case insensitive so we don't get faked out by JAVASCRIPT #1
745
- // Allow more characters after the first so we don't get faked
746
- // out by certain schemes browsers accept
747
- const matches = href.match(/^([a-zA-Z][a-zA-Z0-9.\-+]*):/);
748
- if (!matches) {
749
- // Protocol-relative URL starting with any combination of '/' and '\'
750
- if (href.match(/^[/\\]{2}/)) {
751
- return !options.allowProtocolRelative;
752
- }
753
-
754
- // No scheme
755
- return false;
756
- }
757
- const scheme = matches[1].toLowerCase();
758
-
759
- if (has(options.allowedSchemesByTag, name)) {
760
- return options.allowedSchemesByTag[name].indexOf(scheme) === -1;
761
- }
762
-
763
- return !options.allowedSchemes || options.allowedSchemes.indexOf(scheme) === -1;
735
+ // Resolve the per-tag scheme allowlist if one is configured for
736
+ // this tag, otherwise fall back to the global allowedSchemes.
737
+ const allowedSchemes = has(options.allowedSchemesByTag, name)
738
+ ? options.allowedSchemesByTag[name]
739
+ : (options.allowedSchemes || []);
740
+ return launderNaughtyHref(href, {
741
+ allowedSchemes,
742
+ allowProtocolRelative: options.allowProtocolRelative
743
+ });
764
744
  }
765
745
 
766
746
  function parseUrl(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanitize-html",
3
- "version": "2.17.2",
3
+ "version": "2.17.4",
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",
@@ -27,11 +27,12 @@
27
27
  "htmlparser2": "^10.1.0",
28
28
  "is-plain-object": "^5.0.0",
29
29
  "parse-srcset": "^1.0.2",
30
- "postcss": "^8.3.11"
30
+ "postcss": "^8.3.11",
31
+ "launder": "^1.7.1"
31
32
  },
32
33
  "devDependencies": {
33
34
  "eslint": "^9.39.1",
34
- "mocha": "^10.2.0",
35
+ "mocha": "^11.7.5",
35
36
  "sinon": "^9.0.2",
36
37
  "eslint-config-apostrophe": "^6.0.2"
37
38
  },