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

Commit c0beaad

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 5e7c4ab commit c0beaad

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
@@ -1167,14 +1167,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
11671167

11681168
/**
11691169
* @ngdoc method
1170-
* @name $compileProvider#aHrefSanitizationWhitelist
1170+
* @name $compileProvider#uriSanitizationWhitelist
11711171
* @kind function
11721172
*
11731173
* @description
11741174
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
1175-
* urls during a[href] sanitization.
1175+
* urls during URL-context sanitization.
11761176
*
1177-
* The sanitization is a security measure aimed at preventing XSS attacks via html links.
1177+
* The sanitization is a security measure aimed at preventing XSS attacks.
11781178
*
11791179
* Any url about to be assigned to a[href] via data-binding is first normalized and turned into
11801180
* an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
@@ -1185,45 +1185,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
11851185
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
11861186
* chaining otherwise.
11871187
*/
1188-
this.aHrefSanitizationWhitelist = function(regexp) {
1188+
this.uriSanitizationWhitelist = function(regexp) {
11891189
if (isDefined(regexp)) {
1190-
$$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp);
1190+
$$sanitizeUriProvider.uriSanitizationWhitelist(regexp);
11911191
return this;
11921192
} else {
1193-
return $$sanitizeUriProvider.aHrefSanitizationWhitelist();
1193+
return $$sanitizeUriProvider.uriSanitizationWhitelist();
11941194
}
11951195
};
11961196

11971197

