sanitize-html 2.17.6 → 2.17.7

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 +13 -0
  2. package/index.js +45 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -696,6 +696,19 @@ And you can forbid the use of protocol-relative URLs (starting with `//`) to acc
696
696
  allowProtocolRelative: false
697
697
  ```
698
698
 
699
+ ### SVG animations of URL attributes are discarded
700
+
701
+ For security reasons, if your `allowedTags` includes the SVG animation elements (`animate`, `animateColor`, `animateMotion`, `animateTransform` and `set`), note that an animation which targets a URL attribute is always discarded:
702
+
703
+ ```html
704
+ <!-- Discarded: this would set the link's href to javascript: after sanitization -->
705
+ <animate attributeName="href" values="#safe;javascript:alert(1)" dur=".01s" fill="freeze">
706
+ ```
707
+
708
+ An animation element carries no URL itself. It names the attribute it animates with `attributeName` and supplies the new value in `values`, `from`, `to` or `by`, which the browser copies into the target attribute after sanitization. Scheme checking those values is not sufficient, because `values` is a semicolon-separated *list* of destinations, so we discard the animation instead whenever `attributeName` selects `href`, `xlink:href` or any other attribute listed in `allowedSchemesAppliedToAttributes`.
709
+
710
+ Animations of attributes that are not URLs, such as `fill` or `opacity`, are unaffected.
711
+
699
712
  ### Discarding the entire contents of a disallowed tag
700
713
 
701
714
  Normally, with a few exceptions, if a tag is not allowed, all of the text within it is preserved, and so are any allowed tags within it.
package/index.js CHANGED
@@ -12,6 +12,17 @@ const mediaTags = [
12
12
  ];
13
13
  // Tags that are inherently vulnerable to being used in XSS attacks.
14
14
  const vulnerableTags = [ 'script', 'style' ];
15
+ // SVG SMIL animation elements. These do not carry a URL themselves: they
16
+ // retarget an attribute of another element, naming it with `attributeName`
17
+ // and supplying the new value(s) in `values`, `from`, `to` and `by`.
18
+ const svgAnimationTags = [
19
+ 'animate', 'animatecolor', 'animatemotion', 'animatetransform', 'set'
20
+ ];
21
+ // Attribute names that always name a URL sink, whatever
22
+ // `allowedSchemesAppliedToAttributes` has been narrowed to. A namespace prefix
23
+ // is ignored when matching, so `xlink:href` and any other prefixed spelling of
24
+ // `href` are covered.
25
+ const alwaysUrlAttributes = [ 'href' ];
15
26
 
16
27
  function each(obj, cb) {
17
28
  if (obj) {
@@ -272,7 +283,7 @@ function sanitizeHtml(html, options, _recursing) {
272
283
  }
273
284
  }
274
285
 
275
- if (!tagAllowed(name) || (options.disallowedTagsMode === 'recursiveEscape' && !isEmptyObject(skipMap)) || (options.nestingLimit != null && depth >= options.nestingLimit)) {
286
+ if (!tagAllowed(name) || animatesUrlAttribute(name, attribs) || (options.disallowedTagsMode === 'recursiveEscape' && !isEmptyObject(skipMap)) || (options.nestingLimit != null && depth >= options.nestingLimit)) {
276
287
  skip = true;
277
288
  skipMap[depth] = true;
278
289
  if (options.disallowedTagsMode === 'discard' || options.disallowedTagsMode === 'completelyDiscard') {
@@ -773,6 +784,39 @@ function sanitizeHtml(html, options, _recursing) {
773
784
  });
774
785
  }
775
786
 
787
+ // True if this is an SVG SMIL animation element that animates a URL-bearing
788
+ // attribute, e.g. `<animate attributeName="href" values="#safe;javascript:...">`.
789
+ //
790
+ // Such an element carries no URL of its own: the browser copies the animation
791
+ // values into the target attribute *after* sanitization, so a `javascript:`
792
+ // destination reaches a live link sink without ever being scheme checked.
793
+ // `values` compounds this, because it is a semicolon-separated LIST of
794
+ // destinations: checking it as one flat URL only validates its first entry, so
795
+ // a leading `#safe` fragment carries the rest of the list past the policy.
796
+ //
797
+ // Re-checking each entry of each value attribute would leave the safety of the
798
+ // output resting on our imitation of SMIL list parsing, so we reject the
799
+ // animation on the strength of its target instead. Animations of attributes
800
+ // that are not URL sinks, such as `fill` or `opacity`, are unaffected.
801
+ function animatesUrlAttribute(name, attribs) {
802
+ if (svgAnimationTags.indexOf(name.toLowerCase()) === -1) {
803
+ return false;
804
+ }
805
+ const schemeCheckedAttributes = options.allowedSchemesAppliedToAttributes || [];
806
+ return Object.keys(attribs || {}).some(function(attributeName) {
807
+ if (attributeName.toLowerCase() !== 'attributename') {
808
+ return false;
809
+ }
810
+ const target = (attribs[attributeName] || '').trim().toLowerCase();
811
+ // The target may be namespace prefixed (`xlink:href`). Which prefixes are
812
+ // in scope depends on the document, so consider the local name too.
813
+ const localName = target.slice(target.lastIndexOf(':') + 1);
814
+ return alwaysUrlAttributes.indexOf(localName) !== -1 ||
815
+ schemeCheckedAttributes.indexOf(target) !== -1 ||
816
+ schemeCheckedAttributes.indexOf(localName) !== -1;
817
+ });
818
+ }
819
+
776
820
  function parseUrl(value) {
777
821
  value = value.replace(/^(\w+:)?\s*[\\/]\s*[\\/]/, '$1//');
778
822
  if (value.startsWith('relative:')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanitize-html",
3
- "version": "2.17.6",
3
+ "version": "2.17.7",
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",