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('><') + '>', '' + wrapMapValueClosing.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>$2>') + wrap[2];
+ finalHtml = JQLite.legacyXHTMLReplacement ?
+ html.replace(XHTML_TAG_REGEXP, '<$1>$2>') :
+ 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) {