Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 38deedd

Browse files
committed
fix($compile): reject multi-expression interpolations for src attribute
BREAKING CHANGE: Concatenating expressions makes it hard to reason about whether some combination of concatenated values are unsafe to use and could easily lead to XSS. By requiring that a single expression be used for *[src/ng-src] such as iframe[src], object[src], etc. (but not img[src/ng-src] since that value is sanitized), we ensure that the value that's used is assigned or constructed by some JS code somewhere that is more testable or make it obvious that you bound the value to some user controlled value. This helps reduce the load when auditing for XSS issues. To migrate your code, follow the example below: Before: JS: scope.baseUrl = 'page'; scope.a = 1; scope.b = 2; HTML: <!-- Are a and b properly escaped here? Is baseUrl controlled by user? --> <iframe src="{{baseUrl}}?a={{a}&b={{b}}"> After: JS: var baseUrl = "page"; scope.getIframeSrc = function() { // There are obviously better ways to do this. The // key point is that one will think about this and do // it the right way. var qs = ["a", "b"].map(function(value, name) { return encodeURIComponent(name) + "=" + encodeURIComponent(value); }).join("&"); // baseUrl isn't on scope so it isn't bound to a user // controlled value. return baseUrl + "?" + qs; } HTML: <iframe src="{{getIframeSrc()}}">
1 parent 39841f2 commit 38deedd

File tree

4 files changed

+82
-6
lines changed

4 files changed

+82
-6
lines changed

src/ng/compile.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -1157,6 +1157,16 @@ function $CompileProvider($provide) {
11571157
}
11581158

11591159

