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

Commit 68bfbf9

Browse files
committed
feat($sce): handle URLs through the $sce service.
This is a large patch to handle URLs with the $sce service, similarly to HTML context. However, to keep rough compatibility with existing apps, we need to allow URL-context concatenation, since previously $sce contexts prevented sanitization. There's now a set of special contexts (defined in $interpolate) that allow concatenation, in a roughly intuitive way: trusted types alone are trusted, and any concatenation of values results in a non-trusted type that will be handled by getTrusted once the concatenation is done. Thus, "{{safeType}}" will not be sanitized, "{{ 'javascript:foo' }}" will be, and "javascript:{{safeType}}" will also be. There's backwards incompatible changes in there, mostly the fusion of the two URL context sanitization whitelists (the separation is nice, but not important for security).
1 parent d2cc451 commit 68bfbf9

File tree

11 files changed

+381
-242
lines changed

11 files changed

+381
-242
lines changed

src/ng/compile.js

Lines changed: 25 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,14 +1119,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
11191119

11201120
/**
11211121
* @ngdoc method
1122-
* @name $compileProvider#aHrefSanitizationWhitelist
1122+
* @name $compileProvider#uriSanitizationWhitelist
11231123
* @kind function
11241124
*
11251125
* @description
11261126
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
1127-
* urls during a[href] sanitization.
1127+
* urls during URL-context sanitization.
11281128
*
1129-
* The sanitization is a security measure aimed at preventing XSS attacks via html links.
1129+
* The sanitization is a security measure aimed at preventing XSS attacks.
11301130
*
11311131
* Any url about to be assigned to a[href] via data-binding is first normalized and turned into
11321132
* an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
@@ -1137,45 +1137,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
11371137
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
11381138
* chaining otherwise.
11391139
*/
1140-
this.aHrefSanitizationWhitelist = function(regexp) {
1140+
this.uriSanitizationWhitelist = function(regexp) {
11411141
if (isDefined(regexp)) {
1142-
$$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp);
1142+
$$sanitizeUriProvider.uriSanitizationWhitelist(regexp);
11431143
return this;
11441144
} else {
1145-
return $$sanitizeUriProvider.aHrefSanitizationWhitelist();
1145+
return $$sanitizeUriProvider.uriSanitizationWhitelist();
11461146
}
11471147
};
11481148

11491149

