Skip to content

Commit eff866e

Browse files
backport angular#17028 to fix CVE-2020-7676
GHSA-mhp6-pxh8-r675 NOTE: since we only support using with jquery 3.5.1 in Salesforce/vlocity we've modified the tests to account for this and drop older versions of jquery.
1 parent 8302981 commit eff866e

13 files changed

+2031
-169
lines changed

Gruntfile.js

+5-9
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,7 @@ module.exports = function(grunt) {
136136
tests: {
137137
jqlite: 'karma-jqlite.conf.js',
138138
jquery: 'karma-jquery.conf.js',
139-
'jquery-2.2': 'karma-jquery-2.2.conf.js',
140-
'jquery-2.1': 'karma-jquery-2.1.conf.js',
139+
'jquery-3.5': 'karma-jquery-3.5.conf.js',
141140
docs: 'karma-docs.conf.js',
142141
modules: 'karma-modules.conf.js'
143142
},
@@ -146,8 +145,7 @@ module.exports = function(grunt) {
146145
autotest: {
147146
jqlite: 'karma-jqlite.conf.js',
148147
jquery: 'karma-jquery.conf.js',
149-
'jquery-2.2': 'karma-jquery-2.2.conf.js',
150-
'jquery-2.1': 'karma-jquery-2.1.conf.js',
148+
'jquery-3.5': 'karma-jquery-3.5.conf.js',
151149
modules: 'karma-modules.conf.js',
152150
docs: 'karma-docs.conf.js'
153151
},
@@ -403,10 +401,9 @@ module.exports = function(grunt) {
403401
'tests:docs',
404402
'test:protractor'
405403
]);
406-
grunt.registerTask('test:jqlite', 'Run the unit tests with Karma' , ['tests:jqlite']);
404+
grunt.registerTask('test:jqlite', 'Run the unit tests with Karma', ['tests:jqlite']);
407405
grunt.registerTask('test:jquery', 'Run the jQuery (latest) unit tests with Karma', ['tests:jquery']);
408-
grunt.registerTask('test:jquery-2.2', 'Run the jQuery 2.2 unit tests with Karma', ['tests:jquery-2.2']);
409-
grunt.registerTask('test:jquery-2.1', 'Run the jQuery 2.1 unit tests with Karma', ['tests:jquery-2.1']);
406+
grunt.registerTask('test:jquery-3.5', 'Run the jQuery 3.5 unit tests with Karma', ['tests:jquery-3.5']);
410407
grunt.registerTask('test:modules', 'Run the Karma module tests with Karma', [
411408
'build',
412409
'tests:modules'
@@ -415,8 +412,7 @@ module.exports = function(grunt) {
415412
grunt.registerTask('test:unit', 'Run unit, jQuery and Karma module tests with Karma', [
416413
'test:jqlite',
417414
'test:jquery',
418-
'test:jquery-2.2',
419-
'test:jquery-2.1',
415+
'test:jquery-3.5',
420416
'test:modules'
421417
]);
422418
grunt.registerTask('test:protractor', 'Run the end to end tests with Protractor and keep a test server running in the background', [

angularFiles.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ var angularFiles = {
244244
]
245245
};
246246

247-
['2.1', '2.2'].forEach(function(jQueryVersion) {
247+
['3.5'].forEach(function(jQueryVersion) {
248248
angularFiles['karmaJquery' + jQueryVersion] = []
249249
.concat(angularFiles.karmaJquery)
250250
.map(function(path) {

bower.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
"name": "angularjs",
33
"license": "MIT",
44
"devDependencies": {
5-
"jquery": "3.2.1",
6-
"jquery-2.2": "jquery#2.2.4",
7-
"jquery-2.1": "jquery#2.1.4",
5+
"jquery": "3.5.1",
6+
"jquery-3.5": "jquery#3.5.1",
87
"closure-compiler": "https://dl.google.com/closure-compiler/compiler-20140814.zip",
98
"ng-closure-runner": "https://raw.github.com/angular/ng-closure-runner/v0.2.4/assets/ng-closure-runner.zip"
109
}

karma-jquery-2.2.conf.js

-5
This file was deleted.

karma-jquery-2.1.conf.js karma-jquery-3.5.conf.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
var karmaConfigFactory = require('./karma-jquery.conf-factory');
44

5-
module.exports = karmaConfigFactory('2.1');
5+
module.exports = karmaConfigFactory('3.5');

lib/grunt/utils.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ module.exports = {
182182
var errorFileName = file.replace(/\.js$/, '-errors.json');
183183
var versionNumber = grunt.config('NG_VERSION').full;
184184
var compilationLevel = (file === 'build/angular-message-format.js') ?
185-
'ADVANCED_OPTIMIZATIONS' : 'SIMPLE_OPTIMIZATIONS';
185+
'ADVANCED_OPTIMIZATIONS' : 'SIMPLE_OPTIMIZATIONS';
186186
shell.exec(
187187
'java ' +
188188
this.java32flags() + ' ' +
@@ -221,7 +221,7 @@ module.exports = {
221221
//returns the 32-bit mode force flags for java compiler if supported, this makes the build much faster
222222
java32flags: function() {
223223
if (process.platform === 'win32') return '';
224-
if (shell.exec('java -version -d32 2>&1', {silent: true}).code !== 0) return '';
224+
if (shell.exec('java -d32 2>&1', {silent: true}).code !== 0) return '';
225225
return ' -d32 -client';
226226
},
227227

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@
6060
"jasmine-core": "2.5.2",
6161
"jasmine-node": "^2.0.0",
6262
"jasmine-reporters": "^2.2.0",
63-
"jquery": "^3.2.1",
64-
"karma": "^1.7.0",
63+
"jquery": "3.5.1",
64+
"jquery-3.5": "npm:[email protected]",
65+
"karma": "^2.0.0",
6566
"karma-browserstack-launcher": "^1.2.0",
6667
"karma-chrome-launcher": "^2.1.1",
6768
"karma-firefox-launcher": "^1.0.1",

scripts/travis/build.sh

+1-2
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ case "$JOB" in
3434
;;
3535
"unit-jquery")
3636
grunt test:jquery --browsers="$BROWSERS" --reporters=spec
37-
grunt test:jquery-2.2 --browsers="$BROWSERS" --reporters=spec
38-
grunt test:jquery-2.1 --browsers="$BROWSERS" --reporters=spec
37+
grunt test:jquery-3.5 --browsers="$BROWSERS" --reporters=spec
3938
;;
4039
"docs-app")
4140
grunt tests:docs --browsers="$BROWSERS" --reporters=spec

src/Angular.js

+20
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
hasOwnProperty,
9595
createMap,
9696
stringify,
97+
UNSAFE_restoreLegacyJqLiteXHTMLReplacement,
9798
9899
NODE_TYPE_ELEMENT,
99100
NODE_TYPE_ATTRIBUTE,
@@ -1964,6 +1965,25 @@ function bindJQuery() {
19641965
bindJQueryFired = true;
19651966
}
19661967

1968+
/**
1969+
* @ngdoc function
1970+
* @name angular.UNSAFE_restoreLegacyJqLiteXHTMLReplacement
1971+
* @module ng
1972+
* @kind function
1973+
*
1974+
* @description
1975+
* Restores the pre-1.8 behavior of jqLite that turns XHTML-like strings like
1976+
* `<div /><span />` to `<div></div><span></span>` instead of `<div><span></span></div>`.
1977+
* The new behavior is a security fix. Thus, if you need to call this function, please try to adjust
1978+
* your code for this change and remove your use of this function as soon as possible.
1979+
* Note that this only patches jqLite. If you use jQuery 3.5.0 or newer, please read the
1980+
* [jQuery 3.5 upgrade guide](https://jquery.com/upgrade-guide/3.5/) for more details
1981+
* about the workarounds.
1982+
*/
1983+
function UNSAFE_restoreLegacyJqLiteXHTMLReplacement() {
1984+
JQLite.legacyXHTMLReplacement = true;
1985+
}
1986+
19671987
/**
19681988
* throw error if the argument is falsy.
19691989
*/

src/AngularPublic.js

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
ngModelOptionsDirective,
5252
ngAttributeAliasDirectives,
5353
ngEventDirectives,
54+
UNSAFE_restoreLegacyJqLiteXHTMLReplacement,
5455
5556
$AnchorScrollProvider,
5657
$AnimateProvider,
@@ -155,6 +156,7 @@ function publishExternalAPI(angular) {
155156
'callbacks': {$$counter: 0},
156157
'getTestability': getTestability,
157158
'reloadWithDebugInfo': reloadWithDebugInfo,
159+
'UNSAFE_restoreLegacyJqLiteXHTMLReplacement': UNSAFE_restoreLegacyJqLiteXHTMLReplacement,
158160
'$$minErr': minErr,
159161
'$$csp': csp,
160162
'$$encodeUriSegment': encodeUriSegment,

src/jqLite.js

+58-15
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@
8989
* - [`val()`](http://api.jquery.com/val/)
9090
* - [`wrap()`](http://api.jquery.com/wrap/)
9191
*
92+
* jqLite also provides a method restoring pre-1.8 insecure treatment of XHTML-like tags.
93+
* This legacy behavior turns input like `<div /><span />` to `<div></div><span></span>`
94+
* instead of `<div><span></span></div>` like version 1.8 & newer do. To restore it, invoke:
95+
* ```js
96+
* angular.UNSAFE_restoreLegacyJqLiteXHTMLReplacement();
97+
* ```
98+
* Note that this only patches jqLite. If you use jQuery 3.5.0 or newer, please read the
99+
* [jQuery 3.5 upgrade guide](https://jquery.com/upgrade-guide/3.5/) for more details
100+
* about the workarounds.
101+
*
92102
* ## jQuery/jqLite Extras
93103
* Angular also provides the following additional methods and events to both jQuery and jqLite:
94104
*
@@ -168,20 +178,36 @@ var HTML_REGEXP = /<|&#?\w+;/;
168178
var TAG_NAME_REGEXP = /<([\w:-]+)/;
169179
var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi;
170180

181+
// Table parts need to be wrapped with `<table>` or they're
182+
// stripped to their contents when put in a div.
183+
// XHTML parsers do not magically insert elements in the
184+
// same way that tag soup parsers do, so we cannot shorten
185+
// this by omitting <tbody> or other required elements.
171186
var wrapMap = {
172-
'option': [1, '<select multiple="multiple">', '</select>'],
173-
174-
'thead': [1, '<table>', '</table>'],
175-
'col': [2, '<table><colgroup>', '</colgroup></table>'],
176-
'tr': [2, '<table><tbody>', '</tbody></table>'],
177-
'td': [3, '<table><tbody><tr>', '</tr></tbody></table>'],
178-
'_default': [0, '', '']
187+
thead: ['table'],
188+
col: ['colgroup', 'table'],
189+
tr: ['tbody', 'table'],
190+
td: ['tr', 'tbody', 'table']
179191
};
180192

181-
wrapMap.optgroup = wrapMap.option;
182193
wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
183194
wrapMap.th = wrapMap.td;
184195

196+
// Support: IE <10 only
197+
// IE 9 requires an option wrapper & it needs to have the whole table structure
198+
// set up in advance; assigning `"<td></td>"` to `tr.innerHTML` doesn't work, etc.
199+
var wrapMapIE9 = {
200+
option: [1, '<select multiple="multiple">', '</select>'],
201+
_default: [0, '', '']
202+
};
203+
204+
for (var key in wrapMap) {
205+
var wrapMapValueClosing = wrapMap[key];
206+
var wrapMapValue = wrapMapValueClosing.slice().reverse();
207+
wrapMapIE9[key] = [wrapMapValue.length, '<' + wrapMapValue.join('><') + '>', '</' + wrapMapValueClosing.join('></') + '>'];
208+
}
209+
210+
wrapMapIE9.optgroup = wrapMapIE9.option;
185211

186212
function jqLiteIsTextNode(html) {
187213
return !HTML_REGEXP.test(html);
@@ -202,7 +228,7 @@ function jqLiteHasData(node) {
202228
}
203229

204230
function jqLiteBuildFragment(html, context) {
205-
var tmp, tag, wrap,
231+
var tmp, tag, wrap, finalHtml,
206232
fragment = context.createDocumentFragment(),
207233
nodes = [], i;
208234

@@ -213,13 +239,30 @@ function jqLiteBuildFragment(html, context) {
213239
// Convert html into DOM nodes
214240
tmp = fragment.appendChild(context.createElement('div'));
215241
tag = (TAG_NAME_REGEXP.exec(html) || ['', ''])[1].toLowerCase();
216-
wrap = wrapMap[tag] || wrapMap._default;
217-
tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, '<$1></$2>') + wrap[2];
242+
finalHtml = JQLite.legacyXHTMLReplacement ?
243+
html.replace(XHTML_TAG_REGEXP, '<$1></$2>') :
244+
html;
245+
246+
if (msie < 10) {
247+
wrap = wrapMapIE9[tag] || wrapMapIE9._default;
248+
tmp.innerHTML = wrap[1] + finalHtml + wrap[2];
249+
250+
// Descend through wrappers to the right content
251+
i = wrap[0];
252+
while (i--) {
253+
tmp = tmp.firstChild;
254+
}
255+
} else {
256+
wrap = wrapMap[tag] || [];
218257

219-
// Descend through wrappers to the right content
220-
i = wrap[0];
221-
while (i--) {
222-
tmp = tmp.lastChild;
258+
// Create wrappers & descend into them
259+
i = wrap.length;
260+
while (--i > -1) {
261+
tmp.appendChild(window.document.createElement(wrap[i]));
262+
tmp = tmp.firstChild;
263+
}
264+
265+
tmp.innerHTML = finalHtml;
223266
}
224267

225268
nodes = concat(nodes, tmp.childNodes);

test/jqLiteSpec.js

+98-2
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ describe('jqLite', function() {
4242
var expect = jqLite(expected[i])[0];
4343
value = value && equals(expect, actual);
4444
msg = 'Not equal at index: ' + i
45-
+ ' - Expected: ' + expect
46-
+ ' - Actual: ' + actual;
45+
+ ' - Expected: ' + expect
46+
+ ' - Actual: ' + actual;
4747
}
4848
return { pass: value, message: message };
4949
}
@@ -147,6 +147,12 @@ describe('jqLite', function() {
147147
expect(nodes[0].nodeName.toLowerCase()).toBe('option');
148148
});
149149

150+
it('should allow construction of multiple <option> elements', function () {
151+
var nodes = jqLite('<option></option><option></option>');
152+
expect(nodes.length).toBe(2);
153+
expect(nodes[0].nodeName.toLowerCase()).toBe('option');
154+
expect(nodes[1].nodeName.toLowerCase()).toBe('option');
155+
});
150156

151157
// Special tests for the construction of elements which are restricted (in the HTML5 spec) to
152158
// being children of specific nodes.
@@ -171,6 +177,96 @@ describe('jqLite', function() {
171177
});
172178
});
173179

180+
181+
describe('security', function () {
182+
it('shouldn\'t crash at attempts to close the table wrapper', function() {
183+
// jQuery doesn't pass this test yet.
184+
if (!_jqLiteMode) return;
185+
186+
// Support: IE <10
187+
// In IE 9 we still need to use the old-style innerHTML assignment
188+
// as that's the only one that works.
189+
if (msie < 10) return;
190+
191+
expect(function () {
192+
// This test case attempts to close the tags which wrap input
193+
// based on matching done in wrapMap, escaping the wrapper & thus
194+
// triggering an error when descending.
195+
var el = jqLite('<td></td></tr></tbody></table><td></td>');
196+
expect(el.length).toBe(2);
197+
expect(el[0].nodeName.toLowerCase()).toBe('td');
198+
expect(el[1].nodeName.toLowerCase()).toBe('td');
199+
}).not.toThrow();
200+
});
201+
202+
it('shouldn\'t unsanitize sanitized code', function(done) {
203+
// jQuery <3.5.0 fail those tests.
204+
if (isJQuery2x()) {
205+
done();
206+
return;
207+
}
208+
209+
var counter = 0,
210+
assertCount = 13,
211+
container = jqLite('<div></div>');
212+
213+
function donePartial() {
214+
counter++;
215+
if (counter === assertCount) {
216+
container.remove();
217+
delete window.xss;
218+
done();
219+
}
220+
}
221+
222+
jqLite(document.body).append(container);
223+
window.xss = jasmine.createSpy('xss');
224+
225+
// Thanks to Masato Kinugawa from Cure53 for providing the following test cases.
226+
// Note: below test cases need to invoke the xss function with consecutive
227+
// decimal parameters for the assertions to be correct.
228+
forEach([
229+
'<img alt="<x" title="/><img src=url404 onerror=xss(0)>">',
230+
'<img alt="\n<x" title="/>\n<img src=url404 onerror=xss(1)>">',
231+
'<style><style/><img src=url404 onerror=xss(2)>',
232+
'<xmp><xmp/><img src=url404 onerror=xss(3)>',
233+
'<title><title /><img src=url404 onerror=xss(4)>',
234+
'<iframe><iframe/><img src=url404 onerror=xss(5)>',
235+
'<noframes><noframes/><img src=url404 onerror=xss(6)>',
236+
'<noscript><noscript/><img src=url404 onerror=xss(7)>',
237+
'<foo" alt="" title="/><img src=url404 onerror=xss(8)>">',
238+
'<img alt="<x" title="" src="/><img src=url404 onerror=xss(9)>">',
239+
'<noscript/><img src=url404 onerror=xss(10)>',
240+
'<noembed><noembed/><img src=url404 onerror=xss(11)>',
241+
242+
'<option><style></option></select><img src=url404 onerror=xss(12)></style>'
243+
], function(htmlString, index) {
244+
var element = jqLite('<div></div>');
245+
246+
container.append(element);
247+
element.append(jqLite(htmlString));
248+
249+
window.setTimeout(function() {
250+
expect(window.xss).not.toHaveBeenCalledWith(index);
251+
donePartial();
252+
}, 1000);
253+
});
254+
});
255+
256+
it('should allow to restore legacy insecure behavior', function () {
257+
// jQuery doesn't have this API.
258+
if (!_jqLiteMode) return;
259+
260+
// eslint-disable-next-line new-cap
261+
angular.UNSAFE_restoreLegacyJqLiteXHTMLReplacement();
262+
263+
var elem = jqLite('<div/><span/>');
264+
expect(elem.length).toBe(2);
265+
expect(elem[0].nodeName.toLowerCase()).toBe('div');
266+
expect(elem[1].nodeName.toLowerCase()).toBe('span');
267+
});
268+
});
269+
174270
describe('_data', function() {
175271
it('should provide access to the events present on the element', function() {
176272
var element = jqLite('<i>foo</i>');

0 commit comments

Comments
 (0)