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

Commit

Permalink
fix(interpolate): sanitize non-interpolated expressions for URL contexts
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
petebacondarwin committed Jan 30, 2018
1 parent 7202231 commit 044a801
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 229 deletions.
23 changes: 14 additions & 9 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 @@ -261,8 +266,8 @@ function $InterpolateProvider() {
exp,
concat = [],
expressionPositions = [],
singleExpression,
contextAllowsConcatenation = trustedContext === $sce.URL || trustedContext === $sce.MEDIA_URL;
singleExpression;


while (index < textLength) {
if (((startIndex = text.indexOf(startSymbol, index)) !== -1) &&
Expand Down
9 changes: 9 additions & 0 deletions test/ng/compileSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3414,6 +3414,15 @@ describe('$compile', function() {
})
);

it('should interpolate a multi-part expression for regular attributes', inject(function($compile, $rootScope) {
element = $compile('<div foo="some/{{id}}"></div>')($rootScope);
$rootScope.$digest();
expect(element.attr('foo')).toBe('some/');
$rootScope.$apply(function() {
$rootScope.id = 1;
});
expect(element.attr('foo')).toEqual('some/1');
}));

it('should process attribute interpolation in pre-linking phase at priority 100', function() {
module(function() {
Expand Down
218 changes: 0 additions & 218 deletions test/ng/directive/booleanAttrsSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,221 +118,3 @@ describe('boolean attr directives', function() {
}));
});
});


describe('ngSrc', function() {
it('should interpolate the expression and bind to src with raw same-domain value',
inject(function($compile, $rootScope) {
var element = $compile('<img ng-src="{{id}}"></img>')($rootScope);

$rootScope.$digest();
expect(element.attr('src')).toBeUndefined();

$rootScope.$apply(function() {
$rootScope.id = '/somewhere/here';
});
expect(element.attr('src')).toEqual('/somewhere/here');

dealoc(element);
}));


it('should interpolate the expression and bind to src with a trusted value', inject(function($compile, $rootScope, $sce) {
var element = $compile('<iframe ng-src="{{id}}"></iframe>')($rootScope);

$rootScope.$digest();
expect(element.attr('src')).toBeUndefined();

$rootScope.$apply(function() {
$rootScope.id = $sce.trustAsResourceUrl('http://somewhere');
});
expect(element.attr('src')).toEqual('http://somewhere');

dealoc(element);
}));


it('should NOT interpolate a multi-part expression in a `src` attribute that requires a non-MEDIA_URL context', inject(function($compile, $rootScope) {
expect(function() {
var element = $compile('<iframe ng-src="some/{{id}}"></iframe>')($rootScope);
$rootScope.$apply(function() {
$rootScope.id = 1;
});
dealoc(element);
}).toThrowMinErr(
'$interpolate', 'noconcat', 'Error while interpolating: some/{{id}}\nStrict ' +
'Contextual Escaping disallows interpolations that concatenate multiple expressions ' +
'when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce');
}));

it('should interpolate a multi-part expression for img src attribute (which requires the MEDIA_URL context)', inject(function($compile, $rootScope) {
var element = $compile('<img ng-src="some/{{id}}"></img>')($rootScope);
expect(element.attr('src')).toBe(undefined); // URL concatenations are all-or-nothing
$rootScope.$apply(function() {
$rootScope.id = 1;
});
expect(element.attr('src')).toEqual('some/1');
}));


it('should interpolate a multi-part expression for regular attributes', inject(function($compile, $rootScope) {
var element = $compile('<div foo="some/{{id}}"></div>')($rootScope);
$rootScope.$digest();
expect(element.attr('foo')).toBe('some/');
$rootScope.$apply(function() {
$rootScope.id = 1;
});
expect(element.attr('foo')).toEqual('some/1');
}));


it('should NOT interpolate a wrongly typed expression', inject(function($compile, $rootScope, $sce) {
expect(function() {
var element = $compile('<iframe ng-src="{{id}}"></iframe>')($rootScope);
$rootScope.$apply(function() {
$rootScope.id = $sce.trustAsUrl('http://somewhere');
});
element.attr('src');
}).toThrowMinErr(
'$interpolate', 'interr', 'Can\'t interpolate: {{id}}\nError: [$sce:insecurl] Blocked ' +
'loading resource from url not allowed by $sceDelegate policy. URL: http://somewhere');
}));


// Support: IE 9-11 only
if (msie) {
it('should update the element property as well as the attribute', inject(
function($compile, $rootScope, $sce) {
// 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

var element = $compile('<img ng-src="{{id}}"></img>')($rootScope);

$rootScope.$digest();
expect(element.prop('src')).toBe('');
dealoc(element);

element = $compile('<img ng-src="some/"></img>')($rootScope);

$rootScope.$digest();
expect(element.prop('src')).toMatch('/some/$');
dealoc(element);

element = $compile('<img ng-src="{{id}}"></img>')($rootScope);
$rootScope.$apply(function() {
$rootScope.id = $sce.trustAsResourceUrl('http://somewhere/abc');
});
expect(element.prop('src')).toEqual('http://somewhere/abc');

dealoc(element);
}));
}
});


describe('ngSrcset', function() {
it('should interpolate the expression and bind to srcset', inject(function($compile, $rootScope) {
var element = $compile('<img ng-srcset="some/{{id}} 2x"></div>')($rootScope);

$rootScope.$digest();
expect(element.attr('srcset')).toBeUndefined();

$rootScope.$apply(function() {
$rootScope.id = 1;
});
expect(element.attr('srcset')).toEqual('some/1 2x');

dealoc(element);
}));
});


describe('ngHref', function() {
var element;

afterEach(function() {
dealoc(element);
});


it('should interpolate the expression and bind to href', inject(function($compile, $rootScope) {
element = $compile('<a ng-href="some/{{id}}"></div>')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toEqual('some/');

$rootScope.$apply(function() {
$rootScope.id = 1;
});
expect(element.attr('href')).toEqual('some/1');
}));


it('should bind href and merge with other attrs', inject(function($rootScope, $compile) {
element = $compile('<a ng-href="{{url}}" rel="{{rel}}"></a>')($rootScope);
$rootScope.url = 'http://server';
$rootScope.rel = 'REL';
$rootScope.$digest();
expect(element.attr('href')).toEqual('http://server');
expect(element.attr('rel')).toEqual('REL');
}));


it('should bind href even if no interpolation', inject(function($rootScope, $compile) {
element = $compile('<a ng-href="http://server"></a>')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toEqual('http://server');
}));

it('should not set the href if ng-href is empty', inject(function($rootScope, $compile) {
$rootScope.url = null;
element = $compile('<a ng-href="{{url}}">')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toEqual(undefined);
}));

it('should remove the href if ng-href changes to empty', inject(function($rootScope, $compile) {
$rootScope.url = 'http://www.google.com/';
element = $compile('<a ng-href="{{url}}">')($rootScope);
$rootScope.$digest();

$rootScope.url = null;
$rootScope.$digest();
expect(element.attr('href')).toEqual(undefined);
}));

// Support: IE 9-11 only, Edge 12-15+
if (msie || /\bEdge\/[\d.]+\b/.test(window.navigator.userAgent)) {
// IE/Edge fail when setting a href to a URL containing a % that isn't a valid escape sequence
// See https://github.com/angular/angular.js/issues/13388
it('should throw error if ng-href contains a non-escaped percent symbol', inject(function($rootScope, $compile) {
expect(function() {
element = $compile('<a ng-href="http://www.google.com/{{\'a%link\'}}">')($rootScope);
}).toThrow();
}));
}

if (isDefined(window.SVGElement)) {
describe('SVGAElement', function() {
it('should interpolate the expression and bind to xlink:href', inject(function($compile, $rootScope) {
element = $compile('<svg><a ng-href="some/{{id}}"></a></svg>')($rootScope);
var child = element.children('a');
$rootScope.$digest();
expect(child.attr('xlink:href')).toEqual('some/');

$rootScope.$apply(function() {
$rootScope.id = 1;
});
expect(child.attr('xlink:href')).toEqual('some/1');
}));


it('should bind xlink:href even if no interpolation', inject(function($rootScope, $compile) {
element = $compile('<svg><a ng-href="http://server"></a></svg>')($rootScope);
var child = element.children('a');
$rootScope.$digest();
expect(child.attr('xlink:href')).toEqual('http://server');
}));
});
}
});
104 changes: 104 additions & 0 deletions test/ng/directive/ngHrefSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use strict';

describe('ngHref', function() {
var element;

afterEach(function() {
dealoc(element);
});


it('should interpolate the expression and bind to href', inject(function($compile, $rootScope) {
element = $compile('<a ng-href="some/{{id}}"></div>')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toEqual('some/');

$rootScope.$apply(function() {
$rootScope.id = 1;
});
expect(element.attr('href')).toEqual('some/1');
}));


it('should bind href and merge with other attrs', inject(function($rootScope, $compile) {
element = $compile('<a ng-href="{{url}}" rel="{{rel}}"></a>')($rootScope);
$rootScope.url = 'http://server';
$rootScope.rel = 'REL';
$rootScope.$digest();
expect(element.attr('href')).toEqual('http://server');
expect(element.attr('rel')).toEqual('REL');
}));


it('should bind href even if no interpolation', inject(function($rootScope, $compile) {
element = $compile('<a ng-href="http://server"></a>')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toEqual('http://server');
}));

it('should not set the href if ng-href is empty', inject(function($rootScope, $compile) {
$rootScope.url = null;
element = $compile('<a ng-href="{{url}}">')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toEqual(undefined);
}));

it('should remove the href if ng-href changes to empty', inject(function($rootScope, $compile) {
$rootScope.url = 'http://www.google.com/';
element = $compile('<a ng-href="{{url}}">')($rootScope);
$rootScope.$digest();

$rootScope.url = null;
$rootScope.$digest();
expect(element.attr('href')).toEqual(undefined);
}));

it('should sanitize interpolated url', inject(function($rootScope, $compile) {
$rootScope.imageUrl = 'javascript:alert(1);';
element = $compile('<a ng-href="{{imageUrl}}">')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toBe('unsafe:javascript:alert(1);');
}));

it('should sanitize non-interpolated url', inject(function($rootScope, $compile) {
element = $compile('<a ng-href="javascript:alert(1);">')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toBe('unsafe:javascript:alert(1);');
}));


// Support: IE 9-11 only, Edge 12-15+
if (msie || /\bEdge\/[\d.]+\b/.test(window.navigator.userAgent)) {
// IE/Edge fail when setting a href to a URL containing a % that isn't a valid escape sequence
// See https://github.com/angular/angular.js/issues/13388
it('should throw error if ng-href contains a non-escaped percent symbol', inject(function($rootScope, $compile) {
expect(function() {
element = $compile('<a ng-href="http://www.google.com/{{\'a%link\'}}">')($rootScope);
}).toThrow();
}));
}

if (isDefined(window.SVGElement)) {
describe('SVGAElement', function() {
it('should interpolate the expression and bind to xlink:href', inject(function($compile, $rootScope) {
element = $compile('<svg><a ng-href="some/{{id}}"></a></svg>')($rootScope);
var child = element.children('a');
$rootScope.$digest();
expect(child.attr('xlink:href')).toEqual('some/');

$rootScope.$apply(function() {
$rootScope.id = 1;
});
expect(child.attr('xlink:href')).toEqual('some/1');
}));


it('should bind xlink:href even if no interpolation', inject(function($rootScope, $compile) {
element = $compile('<svg><a ng-href="http://server"></a></svg>')($rootScope);
var child = element.children('a');
$rootScope.$digest();
expect(child.attr('xlink:href')).toEqual('http://server');
}));
});
}
});
Loading

0 comments on commit 044a801

Please sign in to comment.