1198-
/**
1199-
* @ngdoc method
1200-
* @name $compileProvider#imgSrcSanitizationWhitelist
1201-
* @kind function
1202-
*
1203-
* @description
1204-
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
1205-
* urls during img[src] sanitization.
1206-
*
1207-
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
1208-
*
1209-
* Any url about to be assigned to img[src] via data-binding is first normalized and turned into
1210-
* an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist`
1211-
* regular expression. If a match is found, the original url is written into the dom. Otherwise,
1212-
* the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
1213-
*
1214-
* @param {RegExp=} regexp New regexp to whitelist urls with.
1215-
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
1216-
* chaining otherwise.
1217-
*/
1218-
this.imgSrcSanitizationWhitelist = function(regexp) {
1219-
if (isDefined(regexp)) {
1220-
$$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp);
1221-
return this;
1222-
} else {
1223-
return $$sanitizeUriProvider.imgSrcSanitizationWhitelist();
1224-
}
1225-
};
1226-
12271198
/**
12281199
* @ngdoc method
12291200
* @name $compileProvider#debugInfoEnabled
@@ -1287,9 +1258,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
12871258

12881259
this.$get = [
12891260
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
1290-
'$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri',
1261+
'$controller', '$rootScope', '$sce', '$animate',
12911262
function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
1292-
$controller, $rootScope, $sce, $animate, $$sanitizeUri) {
1263+
$controller, $rootScope, $sce, $animate) {
12931264

12941265
var SIMPLE_ATTR_NAME = /^\w/;
12951266
var specialAttrHolder = window.document.createElement('div');
@@ -1458,11 +1429,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
14581429

14591430
nodeName = nodeName_(this.$$element);
14601431

1461-
if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) ||
1462-
(nodeName === 'img' && key === 'src')) {
1463-
// sanitize a[href] and img[src] values
1464-
this[key] = value = $$sanitizeUri(value, key === 'src');
1465-
} else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) {
1432+
// img[srcset] is a bit too weird of a beast to handle through $sce.
1433+
// Instead, for now at least, sanitize each of the URIs individually.
1434+
// That works even dynamically, but it's not bypassable through the $sce.
1435+
// Instead, if you want several unsafe URLs as-is, you should probably
1436+
// use trustAsHtml on the whole tag.
1437+
if (nodeName === 'img' && key === 'srcset') {
1438+
14661439
// sanitize img[srcset] values
14671440
var result = "";
14681441

@@ -1480,16 +1453,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
14801453
for (var i = 0; i < nbrUrisWith2parts; i++) {
14811454
var innerIdx = i * 2;
14821455
// sanitize the uri
1483-
result += $$sanitizeUri(trim(rawUris[innerIdx]), true);
1456+
result += $sce.getTrustedUrl(trim(rawUris[innerIdx]));
14841457
// add the descriptor
1485-
result += (" " + trim(rawUris[innerIdx + 1]));
1458+
result += " " + trim(rawUris[innerIdx + 1]);
14861459
}
14871460

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

14911464
// sanitize the last uri
1492-
result += $$sanitizeUri(trim(lastTuple[0]), true);
1465+
result += $sce.getTrustedUrl(trim(lastTuple[0]));
14931466

14941467
// and add the last descriptor if any
14951468
if (lastTuple.length === 2) {
@@ -2956,6 +2929,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
29562929
(tag !== "img" && (attrNormalizedName === "src" ||
29572930
attrNormalizedName === "ngSrc"))) {
29582931
return $sce.RESOURCE_URL;
2932+
} else if (attrNormalizedName == "src" ||
2933+
attrNormalizedName == "href" ||
2934+
attrNormalizedName == "ngHref" ||
2935+
attrNormalizedName == "ngSrc") {
2936+
// All that is not a RESOURCE_URL but still a URL. This might be overkill for some
2937+
// attributes, but the sanitization is permissive, so it should not be troublesome.
2938+
return $sce.URL;
29592939
}
29602940
}
29612941

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
@@ -278,7 +282,9 @@ function $InterpolateProvider() {
278282
textLength = text.length,
279283
exp,
280284
concat = [],
281-
expressionPositions = [];
285+
expressionPositions = [],
286+
singleExpression = false,
287+
contextAllowsConcatenation = isConcatenationAllowed(trustedContext);
282288

283289
while (index < textLength) {
284290
if (((startIndex = text.indexOf(startSymbol, index)) !== -1) &&
@@ -291,7 +297,7 @@ function $InterpolateProvider() {
291297
parseFns.push($parse(exp, parseStringifyInterceptor));
292298
index = endIndex + endSymbolLength;
293299
expressionPositions.push(concat.length);
294-
concat.push('');
300+
concat.push(''); // Placeholder that will get replaced with the evaluated expression.
295301
} else {
296302
// we did not find an interpolation, so we have to add the remainder to the separators array
297303
if (index !== textLength) {
@@ -301,27 +307,54 @@ function $InterpolateProvider() {
301307
}
302308
}
303309

310+
if (concat.length == 1 && expressionPositions.length == 1) {
311+
singleExpression = true;
312+
}
313+
304314
// Concatenating expressions makes it hard to reason about whether some combination of
305315
// concatenated values are unsafe to use and could easily lead to XSS. By requiring that a
306-
// single expression be used for iframe[src], object[src], etc., we ensure that the value
307-
// that's used is assigned or constructed by some JS code somewhere that is more testable or
308-
// make it obvious that you bound the value to some user controlled value. This helps reduce
309-
// the load when auditing for XSS issues.
310-
if (trustedContext && concat.length > 1) {
311-
$interpolateMinErr.throwNoconcat(text);
312-
}
316+
// single expression be used for some $sce-managed secure contexts (RESOURCE_URLs mostly),
317+
// we ensure that the value that's used is assigned or constructed by some JS code somewhere
318+
// that is more testable or make it obvious that you bound the value to some user controlled
319+
// value. This helps reduce the load when auditing for XSS issues.
320+
// Note that URL-context is dispensed of this, since its getTrusted method can sanitize.
321+
// In that context, .getTrusted will be called on either the single expression or on the
322+
// overall concatenated string (losing trusted types used in the mix, by design). Both these
323+
// methods will sanitize plain strings. Also, HTML could be included, but since it's only used
324+
// in srcdoc attributes, this would not be very useful.
313325

314326
if (!mustHaveExpression || expressions.length) {
315327
var compute = function(values) {
316328
for (var i = 0, ii = expressions.length; i < ii; i++) {
317329
if (allOrNothing && isUndefined(values[i])) return;
318330
concat[expressionPositions[i]] = values[i];
319331
}
320-
return concat.join('');
332+
333+
if (contextAllowsConcatenation) {
334+
if (singleExpression) {
335+
// The raw value was left as-is by parseStringifyInterceptor
336+
return $sce.getTrusted(trustedContext, concat[0]);
337+
} else {
338+
return $sce.getTrusted(trustedContext, concat.join(''));
339+
}
340+
} else if (trustedContext) {
341+
if (concat.length > 1) {
342+
// there's at least two parts, so expr + string or exp + exp, and this context
343+
// doesn't allow that.
344+
$interpolateMinErr.throwNoconcat(text);
345+
} else {
346+
return concat.join('');
347+
}
348+
} else { // In an unprivileged context, just concatenate and return.
349+
return concat.join('');
350+
}
321351
};
322352

323353
var getValue = function(value) {
324-
return trustedContext ?
354+
// In concatenable contexts, getTrusted comes at the end, to avoid sanitizing individual
355+
// parts of a full URL. We don't care about losing the trustedness here, that's handled in
356+
// parseStringifyInterceptor below.
357+
return (trustedContext && !contextAllowsConcatenation) ?
325358
$sce.getTrusted(trustedContext, value) :
326359
$sce.valueOf(value);
327360
};
@@ -338,7 +371,7 @@ function $InterpolateProvider() {
338371

339372
return compute(values);
340373
} catch (err) {
341-
$exceptionHandler($interpolateMinErr.interr(text, err));
374+
$exceptionHandler(err);
342375
}
343376

344377
}, {
@@ -360,8 +393,13 @@ function $InterpolateProvider() {
360393

361394
function parseStringifyInterceptor(value) {
362395
try {
363-
value = getValue(value);
364-
return allOrNothing && !isDefined(value) ? value : stringify(value);
396+
if (contextAllowsConcatenation && singleExpression) {
397+
// No stringification in this case, to keep the trusted value until unwrapping.
398+
return value;
399+
} else {
400+
value = getValue(value);
401+
return allOrNothing && !isDefined(value) ? value : stringify(value);
402+
}
365403
} catch (err) {
366404
$exceptionHandler($interpolateMinErr.interr(text, err));
367405
}

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)