1160+
function isTrustedContext(node, attrNormalizedName) {
1161+
// maction[xlink:href] can source SVG. It's not limited to <maction>.
1162+
if (attrNormalizedName == "xlinkHref" ||
1163+
(nodeName_(node) != "IMG" && (attrNormalizedName == "src" ||
1164+
attrNormalizedName == "ngSrc"))) {
1165+
return true;
1166+
}
1167+
}
1168+
1169+
11601170
function addAttrInterpolateDirective(node, directives, value, name) {
11611171
var interpolateFn = $interpolate(value, true);
11621172

@@ -1177,7 +1187,7 @@ function $CompileProvider($provide) {
11771187

11781188
// we need to interpolate again, in case the attribute value has been updated
11791189
// (e.g. by another directive's compile function)
1180-
interpolateFn = $interpolate(attr[name], true);
1190+
interpolateFn = $interpolate(attr[name], true, isTrustedContext(node, name));
11811191

11821192
// if attribute was updated so that there is no interpolation going on we don't want to
11831193
// register any observers

src/ng/interpolate.js

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
var $interpolateMinErr = minErr('$interpolate');
4+
35
/**
46
* @ngdoc object
57
* @name ng.$interpolateProvider
@@ -82,14 +84,20 @@ function $InterpolateProvider() {
8284
* @param {boolean=} mustHaveExpression if set to true then the interpolation string must have
8385
* embedded expression in order to return an interpolation function. Strings with no
8486
* embedded expression will return null for the interpolation function.
87+
* @param {boolean=} isTrustedContext when true, requires that the interpolation string does not
88+
* contain any concatenations - i.e. the interpolation string is a single expression.
89+
* Interpolations for *[src] and *[ng-src] (except IMG, since itwhich sanitizes its value)
90+
* pass true for this parameter. This helps avoid hunting through the template code to
91+
* figure out of some iframe[src], object[src], etc. was interpolated with a concatenation
92+
* that ended up introducing a XSS.
8593
* @returns {function(context)} an interpolation function which is used to compute the interpolated
8694
* string. The function has these parameters:
8795
*
8896
* * `context`: an object against which any expressions embedded in the strings are evaluated
8997
* against.
9098
*
9199
*/
92-
function $interpolate(text, mustHaveExpression) {
100+
function $interpolate(text, mustHaveExpression, isTrustedContext) {
93101
var startIndex,
94102
endIndex,
95103
index = 0,
@@ -121,6 +129,18 @@ function $InterpolateProvider() {
121129
length = 1;
122130
}
123131

132+
// Concatenating expressions makes it hard to reason about whether some combination of concatenated
133+
// values are unsafe to use and could easily lead to XSS. By requiring that a single
134+
// expression be used for iframe[src], object[src], etc., we ensure that the value that's used
135+
// is assigned or constructed by some JS code somewhere that is more testable or make it
136+
// obvious that you bound the value to some user controlled value. This helps reduce the load
137+
// when auditing for XSS issues.
138+
if (isTrustedContext && parts.length > 1) {
139+
throw $interpolateMinErr('noconcat',
140+
"Error while interpolating: {0}\nYou may not use multiple expressions when " +
141+
"interpolating this expression.", text);
142+
}
143+
124144
if (!mustHaveExpression || hasInterpolation) {
125145
concat.length = length;
126146
fn = function(context) {
@@ -139,7 +159,7 @@ function $InterpolateProvider() {
139159
return concat.join('');
140160
}
141161
catch(err) {
142-
var newErr = minErr('$interpolate')('interr', "Can't interpolate: {0}\n{1}", text, err.toString());
162+
var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, err.toString());
143163
$exceptionHandler(newErr);
144164
}
145165
};

test/ng/directive/booleanAttrsSpec.js

+25-3
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,41 @@ describe('boolean attr directives', function() {
8989

9090
describe('ngSrc', function() {
9191
it('should interpolate the expression and bind to src', inject(function($compile, $rootScope) {
92-
var element = $compile('<div ng-src="some/{{id}}"></div>')($rootScope);
92+
var element = $compile('<div ng-src="{{id}}"></div>')($rootScope);
9393

9494
$rootScope.$digest();
95-
expect(element.attr('src')).toEqual('some/');
95+
expect(element.attr('src')).toBeUndefined();
9696

9797
$rootScope.$apply(function() {
9898
$rootScope.id = 1;
9999
});
100-
expect(element.attr('src')).toEqual('some/1');
100+
expect(element.attr('src')).toEqual('1');
101101

102102
dealoc(element);
103103
}));
104104

105+
describe('isTrustedContext', function() {
106+
it('should NOT interpolate a multi-part expression for non-img src attribute', inject(function($compile, $rootScope) {
107+
expect(function() {
108+
var element = $compile('<div ng-src="some/{{id}}"></div>')($rootScope);
109+
dealoc(element);
110+
}).toThrow(
111+
"[$interpolate:noconcat] Error while interpolating: some/{{id}}\nYou may not use " +
112+
"multiple expressions when interpolating this expression.");
113+
}));
114+
115+
it('should interpolate a multi-part expression for regular attributes', inject(function($compile, $rootScope) {
116+
var element = $compile('<div foo="some/{{id}}"></div>')($rootScope);
117+
$rootScope.$digest();
118+
expect(element.attr('foo')).toBe('some/');
119+
$rootScope.$apply(function() {
120+
$rootScope.id = 1;
121+
});
122+
expect(element.attr('foo')).toEqual('some/1');
123+
}));
124+
125+
});
126+
105127
if (msie) {
106128
it('should update the element property as well as the attribute', inject(
107129
function($compile, $rootScope) {

test/ng/interpolateSpec.js

+24
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,29 @@ describe('$interpolate', function() {
149149
});
150150

151151

152+
describe('isTrustedContext', function() {
153+
it('should NOT interpolate a multi-part expression when isTrustedContext is true', inject(function($interpolate) {
154+
var isTrustedContext = true;
155+
expect(function() {
156+
$interpolate('constant/{{var}}', true, isTrustedContext);
157+
}).toThrow(
158+
"[$interpolate:noconcat] Error while interpolating: constant/{{var}}\nYou may not use " +
159+
"multiple expressions when interpolating this expression.");
160+
expect(function() {
161+
$interpolate('{{foo}}{{bar}}', true, isTrustedContext);
162+
}).toThrow(
163+
"[$interpolate:noconcat] Error while interpolating: {{foo}}{{bar}}\nYou may not use " +
164+
"multiple expressions when interpolating this expression.");
165+
}));
166+
167+
it('should interpolate a multi-part expression when isTrustedContext is false', inject(function($interpolate) {
168+
expect($interpolate('some/{{id}}')()).toEqual('some/');
169+
expect($interpolate('some/{{id}}')({id: 1})).toEqual('some/1');
170+
expect($interpolate('{{foo}}{{bar}}')({foo: 1, bar: 2})).toEqual('12');
171+
}));
172+
});
173+
174+
152175
describe('startSymbol', function() {
153176

154177
beforeEach(module(function($interpolateProvider) {
@@ -199,4 +222,5 @@ describe('$interpolate', function() {
199222
expect($interpolate.endSymbol()).toBe('))');
200223
}));
201224
});
225+
202226
});

0 commit comments

Comments
 (0)