jquery-html-prefilter-3.5.0-backport.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  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. (function (jQuery) {
  19. // Parts of this backport differ by jQuery version.
  20. var versionParts = jQuery.fn.jquery.split('.');
  21. var majorVersion = parseInt(versionParts[0]);
  22. var minorVersion = parseInt(versionParts[1]);
  23. // No backport is needed if we're already on jQuery 3.5 or higher.
  24. if ( (majorVersion > 3) || (majorVersion === 3 && minorVersion >= 5) ) {
  25. return;
  26. }
  27. // Prior to jQuery 3.5, jQuery converted XHTML-style self-closing tags to
  28. // their XML equivalent: e.g., "<div />" to "<div></div>". This is
  29. // problematic for several reasons, including that it's vulnerable to XSS
  30. // attacks. However, since this was jQuery's behavior for many years, many
  31. // Drupal modules and jQuery plugins may be relying on it. Therefore, we
  32. // preserve that behavior, but for a limited set of tags only, that we believe
  33. // to not be vulnerable. This is the set of HTML tags that satisfy all of the
  34. // following conditions:
  35. // - In DOMPurify's list of HTML tags. If an HTML tag isn't safe enough to
  36. // appear in that list, then we don't want to mess with it here either.
  37. // @see https://github.com/cure53/DOMPurify/blob/2.0.11/dist/purify.js#L128
  38. // - A normal element (not a void, template, text, or foreign element).
  39. // @see https://html.spec.whatwg.org/multipage/syntax.html#elements-2
  40. // - An element that is still defined by the current HTML specification
  41. // (not a deprecated element), because we do not want to rely on how
  42. // browsers parse deprecated elements.
  43. // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
  44. // - Not 'html', 'head', or 'body', because this pseudo-XHTML expansion is
  45. // designed for fragments, not entire documents.
  46. // - Not 'colgroup', because due to an idiosyncrasy of jQuery's original
  47. // regular expression, it didn't match on colgroup, and we don't want to
  48. // introduce a behavior change for that.
  49. var selfClosingTagsToReplace = [
  50. 'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',
  51. 'blockquote', 'button', 'canvas', 'caption', 'cite', 'code', 'data',
  52. 'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em',
  53. 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
  54. 'h4', 'h5', 'h6', 'header', 'hgroup', 'i', 'ins', 'kbd', 'label', 'legend',
  55. 'li', 'main', 'map', 'mark', 'menu', 'meter', 'nav', 'ol', 'optgroup',
  56. 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt',
  57. 'ruby', 's', 'samp', 'section', 'select', 'small', 'source', 'span',
  58. 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
  59. 'thead', 'time', 'tr', 'u', 'ul', 'var', 'video'
  60. ];
  61. // Define regular expressions for <TAG/> and <TAG ATTRIBUTES/>. Doing this as
  62. // two expressions makes it easier to target <a/> without also targeting
  63. // every tag that starts with "a".
  64. var xhtmlRegExpGroup = '(' + selfClosingTagsToReplace.join('|') + ')';
  65. var whitespace = '[\\x20\\t\\r\\n\\f]';
  66. var rxhtmlTagWithoutSpaceOrAttributes = new RegExp('<' + xhtmlRegExpGroup + '\\/>', 'gi');
  67. var rxhtmlTagWithSpaceAndMaybeAttributes = new RegExp('<' + xhtmlRegExpGroup + '(' + whitespace + '[^>]*)\\/>', 'gi');
  68. // jQuery 3.5 also fixed a vulnerability for when </select> appears within
  69. // an <option> or <optgroup>, but it did that in local code that we can't
  70. // backport directly. Instead, we filter such cases out. To do so, we need to
  71. // determine when jQuery would otherwise invoke the vulnerable code, which it
  72. // uses this regular expression to determine. The regular expression changed
  73. // for version 3.0.0 and changed again for 3.4.0.
  74. // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4958
  75. // @see https://github.com/jquery/jquery/blob/3.0.0/dist/jquery.js#L4584
  76. // @see https://github.com/jquery/jquery/blob/3.4.0/dist/jquery.js#L4712
  77. var rtagName;
  78. if (majorVersion < 3) {
  79. rtagName = /<([\w:]+)/;
  80. }
  81. else if (minorVersion < 4) {
  82. rtagName = /<([a-z][^\/\0>\x20\t\r\n\f]+)/i;
  83. }
  84. else {
  85. rtagName = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i;
  86. }
  87. // The regular expression that jQuery uses to determine which self-closing
  88. // tags to expand to open and close tags. This is vulnerable, because it
  89. // matches all tag names except the few excluded ones. We only use this
  90. // expression for determining vulnerability. The expression changed for
  91. // version 3, but we only need to check for vulnerability in versions 1 and 2,
  92. // so we use the expression from those versions.
  93. // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4957
  94. var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
  95. jQuery.extend({
  96. htmlPrefilter: function (html) {
  97. // This is how jQuery determines the first tag in the HTML.
  98. // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5521
  99. var tag = ( rtagName.exec( html ) || [ "", "" ] )[ 1 ].toLowerCase();
  100. // It is not valid HTML for <option> or <optgroup> to have <select> as
  101. // either a descendant or sibling, and attempts to inject one can cause
  102. // XSS on jQuery versions before 3.5. Since this is invalid HTML and a
  103. // possible XSS attack, reject the entire string.
  104. // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
  105. if ((tag === 'option' || tag === 'optgroup') && html.match(/<\/?select/i)) {
  106. html = '';
  107. }
  108. // Retain jQuery's prior to 3.5 conversion of pseudo-XHTML, but for only
  109. // the tags in the `selfClosingTagsToReplace` list defined above.
  110. // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5518
  111. // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
  112. html = html.replace(rxhtmlTagWithoutSpaceOrAttributes, "<$1></$1>");
  113. html = html.replace(rxhtmlTagWithSpaceAndMaybeAttributes, "<$1$2></$1>");
  114. // Prior to jQuery 1.12 and 2.2, this function gets called (via code later
  115. // in this file) in addition to, rather than instead of, the unsafe
  116. // expansion of self-closing tags (including ones not in the list above).
  117. // We can't prevent that unsafe expansion from running, so instead we
  118. // check to make sure that it doesn't affect the DOM returned by the
  119. // browser's parsing logic. If it does affect it, then it's vulnerable to
  120. // XSS, so we reject the entire string.
  121. if ( (majorVersion === 1 && minorVersion < 12) || (majorVersion === 2 && minorVersion < 2) ) {
  122. var htmlRisky = html.replace(rxhtmlTag, "<$1></$2>");
  123. if (htmlRisky !== html) {
  124. // Even though htmlRisky and html are different strings, they might
  125. // represent the same HTML structure once parsed, in which case,
  126. // htmlRisky is actually safe. We can ask the browser to parse both
  127. // to find out, but the browser can't parse table fragments (e.g., a
  128. // root-level "<td>"), so we need to wrap them. We just need this
  129. // technique to work on all supported browsers; we don't need to
  130. // copy from the specific jQuery version we're using.
  131. // @see https://github.com/jquery/jquery/blob/3.5.1/dist/jquery.js#L4939
  132. var wrapMap = {
  133. thead: [ 1, "<table>", "</table>" ],
  134. col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
  135. tr: [ 2, "<table><tbody>", "</tbody></table>" ],
  136. td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
  137. };
  138. wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
  139. wrapMap.th = wrapMap.td;
  140. // Function to wrap HTML into something that a browser can parse.
  141. // @see https://github.com/jquery/jquery/blob/3.5.1/dist/jquery.js#L5032
  142. var getWrappedHtml = function (html) {
  143. var wrap = wrapMap[tag];
  144. if (wrap) {
  145. html = wrap[1] + html + wrap[2];
  146. }
  147. return html;
  148. };
  149. // Function to return canonical HTML after parsing it. This parses
  150. // only; it doesn't execute scripts.
  151. // @see https://github.com/jquery/jquery-migrate/blob/3.3.0/src/jquery/manipulation.js#L5
  152. var getParsedHtml = function (html) {
  153. var doc = window.document.implementation.createHTMLDocument( "" );
  154. doc.body.innerHTML = html;
  155. return doc.body ? doc.body.innerHTML : '';
  156. };
  157. // If the browser couldn't parse either one successfully, or if
  158. // htmlRisky parses differently than html, then html is vulnerable,
  159. // so reject it.
  160. var htmlParsed = getParsedHtml(getWrappedHtml(html));
  161. var htmlRiskyParsed = getParsedHtml(getWrappedHtml(htmlRisky));
  162. if (htmlRiskyParsed === '' || htmlParsed === '' || (htmlRiskyParsed !== htmlParsed)) {
  163. html = '';
  164. }
  165. }
  166. }
  167. return html;
  168. }
  169. });
  170. // Prior to jQuery 1.12 and 2.2, jQuery.clean(), jQuery.buildFragment(), and
  171. // jQuery.fn.html() did not call jQuery.htmlPrefilter(), so we add that.
  172. if ( (majorVersion === 1 && minorVersion < 12) || (majorVersion === 2 && minorVersion < 2) ) {
  173. // Filter the HTML coming into jQuery.fn.html().
  174. var fnOriginalHtml = jQuery.fn.html;
  175. jQuery.fn.extend({
  176. // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5147
  177. html: function (value) {
  178. if (typeof value === "string") {
  179. value = jQuery.htmlPrefilter(value);
  180. }
  181. // .html() can be called as a setter (with an argument) or as a getter
  182. // (without an argument), so invoke fnOriginalHtml() the same way that
  183. // we were invoked.
  184. return fnOriginalHtml.apply(this, arguments.length ? [value] : []);
  185. }
  186. });
  187. // The regular expression that jQuery uses to determine if a string is HTML.
  188. // Used by both clean() and buildFragment().
  189. // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4960
  190. var rhtml = /<|&#?\w+;/;
  191. // Filter HTML coming into:
  192. // - jQuery.clean() for versions prior to 1.9.
  193. // - jQuery.buildFragment() for 1.9 and above.
  194. //
  195. // The looping constructs in the two functions might be essentially
  196. // identical, but they're each expressed here in the way that most closely
  197. // matches their original expression in jQuery, so that we filter all of
  198. // the items and only the items that jQuery will treat as HTML strings.
  199. if (majorVersion === 1 && minorVersion < 9) {
  200. var originalClean = jQuery.clean;
  201. jQuery.extend({
  202. // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5493
  203. 'clean': function (elems, context, fragment, scripts) {
  204. for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
  205. if ( typeof elem === "string" && rhtml.test( elem ) ) {
  206. elems[i] = elem = jQuery.htmlPrefilter(elem);
  207. }
  208. }
  209. return originalClean.call(this, elems, context, fragment, scripts);
  210. }
  211. });
  212. }
  213. else {
  214. var originalBuildFragment = jQuery.buildFragment;
  215. jQuery.extend({
  216. // @see https://github.com/jquery/jquery/blob/1.9.0/jquery.js#L6419
  217. 'buildFragment': function (elems, context, scripts, selection) {
  218. var l = elems.length;
  219. for ( var i = 0; i < l; i++ ) {
  220. var elem = elems[i];
  221. if (elem || elem === 0) {
  222. if ( jQuery.type( elem ) !== "object" && rhtml.test( elem ) ) {
  223. elems[i] = elem = jQuery.htmlPrefilter(elem);
  224. }
  225. }
  226. }
  227. return originalBuildFragment.call(this, elems, context, scripts, selection);
  228. }
  229. });
  230. }
  231. }
  232. })(jQuery);