From 4edfb0966acd565121493367febe8473536da87b Mon Sep 17 00:00:00 2001 From: PPInfy <56343352+PPInfy@users.noreply.github.com> Date: Mon, 5 Oct 2020 16:16:44 +0530 Subject: [PATCH] Security fixes - prevent possible XSS due to regex-based HTML replacement Updated To have https://github.com/angular/angular.js/pull/17028 --- src/.eslintrc.json | 1 + src/Angular.js | 20 ++++++++++++ src/AngularPublic.js | 1 + src/jqLite.js | 77 ++++++++++++++++++++++++++++++++------------ 4 files changed, 79 insertions(+), 20 deletions(-) diff --git a/src/.eslintrc.json b/src/.eslintrc.json index 00426ae42094..9a2b62d1a560 100644 --- a/src/.eslintrc.json +++ b/src/.eslintrc.json @@ -102,6 +102,7 @@ "VALIDITY_STATE_PROPERTY": false, "reloadWithDebugInfo": false, "stringify": false, + "UNSAFE_restoreLegacyJqLiteXHTMLReplacement": false, "NODE_TYPE_ELEMENT": false, "NODE_TYPE_ATTRIBUTE": false, diff --git a/src/Angular.js b/src/Angular.js index 6d5b6bb74082..0f19f37ba7ec 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1967,6 +1967,26 @@ function bindJQuery() { bindJQueryFired = true; } +/** + * @ngdoc function + * @name angular.UNSAFE_restoreLegacyJqLiteXHTMLReplacement + * @module ng + * @kind function + * + * @description + * Restores the pre-1.8 behavior of jqLite that turns XHTML-like strings like + * `
` to `
` instead of `
`. + * The new behavior is a security fix. Thus, if you need to call this function, please try to adjust + * your code for this change and remove your use of this function as soon as possible. + + * Note that this only patches jqLite. If you use jQuery 3.5.0 or newer, please read the + * [jQuery 3.5 upgrade guide](https://jquery.com/upgrade-guide/3.5/) for more details + * about the workarounds. + */ +function UNSAFE_restoreLegacyJqLiteXHTMLReplacement() { + JQLite.legacyXHTMLReplacement = true; +} + /** * throw error if the argument is falsy. */ diff --git a/src/AngularPublic.js b/src/AngularPublic.js index d2d56bdb4b8d..e7f0b02deecd 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -155,6 +155,7 @@ function publishExternalAPI(angular) { 'callbacks': {$$counter: 0}, 'getTestability': getTestability, 'reloadWithDebugInfo': reloadWithDebugInfo, + 'UNSAFE_restoreLegacyJqLiteXHTMLReplacement': UNSAFE_restoreLegacyJqLiteXHTMLReplacement, '$$minErr': minErr, '$$csp': csp, '$$encodeUriSegment': encodeUriSegment, diff --git a/src/jqLite.js b/src/jqLite.js index 541d2642fb26..f697edfcece1 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -170,19 +170,26 @@ var TAG_NAME_REGEXP = /<([\w:-]+)/; var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi; var wrapMap = { - 'option': [1, ''], - 'thead': [1, '', '
'], - 'col': [2, '', '
'], - 'tr': [2, '', '
'], - 'td': [3, '', '
'], - '_default': [0, '', ''] + thead: ['table'], + col: ['colgroup', 'table'], + tr: ['tbody', 'table'], + td: ['tr', 'tbody', 'table'] }; -wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; +var wrapMapIE9 = { + option: [1, ''], + _default: [0, '', ''] +}; +for (var key in wrapMap) { + var wrapMapValueClosing = wrapMap[key]; + var wrapMapValue = wrapMapValueClosing.slice().reverse(); + wrapMapIE9[key] = [wrapMapValue.length, '<' + wrapMapValue.join('><') + '>', '']; +} +wrapMapIE9.optgroup = wrapMapIE9.option; function jqLiteIsTextNode(html) { return !HTML_REGEXP.test(html); @@ -203,7 +210,7 @@ function jqLiteHasData(node) { } function jqLiteBuildFragment(html, context) { - var tmp, tag, wrap, + var tmp, tag, wrap, finalHtml, fragment = context.createDocumentFragment(), nodes = [], i; @@ -214,13 +221,29 @@ function jqLiteBuildFragment(html, context) { // Convert html into DOM nodes tmp = fragment.appendChild(context.createElement('div')); tag = (TAG_NAME_REGEXP.exec(html) || ['', ''])[1].toLowerCase(); - wrap = wrapMap[tag] || wrapMap._default; - tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, '<$1>') + wrap[2]; + finalHtml = JQLite.legacyXHTMLReplacement ? + html.replace(XHTML_TAG_REGEXP, '<$1>') : + html; + if (msie < 10) { + wrap = wrapMapIE9[tag] || wrapMapIE9._default; + tmp.innerHTML = wrap[1] + finalHtml + wrap[2]; // Descend through wrappers to the right content i = wrap[0]; while (i--) { - tmp = tmp.lastChild; + tmp = tmp.firstChild; + } + } else { + wrap = wrapMap[tag] || []; + + // Create wrappers & descend into them + i = wrap.length; + while (--i > -1) { + tmp.appendChild(window.document.createElement(wrap[i])); + tmp = tmp.firstChild; + } + + tmp.innerHTML = finalHtml; } nodes = concat(nodes, tmp.childNodes); @@ -311,6 +334,23 @@ function jqLiteDealoc(element, onlyDescendants) { } } +function isEmptyObject(obj) { + var name; + for (name in obj) { + return false; + } + return true; +} +function removeIfEmptyData(element) { + var expandoId = element.ng339; + var expandoStore = expandoId && jqCache[expandoId]; + var events = expandoStore && expandoStore.events; + var data = expandoStore && expandoStore.data; + if ((!data || isEmptyObject(data)) && (!events || isEmptyObject(events))) { + delete jqCache[expandoId]; + element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it + } +} function jqLiteOff(element, type, fn, unsupported) { if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument'); @@ -347,6 +387,7 @@ function jqLiteOff(element, type, fn, unsupported) { } }); } + removeIfEmptyData(element); } function jqLiteRemoveData(element, name) { @@ -356,17 +397,12 @@ function jqLiteRemoveData(element, name) { if (expandoStore) { if (name) { delete expandoStore.data[name]; - return; - } + } else { - if (expandoStore.handle) { - if (expandoStore.events.$destroy) { - expandoStore.handle({}, '$destroy'); + expandoStore.data = {}; } - jqLiteOff(element); - } - delete jqCache[expandoId]; - element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it + + removeIfEmptyData(element); } } @@ -616,6 +652,7 @@ forEach({ cleanData: function jqLiteCleanData(nodes) { for (var i = 0, ii = nodes.length; i < ii; i++) { jqLiteRemoveData(nodes[i]); + jqLiteOff(nodes[i]); } } }, function(fn, name) {