Projet

Général

Profil

Paste
Télécharger (12,3 ko) Statistiques
| Branche: | Révision:

root / drupal7 / misc / jquery-html-prefilter-3.5.0-backport.js @ 26e8440b

1
/**
2
 * For jQuery versions less than 3.5.0, this replaces the jQuery.htmlPrefilter()
3
 * function with one that fixes these security vulnerabilities while also
4
 * retaining the pre-3.5.0 behavior where it's safe to do so.
5
 * - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
6
 * - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
7
 *
8
 * Additionally, for jQuery versions that do not have a jQuery.htmlPrefilter()
9
 * function (1.x prior to 1.12 and 2.x prior to 2.2), this adds it, and
10
 * extends the functions that need to call it to do so.
11
 *
12
 * Drupal core's jQuery version is 1.4.4, but jQuery Update can provide a
13
 * different version, so this covers all versions between 1.4.4 and 3.4.1.
14
 * The GitHub links in the code comments below link to jQuery 1.5 code, because
15
 * 1.4.4 isn't on GitHub, but the referenced code didn't change from 1.4.4 to
16
 * 1.5.
17
 */
18

    
19
(function (jQuery) {
20

    
21
  // Parts of this backport differ by jQuery version.
22
  var versionParts = jQuery.fn.jquery.split('.');
23
  var majorVersion = parseInt(versionParts[0]);
24
  var minorVersion = parseInt(versionParts[1]);
25

    
26
  // No backport is needed if we're already on jQuery 3.5 or higher.
27
  if ( (majorVersion > 3) || (majorVersion === 3 && minorVersion >= 5) ) {
28
    return;
29
  }
30

    
31
  // Prior to jQuery 3.5, jQuery converted XHTML-style self-closing tags to
32
  // their XML equivalent: e.g., "<div />" to "<div></div>". This is
33
  // problematic for several reasons, including that it's vulnerable to XSS
34
  // attacks. However, since this was jQuery's behavior for many years, many
35
  // Drupal modules and jQuery plugins may be relying on it. Therefore, we
36
  // preserve that behavior, but for a limited set of tags only, that we believe
37
  // to not be vulnerable. This is the set of HTML tags that satisfy all of the
38
  // following conditions:
39
  // - In DOMPurify's list of HTML tags. If an HTML tag isn't safe enough to
40
  //   appear in that list, then we don't want to mess with it here either.
41
  //   @see https://github.com/cure53/DOMPurify/blob/2.0.11/dist/purify.js#L128
42
  // - A normal element (not a void, template, text, or foreign element).
43
  //   @see https://html.spec.whatwg.org/multipage/syntax.html#elements-2
44
  // - An element that is still defined by the current HTML specification
45
  //   (not a deprecated element), because we do not want to rely on how
46
  //   browsers parse deprecated elements.
47
  //   @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
48
  // - Not 'html', 'head', or 'body', because this pseudo-XHTML expansion is
49
  //   designed for fragments, not entire documents.
50
  // - Not 'colgroup', because due to an idiosyncrasy of jQuery's original
51
  //   regular expression, it didn't match on colgroup, and we don't want to
52
  //   introduce a behavior change for that.
53
  var selfClosingTagsToReplace = [
54
    'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',
55
    'blockquote', 'button', 'canvas', 'caption', 'cite', 'code', 'data',
56
    'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em',
57
    'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
58
    'h4', 'h5', 'h6', 'header', 'hgroup', 'i', 'ins', 'kbd', 'label', 'legend',
59
    'li', 'main', 'map', 'mark', 'menu', 'meter', 'nav', 'ol', 'optgroup',
60
    'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt',
61
    'ruby', 's', 'samp', 'section', 'select', 'small', 'source', 'span',
62
    'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
63
    'thead', 'time', 'tr', 'u', 'ul', 'var', 'video'
64
  ];
65

    
66
  // Define regular expressions for <TAG/> and <TAG ATTRIBUTES/>. Doing this as
67
  // two expressions makes it easier to target <a/> without also targeting
68
  // every tag that starts with "a".
69
  var xhtmlRegExpGroup = '(' + selfClosingTagsToReplace.join('|') + ')';
70
  var whitespace = '[\\x20\\t\\r\\n\\f]';
71
  var rxhtmlTagWithoutSpaceOrAttributes = new RegExp('<' + xhtmlRegExpGroup + '\\/>', 'gi');
72
  var rxhtmlTagWithSpaceAndMaybeAttributes = new RegExp('<' + xhtmlRegExpGroup + '(' + whitespace + '[^>]*)\\/>', 'gi');
73

    
74
  // jQuery 3.5 also fixed a vulnerability for when </select> appears within
75
  // an <option> or <optgroup>, but it did that in local code that we can't
76
  // backport directly. Instead, we filter such cases out. To do so, we need to
77
  // determine when jQuery would otherwise invoke the vulnerable code, which it
78
  // uses this regular expression to determine. The regular expression changed
79
  // for version 3.0.0 and changed again for 3.4.0.
80
  // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4958
81
  // @see https://github.com/jquery/jquery/blob/3.0.0/dist/jquery.js#L4584
82
  // @see https://github.com/jquery/jquery/blob/3.4.0/dist/jquery.js#L4712
83
  var rtagName;
84
  if (majorVersion < 3) {
85
    rtagName = /<([\w:]+)/;
86
  }
87
  else if (minorVersion < 4) {
88
    rtagName = /<([a-z][^\/\0>\x20\t\r\n\f]+)/i;
89
  }
90
  else {
91
    rtagName = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i;
92
  }
93

    
94
  // The regular expression that jQuery uses to determine which self-closing
95
  // tags to expand to open and close tags. This is vulnerable, because it
96
  // matches all tag names except the few excluded ones. We only use this
97
  // expression for determining vulnerability. The expression changed for
98
  // version 3, but we only need to check for vulnerability in versions 1 and 2,
99
  // so we use the expression from those versions.
100
  // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4957
101
  var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
102

    
103
  jQuery.extend({
104
    htmlPrefilter: function (html) {
105
      // This is how jQuery determines the first tag in the HTML.
106
      // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5521
107
      var tag = ( rtagName.exec( html ) || [ "", "" ] )[ 1 ].toLowerCase();
108

    
109
      // It is not valid HTML for <option> or <optgroup> to have <select> as
110
      // either a descendant or sibling, and attempts to inject one can cause
111
      // XSS on jQuery versions before 3.5. Since this is invalid HTML and a
112
      // possible XSS attack, reject the entire string.
113
      // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
114
      if ((tag === 'option' || tag === 'optgroup') && html.match(/<\/?select/i)) {
115
        html = '';
116
      }
117

    
118
      // Retain jQuery's prior to 3.5 conversion of pseudo-XHTML, but for only
119
      // the tags in the `selfClosingTagsToReplace` list defined above.
120
      // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5518
121
      // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
122
      html = html.replace(rxhtmlTagWithoutSpaceOrAttributes, "<$1></$1>");
123
      html = html.replace(rxhtmlTagWithSpaceAndMaybeAttributes, "<$1$2></$1>");
124

    
125
      // Prior to jQuery 1.12 and 2.2, this function gets called (via code later
126
      // in this file) in addition to, rather than instead of, the unsafe
127
      // expansion of self-closing tags (including ones not in the list above).
128
      // We can't prevent that unsafe expansion from running, so instead we
129
      // check to make sure that it doesn't affect the DOM returned by the
130
      // browser's parsing logic. If it does affect it, then it's vulnerable to
131
      // XSS, so we reject the entire string.
132
      if ( (majorVersion === 1 && minorVersion < 12) || (majorVersion === 2 && minorVersion < 2) ) {
133
        var htmlRisky = html.replace(rxhtmlTag, "<$1></$2>");
134
        if (htmlRisky !== html) {
135
          // Even though htmlRisky and html are different strings, they might
136
          // represent the same HTML structure once parsed, in which case,
137
          // htmlRisky is actually safe. We can ask the browser to parse both
138
          // to find out, but the browser can't parse table fragments (e.g., a
139
          // root-level "<td>"), so we need to wrap them. We just need this
140
          // technique to work on all supported browsers; we don't need to
141
          // copy from the specific jQuery version we're using.
142
          // @see https://github.com/jquery/jquery/blob/3.5.1/dist/jquery.js#L4939
143
          var wrapMap = {
144
            thead: [ 1, "<table>", "</table>" ],
145
            col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
146
            tr: [ 2, "<table><tbody>", "</tbody></table>" ],
147
            td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
148
          };
149
          wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
150
          wrapMap.th = wrapMap.td;
151

    
152
          // Function to wrap HTML into something that a browser can parse.
153
          // @see https://github.com/jquery/jquery/blob/3.5.1/dist/jquery.js#L5032
154
          var getWrappedHtml = function (html) {
155
            var wrap = wrapMap[tag];
156
            if (wrap) {
157
              html = wrap[1] + html + wrap[2];
158
            }
159
            return html;
160
          };
161

    
162
          // Function to return canonical HTML after parsing it. This parses
163
          // only; it doesn't execute scripts.
164
          // @see https://github.com/jquery/jquery-migrate/blob/3.3.0/src/jquery/manipulation.js#L5
165
          var getParsedHtml = function (html) {
166
            var doc = window.document.implementation.createHTMLDocument( "" );
167
            doc.body.innerHTML = html;
168
            return doc.body ? doc.body.innerHTML : '';
169
          };
170

    
171
          // If the browser couldn't parse either one successfully, or if
172
          // htmlRisky parses differently than html, then html is vulnerable,
173
          // so reject it.
174
          var htmlParsed = getParsedHtml(getWrappedHtml(html));
175
          var htmlRiskyParsed = getParsedHtml(getWrappedHtml(htmlRisky));
176
          if (htmlRiskyParsed === '' || htmlParsed === '' || (htmlRiskyParsed !== htmlParsed)) {
177
            html = '';
178
          }
179
        }
180
      }
181

    
182
      return html;
183
    }
184
  });
185

    
186
  // Prior to jQuery 1.12 and 2.2, jQuery.clean(), jQuery.buildFragment(), and
187
  // jQuery.fn.html() did not call jQuery.htmlPrefilter(), so we add that.
188
  if ( (majorVersion === 1 && minorVersion < 12) || (majorVersion === 2 && minorVersion < 2) ) {
189
    // Filter the HTML coming into jQuery.fn.html().
190
    var fnOriginalHtml = jQuery.fn.html;
191
    jQuery.fn.extend({
192
      // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5147
193
      html: function (value) {
194
        if (typeof value === "string") {
195
          value = jQuery.htmlPrefilter(value);
196
        }
197
        // .html() can be called as a setter (with an argument) or as a getter
198
        // (without an argument), so invoke fnOriginalHtml() the same way that
199
        // we were invoked.
200
        return fnOriginalHtml.apply(this, arguments.length ? [value] : []);
201
      }
202
    });
203

    
204
    // The regular expression that jQuery uses to determine if a string is HTML.
205
    // Used by both clean() and buildFragment().
206
    // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4960
207
    var rhtml = /<|&#?\w+;/;
208

    
209
    // Filter HTML coming into:
210
    // - jQuery.clean() for versions prior to 1.9.
211
    // - jQuery.buildFragment() for 1.9 and above.
212
    //
213
    // The looping constructs in the two functions might be essentially
214
    // identical, but they're each expressed here in the way that most closely
215
    // matches their original expression in jQuery, so that we filter all of
216
    // the items and only the items that jQuery will treat as HTML strings.
217
    if (majorVersion === 1 && minorVersion < 9) {
218
      var originalClean = jQuery.clean;
219
      jQuery.extend({
220
        // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5493
221
        'clean': function (elems, context, fragment, scripts) {
222
          for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
223
            if ( typeof elem === "string" && rhtml.test( elem ) ) {
224
              elems[i] = elem = jQuery.htmlPrefilter(elem);
225
            }
226
          }
227
          return originalClean.call(this, elems, context, fragment, scripts);
228
        }
229
      });
230
    }
231
    else {
232
      var originalBuildFragment = jQuery.buildFragment;
233
      jQuery.extend({
234
        // @see https://github.com/jquery/jquery/blob/1.9.0/jquery.js#L6419
235
        'buildFragment': function (elems, context, scripts, selection) {
236
          var l = elems.length;
237
          for ( var i = 0; i < l; i++ ) {
238
            var elem = elems[i];
239
            if (elem || elem === 0) {
240
              if ( jQuery.type( elem ) !== "object" && rhtml.test( elem ) ) {
241
                elems[i] = elem = jQuery.htmlPrefilter(elem);
242
              }
243
            }
244
          }
245
          return originalBuildFragment.call(this, elems, context, scripts, selection);
246
        }
247
      });
248
    }
249
  }
250

    
251
})(jQuery);