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); |