1150-
/**
1151-
* @ngdoc method
1152-
* @name $compileProvider#imgSrcSanitizationWhitelist
1153-
* @kind function
1154-
*
1155-
* @description
1156-
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
1157-
* urls during img[src] sanitization.
1158-
*
1159-
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
1160-
*
1161-
* Any url about to be assigned to img[src] via data-binding is first normalized and turned into
1162-
* an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist`
1163-
* regular expression. If a match is found, the original url is written into the dom. Otherwise,
1164-
* the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
1165-
*
1166-
* @param {RegExp=} regexp New regexp to whitelist urls with.
1167-
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
1168-
* chaining otherwise.
1169-
*/
1170-
this.imgSrcSanitizationWhitelist = function(regexp) {
1171-
if (isDefined(regexp)) {
1172-
$$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp);
1173-
return this;
1174-
} else {
1175-
return $$sanitizeUriProvider.imgSrcSanitizationWhitelist();
1176-
}
1177-
};
1178-
11791150
/**
11801151
* @ngdoc method
11811152
* @name $compileProvider#debugInfoEnabled
@@ -1209,9 +1180,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
12091180

12101181
this.$get = [
12111182
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
1212-
'$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri',
1183+
'$controller', '$rootScope', '$sce', '$animate',
12131184
function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
1214-
$controller, $rootScope, $sce, $animate, $$sanitizeUri) {
1185+
$controller, $rootScope, $sce, $animate) {
12151186

12161187
var SIMPLE_ATTR_NAME = /^\w/;
12171188
var specialAttrHolder = document.createElement('div');
@@ -1350,11 +1321,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
13501321

13511322
nodeName = nodeName_(this.$$element);
13521323

1353-
if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) ||
1354-
(nodeName === 'img' && key === 'src')) {
1355-
// sanitize a[href] and img[src] values
1356-
this[key] = value = $$sanitizeUri(value, key === 'src');
1357-
} else if (nodeName === 'img' && key === 'srcset') {
1324+
// img[srcset] is a bit too weird of a beast to handle through $sce.
1325+
// Instead, for now at least, sanitize each of the URIs individually.
1326+
// That works even dynamically, but it's not bypassable through the $sce.
1327+
// Instead, if you want several unsafe URLs as-is, you should probably
1328+
// use trustAsHtml on the whole tag.
1329+
if (nodeName === 'img' && key === 'srcset') {
1330+
13581331
// sanitize img[srcset] values
13591332
var result = "";
13601333

@@ -1372,16 +1345,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
13721345
for (var i = 0; i < nbrUrisWith2parts; i++) {
13731346
var innerIdx = i * 2;
13741347
// sanitize the uri
1375-
result += $$sanitizeUri(trim(rawUris[innerIdx]), true);
1348+
result += $sce.getTrustedUrl(trim(rawUris[innerIdx]));
13761349
// add the descriptor
1377-
result += (" " + trim(rawUris[innerIdx + 1]));
1350+
result += " " + trim(rawUris[innerIdx + 1]);
13781351
}
13791352

13801353
// split the last item into uri and descriptor
13811354
var lastTuple = trim(rawUris[i * 2]).split(/\s/);
13821355

13831356
// sanitize the last uri
1384-
result += $$sanitizeUri(trim(lastTuple[0]), true);
1357+
result += $sce.getTrustedUrl(trim(lastTuple[0]));
13851358

13861359
// and add the last descriptor if any
13871360
if (lastTuple.length === 2) {
@@ -2815,6 +2788,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
28152788
(tag != "img" && (attrNormalizedName == "src" ||
28162789
attrNormalizedName == "ngSrc"))) {
28172790
return $sce.RESOURCE_URL;
2791+
} else if (attrNormalizedName == "src" ||
2792+
attrNormalizedName == "href" ||
2793+
attrNormalizedName == "ngHref" ||
2794+
attrNormalizedName == "ngSrc") {
2795+
// All that is not a RESOURCE_URL but still a URL. This might be overkill for some
2796+
// attributes, but the sanitization is permissive, so it should not be troublesome.
2797+
return $sce.URL;
28182798
}
28192799
}
28202800

src/ng/interpolate.js

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ var $interpolateMinErr = angular.$interpolateMinErr = minErr('$interpolate');
44
$interpolateMinErr.throwNoconcat = function(text) {
55
throw $interpolateMinErr('noconcat',
66
"Error while interpolating: {0}\nStrict Contextual Escaping disallows " +
7-
"interpolations that concatenate multiple expressions when a trusted value is " +
8-
"required. See http://docs.angularjs.org/api/ng.$sce", text);
7+
"interpolations that concatenate multiple expressions in some secure contexts. " +
8+
"See http://docs.angularjs.org/api/ng.$sce", text);
99
};
1010

1111
$interpolateMinErr.interr = function(text, err) {
@@ -137,6 +137,10 @@ function $InterpolateProvider() {
137137
}, listener, objectEquality);
138138
}
139139

140+
function isConcatenationAllowed(context) {
141+
return context == $sce.URL;
142+
}
143+
140144
/**
141145
* @ngdoc service
142146
* @name $interpolate
@@ -254,7 +258,9 @@ function $InterpolateProvider() {
254258
textLength = text.length,
255259
exp,
256260
concat = [],
257-
expressionPositions = [];
261+
expressionPositions = [],
262+
singleExpression = false,
263+
contextAllowsConcatenation = isConcatenationAllowed(trustedContext);
258264

259265
while (index < textLength) {
260266
if (((startIndex = text.indexOf(startSymbol, index)) != -1) &&
@@ -267,7 +273,7 @@ function $InterpolateProvider() {
267273
parseFns.push($parse(exp, parseStringifyInterceptor));
268274
index = endIndex + endSymbolLength;
269275
expressionPositions.push(concat.length);
270-
concat.push('');
276+
concat.push(''); // Placeholder that will get replaced with the evaluated expression.
271277
} else {
272278
// we did not find an interpolation, so we have to add the remainder to the separators array
273279
if (index !== textLength) {
@@ -277,27 +283,54 @@ function $InterpolateProvider() {
277283
}
278284
}
279285

286+
if (concat.length == 1 && expressionPositions.length == 1) {
287+
singleExpression = true;
288+
}
289+
280290
// Concatenating expressions makes it hard to reason about whether some combination of
281291
// concatenated values are unsafe to use and could easily lead to XSS. By requiring that a
282-
// single expression be used for iframe[src], object[src], etc., we ensure that the value
283-
// that's used is assigned or constructed by some JS code somewhere that is more testable or
284-
// make it obvious that you bound the value to some user controlled value. This helps reduce
285-
// the load when auditing for XSS issues.
286-
if (trustedContext && concat.length > 1) {
287-
$interpolateMinErr.throwNoconcat(text);
288-
}
292+
// single expression be used for some $sce-managed secure contexts (RESOURCE_URLs mostly),
293+
// we ensure that the value that's used is assigned or constructed by some JS code somewhere
294+
// that is more testable or make it obvious that you bound the value to some user controlled
295+
// value. This helps reduce the load when auditing for XSS issues.
296+
// Note that URL-context is dispensed of this, since its getTrusted method can sanitize.
297+
// In that context, .getTrusted will be called on either the single expression or on the
298+
// overall concatenated string (losing trusted types used in the mix, by design). Both these
299+
// methods will sanitize plain strings. Also, HTML could be included, but since it's only used
300+
// in srcdoc attributes, this would not be very useful.
289301

290302
if (!mustHaveExpression || expressions.length) {
291303
var compute = function(values) {
292304
for (var i = 0, ii = expressions.length; i < ii; i++) {
293305
if (allOrNothing && isUndefined(values[i])) return;
294306
concat[expressionPositions[i]] = values[i];
295307
}
296-
return concat.join('');
308+
309+
if (contextAllowsConcatenation) {
310+
if (singleExpression) {
311+
// The raw value was left as-is by parseStringifyInterceptor
312+
return $sce.getTrusted(trustedContext, concat[0]);
313+
} else {
314+
return $sce.getTrusted(trustedContext, concat.join(''));
315+
}
316+
} else if (trustedContext) {
317+
if (concat.length > 1) {
318+
// there's at least two parts, so expr + string or exp + exp, and this context
319+
// doesn't allow that.
320+
$interpolateMinErr.throwNoconcat(text);
321+
} else {
322+
return concat.join('');
323+
}
324+
} else { // In an unprivileged context, just concatenate and return.
325+
return concat.join('');
326+
}
297327
};
298328

299329
var getValue = function(value) {
300-
return trustedContext ?
330+
// In concatenable contexts, getTrusted comes at the end, to avoid sanitizing individual
331+
// parts of a full URL. We don't care about losing the trustedness here, that's handled in
332+
// parseStringifyInterceptor below.
333+
return (trustedContext && !contextAllowsConcatenation) ?
301334
$sce.getTrusted(trustedContext, value) :
302335
$sce.valueOf(value);
303336
};
@@ -314,7 +347,7 @@ function $InterpolateProvider() {
314347

315348
return compute(values);
316349
} catch (err) {
317-
$exceptionHandler($interpolateMinErr.interr(text, err));
350+
$exceptionHandler(err);
318351
}
319352

320353
}, {
@@ -336,8 +369,13 @@ function $InterpolateProvider() {
336369

337370
function parseStringifyInterceptor(value) {
338371
try {
339-
value = getValue(value);
340-
return allOrNothing && !isDefined(value) ? value : stringify(value);
372+
if (contextAllowsConcatenation && singleExpression) {
373+
// No stringification in this case, to keep the trusted value until unwrapping.
374+
return value;
375+
} else {
376+
value = getValue(value);
377+
return allOrNothing && !isDefined(value) ? value : stringify(value);
378+
}
341379
} catch (err) {
342380
$exceptionHandler($interpolateMinErr.interr(text, err));
343381
}

src/ng/sanitizeUri.js

Lines changed: 14 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,41 @@
22

33
/**
44
* @description
5-
* Private service to sanitize uris for links and images. Used by $compile and $sanitize.
5+
* Private service to sanitize uris for $sce.URL context. Used by $compile, $sce and $sanitize.
66
*/
77
function $$SanitizeUriProvider() {
8-
var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/,
9-
imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/;
10-
11-
/**
12-
* @description
13-
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
14-
* urls during a[href] sanitization.
15-
*
16-
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
17-
*
18-
* Any url about to be assigned to a[href] via data-binding is first normalized and turned into
19-
* an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
20-
* regular expression. If a match is found, the original url is written into the dom. Otherwise,
21-
* the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
22-
*
23-
* @param {RegExp=} regexp New regexp to whitelist urls with.
24-
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
25-
* chaining otherwise.
26-
*/
27-
this.aHrefSanitizationWhitelist = function(regexp) {
28-
if (isDefined(regexp)) {
29-
aHrefSanitizationWhitelist = regexp;
30-
return this;
31-
}
32-
return aHrefSanitizationWhitelist;
33-
};
8+
var uriSanitizationWhitelist = /^\s*((https?|ftp|file|blob|tel|mailto):|data:image\/)/i;
349

3510

3611
/**
3712
* @description
3813
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
39-
* urls during img[src] sanitization.
14+
* urls.
4015
*
41-
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
16+
* The sanitization is a security measure aimed at prevent XSS attacks.
4217
*
43-
* Any url about to be assigned to img[src] via data-binding is first normalized and turned into
44-
* an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist`
18+
* Any url about to be assigned to URL context via data-binding is first normalized and turned into
19+
* an absolute url. Afterwards, the url is matched against the `urlSanitizationWhitelist`
4520
* regular expression. If a match is found, the original url is written into the dom. Otherwise,
46-
* the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
21+
* the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM,
22+
* making it inactive.
4723
*
4824
* @param {RegExp=} regexp New regexp to whitelist urls with.
4925
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
5026
* chaining otherwise.
5127
*/
52-
this.imgSrcSanitizationWhitelist = function(regexp) {
28+
this.uriSanitizationWhitelist = function(regexp) {
5329
if (isDefined(regexp)) {
54-
imgSrcSanitizationWhitelist = regexp;
30+
uriSanitizationWhitelist = regexp;
5531
return this;
5632
}
57-
return imgSrcSanitizationWhitelist;
33+
return uriSanitizationWhitelist;
5834
};
5935

6036
this.$get = function() {
61-
return function sanitizeUri(uri, isImage) {
62-
var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
63-
var normalizedVal;
64-
normalizedVal = urlResolve(uri).href;
65-
if (normalizedVal !== '' && !normalizedVal.match(regex)) {
37+
return function sanitizeUri(uri) {
38+
var normalizedVal = urlResolve(uri).href;
39+
if (normalizedVal !== '' && !normalizedVal.match(uriSanitizationWhitelist)) {
6640
return 'unsafe:' + normalizedVal;
6741
}
6842
return uri;

0 commit comments

Comments
 (0)