Thanks to visit codestin.com
Credit goes to github.com

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

feat($sce): handle URLs through the $sce service #16378

Merged
merged 1 commit into from
Jan 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/content/error/$compile/srcset.ngdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@ngdoc error
@name $compile:srcset
@fullName Invalid value passed to `attr.$set('srcset', value)`
@description

This error occurs if you try to programmatically set the `srcset` attribute with a non-string value.

This can be the case if you tried to avoid the automatic sanitization of the `srcset` value by
passing a "trusted" value provided by calls to `$sce.trustAsMediaUrl(value)`.

If you want to programmatically set explicitly trusted unsafe URLs, you should use `$sce.trustAsHtml`
on the whole `img` tag and inject it into the DOM using the `ng-bind-html` directive.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😱

48 changes: 31 additions & 17 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -1528,9 +1528,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {

this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
'$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri',
'$controller', '$rootScope', '$sce', '$animate',
function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
$controller, $rootScope, $sce, $animate, $$sanitizeUri) {
$controller, $rootScope, $sce, $animate) {

var SIMPLE_ATTR_NAME = /^\w/;
var specialAttrHolder = window.document.createElement('div');
Expand Down Expand Up @@ -1679,8 +1679,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
*/
$set: function(key, value, writeAttr, attrName) {
// TODO: decide whether or not to throw an error if "class"
//is set through this function since it may cause $updateClass to
//become unstable.
// is set through this function since it may cause $updateClass to
// become unstable.

var node = this.$$element[0],
booleanKey = getBooleanAttrName(node, key),
Expand Down Expand Up @@ -1710,13 +1710,20 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {

nodeName = nodeName_(this.$$element);

if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) ||
(nodeName === 'img' && key === 'src') ||
(nodeName === 'image' && key === 'xlinkHref')) {
// sanitize a[href] and img[src] values
this[key] = value = $$sanitizeUri(value, nodeName === 'img' || nodeName === 'image');
Copy link
Member

@gkalpak gkalpak Jan 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, doesn't this leave people using ng-href/src lessmore vulnerable (unless they use interpolation)?
(Not common to use ng-href/src without interpollation, but possible.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you mean here. Do you mean "more" vulnerable?
It is true that if you were programmatically setting the attribute via $set then it is no longer checked, but this is quite a common paradigm, where one has to be responsible for coding against the lower level API.

Copy link
Member

@gkalpak gkalpak Jan 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I meant "more" (everybody knows "less" is "more" 😛)
The thing is that some of the built-in directives (e.g. ngHref/ngSrc) use $set under the hood.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am pretty certain that ngHref and ngSrc are not affected. They only call $set with a value that is passed from $observe, which has already interpolated the value, and so has passed the value through SCE.

} else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) {
// sanitize img[srcset] values
// Sanitize img[srcset] values.
if (nodeName === 'img' && key === 'srcset' && value) {
if (!isString(value)) {
throw $compileMinErr('srcset', 'Can\'t pass trusted values to `$set(\'srcset\', value)`: "{0}"', value.toString());
}

// Such values are a bit too complex to handle automatically inside $sce.
// Instead, we sanitize each of the URIs individually, which works, even dynamically.

// It's not possible to work around this using `$sce.trustAsMediaUrl`.
// If you want to programmatically set explicitly trusted unsafe URLs, you should use
// `$sce.trustAsHtml` on the whole `img` tag and inject it into the DOM using the
// `ng-bind-html` directive.

var result = '';

// first check if there are spaces because it's not the same pattern
Expand All @@ -1733,16 +1740,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
for (var i = 0; i < nbrUrisWith2parts; i++) {
var innerIdx = i * 2;
// sanitize the uri
result += $$sanitizeUri(trim(rawUris[innerIdx]), true);
result += $sce.getTrustedMediaUrl(trim(rawUris[innerIdx]));
// add the descriptor
result += (' ' + trim(rawUris[innerIdx + 1]));
result += ' ' + trim(rawUris[innerIdx + 1]);
}

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

// sanitize the last uri
result += $$sanitizeUri(trim(lastTuple[0]), true);
result += $sce.getTrustedMediaUrl(trim(lastTuple[0]));

// and add the last descriptor if any
if (lastTuple.length === 2) {
Expand Down Expand Up @@ -3268,14 +3275,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
var tag = nodeName_(node);
// All tags with src attributes require a RESOURCE_URL value, except for
// img and various html5 media tags.
// img and various html5 media tags, which require the MEDIA_URL context.
if (attrNormalizedName === 'src' || attrNormalizedName === 'ngSrc') {
if (['img', 'video', 'audio', 'source', 'track'].indexOf(tag) === -1) {
return $sce.RESOURCE_URL;
}
return $sce.MEDIA_URL;
} else if (attrNormalizedName === 'xlinkHref') {
// Some xlink:href are okay, most aren't
if (tag === 'image') return $sce.MEDIA_URL;
if (tag === 'a') return $sce.URL;
return $sce.RESOURCE_URL;
} else if (
// Some xlink:href are okay, most aren't
(attrNormalizedName === 'xlinkHref' && (tag !== 'image' && tag !== 'a')) ||
// Formaction
(tag === 'form' && attrNormalizedName === 'action') ||
// If relative URLs can go where they are not expected to, then
Expand All @@ -3285,6 +3296,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
(tag === 'link' && attrNormalizedName === 'href')
) {
return $sce.RESOURCE_URL;
} else if (tag === 'a' && (attrNormalizedName === 'href' ||
attrNormalizedName === 'ngHref')) {
return $sce.URL;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/ng/directive/attrs.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
// On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
// then calling element.setAttribute('src', 'foo') doesn't do anything, so we need
// to set the property as well to achieve the desired effect.
// We use attr[attrName] value since $set can sanitize the url.
// We use attr[attrName] value since $set might have sanitized the url.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the commit message, I thought $set() doesn't sanitize it any more 😕

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still sanitize img[srcset] when called through $set.

if (msie && propName) element.prop(propName, attr[name]);
});
}
Expand Down
75 changes: 50 additions & 25 deletions src/ng/interpolate.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,16 +238,21 @@ function $InterpolateProvider() {
* - `context`: evaluation context for all expressions embedded in the interpolated text
*/
function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) {
var contextAllowsConcatenation = trustedContext === $sce.URL || trustedContext === $sce.MEDIA_URL;

// Provide a quick exit and simplified result function for text with no interpolation
if (!text.length || text.indexOf(startSymbol) === -1) {
var constantInterp;
if (!mustHaveExpression) {
var unescapedText = unescapeText(text);
constantInterp = valueFn(unescapedText);
constantInterp.exp = text;
constantInterp.expressions = [];
constantInterp.$$watchDelegate = constantWatchDelegate;
if (mustHaveExpression && !contextAllowsConcatenation) return;

var unescapedText = unescapeText(text);
if (contextAllowsConcatenation) {
unescapedText = $sce.getTrusted(trustedContext, unescapedText);
}
var constantInterp = valueFn(unescapedText);
constantInterp.exp = text;
constantInterp.expressions = [];
constantInterp.$$watchDelegate = constantWatchDelegate;

return constantInterp;
}

Expand All @@ -256,11 +261,13 @@ function $InterpolateProvider() {
endIndex,
index = 0,
expressions = [],
parseFns = [],
parseFns,
textLength = text.length,
exp,
concat = [],
expressionPositions = [];
expressionPositions = [],
singleExpression;


while (index < textLength) {
if (((startIndex = text.indexOf(startSymbol, index)) !== -1) &&
Expand All @@ -270,10 +277,9 @@ function $InterpolateProvider() {
}
exp = text.substring(startIndex + startSymbolLength, endIndex);
expressions.push(exp);
parseFns.push($parse(exp, parseStringifyInterceptor));
index = endIndex + endSymbolLength;
expressionPositions.push(concat.length);
concat.push('');
concat.push(''); // Placeholder that will get replaced with the evaluated expression.
} else {
// we did not find an interpolation, so we have to add the remainder to the separators array
if (index !== textLength) {
Expand All @@ -283,29 +289,42 @@ function $InterpolateProvider() {
}
}

singleExpression = concat.length === 1 && expressionPositions.length === 1;
// Intercept expression if we need to stringify concatenated inputs, which may be SCE trusted
// objects rather than simple strings
// (we don't modify the expression if the input consists of only a single trusted input)
var interceptor = contextAllowsConcatenation && singleExpression ? undefined : parseStringifyInterceptor;
parseFns = expressions.map(function(exp) { return $parse(exp, interceptor); });

// 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 iframe[src], object[src], etc., 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.
if (trustedContext && concat.length > 1) {
$interpolateMinErr.throwNoconcat(text);
}
// single expression be used for some $sce-managed secure contexts (RESOURCE_URLs mostly),
// 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.

// Note that URL and MEDIA_URL $sce contexts do not need this, since `$sce` can sanitize the values
// passed to it. In that case, `$sce.getTrusted` will be called on either the single expression
// or on the overall concatenated string (losing trusted types used in the mix, by design).
// Both these methods will sanitize plain strings. Also, HTML could be included, but since it's
// only used in srcdoc attributes, this would not be very useful.

if (!mustHaveExpression || expressions.length) {
var compute = function(values) {
for (var i = 0, ii = expressions.length; i < ii; i++) {
if (allOrNothing && isUndefined(values[i])) return;
concat[expressionPositions[i]] = values[i];
}
return concat.join('');
};

var getValue = function(value) {
return trustedContext ?
$sce.getTrusted(trustedContext, value) :
$sce.valueOf(value);
if (contextAllowsConcatenation) {
// If `singleExpression` then `concat[0]` might be a "trusted" value or `null`, rather than a string
return $sce.getTrusted(trustedContext, singleExpression ? concat[0] : concat.join(''));
} else if (trustedContext && concat.length > 1) {
// This context does not allow more than one part, e.g. expr + string or exp + exp.
$interpolateMinErr.throwNoconcat(text);
}
// In an unprivileged context or only one part: just concatenate and return.
return concat.join('');
};

return extend(function interpolationFn(context) {
Expand Down Expand Up @@ -340,7 +359,13 @@ function $InterpolateProvider() {

function parseStringifyInterceptor(value) {
try {
value = getValue(value);
// In concatenable contexts, getTrusted comes at the end, to avoid sanitizing individual
// parts of a full URL. We don't care about losing the trustedness here.
// In non-concatenable contexts, where there is only one expression, this interceptor is
// not applied to the expression.
value = (trustedContext && !contextAllowsConcatenation) ?
$sce.getTrusted(trustedContext, value) :
$sce.valueOf(value);
return allOrNothing && !isDefined(value) ? value : stringify(value);
} catch (err) {
$exceptionHandler($interpolateMinErr.interr(text, err));
Expand Down
38 changes: 24 additions & 14 deletions src/ng/sanitizeUri.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Private service to sanitize uris for links and images. Used by $compile and $sanitize.
*/
function $$SanitizeUriProvider() {

var aHrefSanitizationWhitelist = /^\s*(https?|s?ftp|mailto|tel|file):/,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a tiny quirk hiding there, which is probably without consequences: mailto and tel are in the URL whitelist, but not in the MEDIA_URL. This makes sense, but "safe under URL" is supposed to be a subset of "safe under MEDIA_URL" in my interpretation of the subclassing in the $sce. I don't think anyone cares either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I wondered about that. One is not really a "super-set" of the other but I needed both to be covered by RESOURCE_URL, so I had to force a hierarchy on them. Given that users can manipulate these whitelists in anyway they like, I don't feel too bad about the defaults not quite aligning.

imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/;

Expand All @@ -14,12 +15,16 @@ function $$SanitizeUriProvider() {
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
* urls during a[href] sanitization.
*
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
* The sanitization is a security measure aimed at prevent XSS attacks via HTML anchor links.
*
* Any url due to be assigned to an `a[href]` attribute via interpolation is marked as requiring
* the $sce.URL security context. When interpolation occurs a call is made to `$sce.trustAsUrl(url)`
* which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize the potentially malicious URL.
*
* If the URL matches the `aHrefSanitizationWhitelist` regular expression, it is returned unchanged.
*
* Any url about to be assigned to a[href] via data-binding is first normalized and turned into
* an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
* regular expression. If a match is found, the original url is written into the dom. Otherwise,
* the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
* If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written
* to the DOM it is inactive and potentially malicious code will not be executed.
*
* @param {RegExp=} regexp New regexp to whitelist urls with.
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
Expand All @@ -39,12 +44,17 @@ function $$SanitizeUriProvider() {
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
* urls during img[src] sanitization.
*
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
* The sanitization is a security measure aimed at prevent XSS attacks via HTML image src links.
*
* Any URL due to be assigned to an `img[src]` attribute via interpolation is marked as requiring
* the $sce.MEDIA_URL security context. When interpolation occurs a call is made to
* `$sce.trustAsMediaUrl(url)` which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize
* the potentially malicious URL.
*
* If the URL matches the `aImgSanitizationWhitelist` regular expression, it is returned unchanged.
*
* Any url about to be assigned to img[src] via data-binding is first normalized and turned into
* an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist`
* regular expression. If a match is found, the original url is written into the dom. Otherwise,
* the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
* If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written
* to the DOM it is inactive and potentially malicious code will not be executed.
*
* @param {RegExp=} regexp New regexp to whitelist urls with.
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
Expand All @@ -59,10 +69,10 @@ function $$SanitizeUriProvider() {
};

this.$get = function() {
return function sanitizeUri(uri, isImage) {
var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
var normalizedVal;
normalizedVal = urlResolve(uri && uri.trim()).href;
return function sanitizeUri(uri, isMediaUrl) {
// if (!uri) return uri;
var regex = isMediaUrl ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
var normalizedVal = urlResolve(uri && uri.trim()).href;
if (normalizedVal !== '' && !normalizedVal.match(regex)) {
return 'unsafe:' + normalizedVal;
}
Expand Down
Loading