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

Commit 2848899

Browse files
fix(interpolate): sanitize non-interpolated expressions for URL contexts
The MEDIA_URL and URL $sce contexts are used to describe URLs that can be used in anchor links and image sources, etc. The previous commit introduced new behaviour where we did not sanitize URL expressions if they did not contain an interpolation. This meant that hard coded `ng-href` attributes would not be sanitized. This change forces the `$interpolate` service to check the trust (and so run sanitization) for all expressions that require the `URL` or `MEDIA_URL` trusted contexts.
1 parent 7202231 commit 2848899

File tree

6 files changed

+235
-229
lines changed

6 files changed

+235
-229
lines changed

src/ng/interpolate.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -238,16 +238,21 @@ function $InterpolateProvider() {
238238
* - `context`: evaluation context for all expressions embedded in the interpolated text
239239
*/
240240
function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) {
241+
var contextAllowsConcatenation = trustedContext === $sce.URL || trustedContext === $sce.MEDIA_URL;
242+
241243
// Provide a quick exit and simplified result function for text with no interpolation
242244
if (!text.length || text.indexOf(startSymbol) === -1) {
243-
var constantInterp;
244-
if (!mustHaveExpression) {
245-
var unescapedText = unescapeText(text);
246-
constantInterp = valueFn(unescapedText);
247-
constantInterp.exp = text;
248-
constantInterp.expressions = [];
249-
constantInterp.$$watchDelegate = constantWatchDelegate;
245+
if (mustHaveExpression && !contextAllowsConcatenation) return;
246+
247+
var unescapedText = unescapeText(text);
248+
if (contextAllowsConcatenation) {
249+
unescapedText = $sce.getTrusted(trustedContext, unescapedText);
250250
}
251+
var constantInterp = valueFn(unescapedText);
252+
constantInterp.exp = text;
253+
constantInterp.expressions = [];
254+
constantInterp.$$watchDelegate = constantWatchDelegate;
255+
251256
return constantInterp;
252257
}
253258

@@ -261,8 +266,8 @@ function $InterpolateProvider() {
261266
exp,
262267
concat = [],
263268
expressionPositions = [],
264-
singleExpression,
265-
contextAllowsConcatenation = trustedContext === $sce.URL || trustedContext === $sce.MEDIA_URL;
269+
singleExpression;
270+
266271

267272
while (index < textLength) {
268273
if (((startIndex = text.indexOf(startSymbol, index)) !== -1) &&

test/ng/compileSpec.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3414,6 +3414,15 @@ describe('$compile', function() {
34143414
})
34153415
);
34163416

3417+
it('should interpolate a multi-part expression for regular attributes', inject(function($compile, $rootScope) {
3418+
element = $compile('<div foo="some/{{id}}"></div>')($rootScope);
3419+
$rootScope.$digest();
3420+
expect(element.attr('foo')).toBe('some/');
3421+
$rootScope.$apply(function() {
3422+
$rootScope.id = 1;
3423+
});
3424+
expect(element.attr('foo')).toEqual('some/1');
3425+
}));
34173426

34183427
it('should process attribute interpolation in pre-linking phase at priority 100', function() {
34193428
module(function() {

test/ng/directive/booleanAttrsSpec.js

Lines changed: 0 additions & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -118,221 +118,3 @@ describe('boolean attr directives', function() {
118118
}));
119119
});
120120
});
121-
122-
123-
describe('ngSrc', function() {
124-
it('should interpolate the expression and bind to src with raw same-domain value',
125-
inject(function($compile, $rootScope) {
126-
var element = $compile('<img ng-src="{{id}}"></img>')($rootScope);
127-
128-
$rootScope.$digest();
129-
expect(element.attr('src')).toBeUndefined();
130-
131-
$rootScope.$apply(function() {
132-
$rootScope.id = '/somewhere/here';
133-
});
134-
expect(element.attr('src')).toEqual('/somewhere/here');
135-
136-
dealoc(element);
137-
}));
138-
139-
140-
it('should interpolate the expression and bind to src with a trusted value', inject(function($compile, $rootScope, $sce) {
141-
var element = $compile('<iframe ng-src="{{id}}"></iframe>')($rootScope);
142-
143-
$rootScope.$digest();
144-
expect(element.attr('src')).toBeUndefined();
145-
146-
$rootScope.$apply(function() {
147-
$rootScope.id = $sce.trustAsResourceUrl('http://somewhere');
148-
});
149-
expect(element.attr('src')).toEqual('http://somewhere');
150-
151-
dealoc(element);
152-
}));
153-
154-
155-
it('should NOT interpolate a multi-part expression in a `src` attribute that requires a non-MEDIA_URL context', inject(function($compile, $rootScope) {
156-
expect(function() {
157-
var element = $compile('<iframe ng-src="some/{{id}}"></iframe>')($rootScope);
158-
$rootScope.$apply(function() {
159-
$rootScope.id = 1;
160-
});
161-
dealoc(element);
162-
}).toThrowMinErr(
163-
'$interpolate', 'noconcat', 'Error while interpolating: some/{{id}}\nStrict ' +
164-
'Contextual Escaping disallows interpolations that concatenate multiple expressions ' +
165-
'when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce');
166-
}));
167-
168-
it('should interpolate a multi-part expression for img src attribute (which requires the MEDIA_URL context)', inject(function($compile, $rootScope) {
169-
var element = $compile('<img ng-src="some/{{id}}"></img>')($rootScope);
170-
expect(element.attr('src')).toBe(undefined); // URL concatenations are all-or-nothing
171-
$rootScope.$apply(function() {
172-
$rootScope.id = 1;
173-
});
174-
expect(element.attr('src')).toEqual('some/1');
175-
}));
176-
177-
178-
it('should interpolate a multi-part expression for regular attributes', inject(function($compile, $rootScope) {
179-
var element = $compile('<div foo="some/{{id}}"></div>')($rootScope);
180-
$rootScope.$digest();
181-
expect(element.attr('foo')).toBe('some/');
182-
$rootScope.$apply(function() {
183-
$rootScope.id = 1;
184-
});
185-
expect(element.attr('foo')).toEqual('some/1');
186-
}));
187-
188-
189-
it('should NOT interpolate a wrongly typed expression', inject(function($compile, $rootScope, $sce) {
190-
expect(function() {
191-
var element = $compile('<iframe ng-src="{{id}}"></iframe>')($rootScope);
192-
$rootScope.$apply(function() {
193-
$rootScope.id = $sce.trustAsUrl('http://somewhere');
194-
});
195-
element.attr('src');
196-
}).toThrowMinErr(
197-
'$interpolate', 'interr', 'Can\'t interpolate: {{id}}\nError: [$sce:insecurl] Blocked ' +
198-
'loading resource from url not allowed by $sceDelegate policy. URL: http://somewhere');
199-
}));
200-
201-
202-
// Support: IE 9-11 only
203-
if (msie) {
204-
it('should update the element property as well as the attribute', inject(
205-
function($compile, $rootScope, $sce) {
206-
// on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
207-
// then calling element.setAttribute('src', 'foo') doesn't do anything, so we need
208-
// to set the property as well to achieve the desired effect
209-
210-
var element = $compile('<img ng-src="{{id}}"></img>')($rootScope);
211-
212-
$rootScope.$digest();
213-
expect(element.prop('src')).toBe('');
214-
dealoc(element);
215-
216-
element = $compile('<img ng-src="some/"></img>')($rootScope);
217-
218-
$rootScope.$digest();
219-
expect(element.prop('src')).toMatch('/some/$');
220-
dealoc(element);
221-
222-
element = $compile('<img ng-src="{{id}}"></img>')($rootScope);
223-
$rootScope.$apply(function() {
224-
$rootScope.id = $sce.trustAsResourceUrl('http://somewhere/abc');
225-
});
226-
expect(element.prop('src')).toEqual('http://somewhere/abc');
227-
228-
dealoc(element);
229-
}));
230-
}
231-
});
232-
233-
234-
describe('ngSrcset', function() {
235-
it('should interpolate the expression and bind to srcset', inject(function($compile, $rootScope) {
236-
var element = $compile('<img ng-srcset="some/{{id}} 2x"></div>')($rootScope);
237-
238-
$rootScope.$digest();
239-
expect(element.attr('srcset')).toBeUndefined();
240-
241-
$rootScope.$apply(function() {
242-
$rootScope.id = 1;
243-
});
244-
expect(element.attr('srcset')).toEqual('some/1 2x');
245-
246-
dealoc(element);
247-
}));
248-
});
249-
250-
251-
describe('ngHref', function() {
252-
var element;
253-
254-
afterEach(function() {
255-
dealoc(element);
256-
});
257-
258-
259-
it('should interpolate the expression and bind to href', inject(function($compile, $rootScope) {
260-
element = $compile('<a ng-href="some/{{id}}"></div>')($rootScope);
261-
$rootScope.$digest();
262-
expect(element.attr('href')).toEqual('some/');
263-
264-
$rootScope.$apply(function() {
265-
$rootScope.id = 1;
266-
});
267-
expect(element.attr('href')).toEqual('some/1');
268-
}));
269-
270-
271-
it('should bind href and merge with other attrs', inject(function($rootScope, $compile) {
272-
element = $compile('<a ng-href="{{url}}" rel="{{rel}}"></a>')($rootScope);
273-
$rootScope.url = 'http://server';
274-
$rootScope.rel = 'REL';
275-
$rootScope.$digest();
276-
expect(element.attr('href')).toEqual('http://server');
277-
expect(element.attr('rel')).toEqual('REL');
278-
}));
279-
280-
281-
it('should bind href even if no interpolation', inject(function($rootScope, $compile) {
282-
element = $compile('<a ng-href="http://server"></a>')($rootScope);
283-
$rootScope.$digest();
284-
expect(element.attr('href')).toEqual('http://server');
285-
}));
286-
287-
it('should not set the href if ng-href is empty', inject(function($rootScope, $compile) {
288-
$rootScope.url = null;
289-
element = $compile('<a ng-href="{{url}}">')($rootScope);
290-
$rootScope.$digest();
291-
expect(element.attr('href')).toEqual(undefined);
292-
}));
293-
294-
it('should remove the href if ng-href changes to empty', inject(function($rootScope, $compile) {
295-
$rootScope.url = 'http://www.google.com/';
296-
element = $compile('<a ng-href="{{url}}">')($rootScope);
297-
$rootScope.$digest();
298-
299-
$rootScope.url = null;
300-
$rootScope.$digest();
301-
expect(element.attr('href')).toEqual(undefined);
302-
}));
303-
304-
// Support: IE 9-11 only, Edge 12-15+
305-
if (msie || /\bEdge\/[\d.]+\b/.test(window.navigator.userAgent)) {
306-
// IE/Edge fail when setting a href to a URL containing a % that isn't a valid escape sequence
307-
// See https://github.com/angular/angular.js/issues/13388
308-
it('should throw error if ng-href contains a non-escaped percent symbol', inject(function($rootScope, $compile) {
309-
expect(function() {
310-
element = $compile('<a ng-href="http://www.google.com/{{\'a%link\'}}">')($rootScope);
311-
}).toThrow();
312-
}));
313-
}
314-
315-
if (isDefined(window.SVGElement)) {
316-
describe('SVGAElement', function() {
317-
it('should interpolate the expression and bind to xlink:href', inject(function($compile, $rootScope) {
318-
element = $compile('<svg><a ng-href="some/{{id}}"></a></svg>')($rootScope);
319-
var child = element.children('a');
320-
$rootScope.$digest();
321-
expect(child.attr('xlink:href')).toEqual('some/');
322-
323-
$rootScope.$apply(function() {
324-
$rootScope.id = 1;
325-
});
326-
expect(child.attr('xlink:href')).toEqual('some/1');
327-
}));
328-
329-
330-
it('should bind xlink:href even if no interpolation', inject(function($rootScope, $compile) {
331-
element = $compile('<svg><a ng-href="http://server"></a></svg>')($rootScope);
332-
var child = element.children('a');
333-
$rootScope.$digest();
334-
expect(child.attr('xlink:href')).toEqual('http://server');
335-
}));
336-
});
337-
}
338-
});

test/ng/directive/ngHrefSpec.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
'use strict';
2+
3+
describe('ngHref', function() {
4+
var element;
5+
6+
afterEach(function() {
7+
dealoc(element);
8+
});
9+
10+
11+
it('should interpolate the expression and bind to href', inject(function($compile, $rootScope) {
12+
element = $compile('<a ng-href="some/{{id}}"></div>')($rootScope);
13+
$rootScope.$digest();
14+
expect(element.attr('href')).toEqual('some/');
15+
16+
$rootScope.$apply(function() {
17+
$rootScope.id = 1;
18+
});
19+
expect(element.attr('href')).toEqual('some/1');
20+
}));
21+
22+
23+
it('should bind href and merge with other attrs', inject(function($rootScope, $compile) {
24+
element = $compile('<a ng-href="{{url}}" rel="{{rel}}"></a>')($rootScope);
25+
$rootScope.url = 'http://server';
26+
$rootScope.rel = 'REL';
27+
$rootScope.$digest();
28+
expect(element.attr('href')).toEqual('http://server');
29+
expect(element.attr('rel')).toEqual('REL');
30+
}));
31+
32+
33+
it('should bind href even if no interpolation', inject(function($rootScope, $compile) {
34+
element = $compile('<a ng-href="http://server"></a>')($rootScope);
35+
$rootScope.$digest();
36+
expect(element.attr('href')).toEqual('http://server');
37+
}));
38+
39+
it('should not set the href if ng-href is empty', inject(function($rootScope, $compile) {
40+
$rootScope.url = null;
41+
element = $compile('<a ng-href="{{url}}">')($rootScope);
42+
$rootScope.$digest();
43+
expect(element.attr('href')).toEqual(undefined);
44+
}));
45+
46+
it('should remove the href if ng-href changes to empty', inject(function($rootScope, $compile) {
47+
$rootScope.url = 'http://www.google.com/';
48+
element = $compile('<a ng-href="{{url}}">')($rootScope);
49+
$rootScope.$digest();
50+
51+
$rootScope.url = null;
52+
$rootScope.$digest();
53+
expect(element.attr('href')).toEqual(undefined);
54+
}));
55+
56+
it('should sanitize interpolated url', inject(function($rootScope, $compile) {
57+
/* eslint no-script-url: "off" */
58+
$rootScope.imageUrl = 'javascript:alert(1);';
59+
element = $compile('<a ng-href="{{imageUrl}}">')($rootScope);
60+
$rootScope.$digest();
61+
expect(element.attr('href')).toBe('unsafe:javascript:alert(1);');
62+
}));
63+
64+
it('should sanitize non-interpolated url', inject(function($rootScope, $compile) {
65+
element = $compile('<a ng-href="javascript:alert(1);">')($rootScope);
66+
$rootScope.$digest();
67+
expect(element.attr('href')).toBe('unsafe:javascript:alert(1);');
68+
}));
69+
70+
71+
// Support: IE 9-11 only, Edge 12-15+
72+
if (msie || /\bEdge\/[\d.]+\b/.test(window.navigator.userAgent)) {
73+
// IE/Edge fail when setting a href to a URL containing a % that isn't a valid escape sequence
74+
// See https://github.com/angular/angular.js/issues/13388
75+
it('should throw error if ng-href contains a non-escaped percent symbol', inject(function($rootScope, $compile) {
76+
expect(function() {
77+
element = $compile('<a ng-href="http://www.google.com/{{\'a%link\'}}">')($rootScope);
78+
}).toThrow();
79+
}));
80+
}
81+
82+
if (isDefined(window.SVGElement)) {
83+
describe('SVGAElement', function() {
84+
it('should interpolate the expression and bind to xlink:href', inject(function($compile, $rootScope) {
85+
element = $compile('<svg><a ng-href="some/{{id}}"></a></svg>')($rootScope);
86+
var child = element.children('a');
87+
$rootScope.$digest();
88+
expect(child.attr('xlink:href')).toEqual('some/');
89+
90+
$rootScope.$apply(function() {
91+
$rootScope.id = 1;
92+
});
93+
expect(child.attr('xlink:href')).toEqual('some/1');
94+
}));
95+
96+
97+
it('should bind xlink:href even if no interpolation', inject(function($rootScope, $compile) {
98+
element = $compile('<svg><a ng-href="http://server"></a></svg>')($rootScope);
99+
var child = element.children('a');
100+
$rootScope.$digest();
101+
expect(child.attr('xlink:href')).toEqual('http://server');
102+
}));
103+
});
104+
}
105+
});

0 commit comments

Comments
 (0)