Skip to content

Commit a1bf258

Browse files
feat($httpBackend): JSONP requests now require trusted resource
Reject JSONP requests that are not trusted by `$sce` as "ResourceUrl". This change makes is easier for developers to see clearly where in their code they are making JSONP calls that may be to untrusted endpoings and forces them to think about how these URLs are generated. Be aware that this commit does not put any constraint on the parameters that will be appended to the URL. Developers should be mindful of what parameters can be attached and how they are generated. Closes angular#11352 BREAKING CHANGE All JSONP requests now require the URL to be trusted as resource URLs. There are two approaches to trust a URL: **Whitelisting with the `$sceDelegateProvider.resourceUrlWhitelist()` method.** You configure this list in a module configuration block: ``` appModule.config(['$sceDelegateProvider', function($sceDelegateProvider) { $sceDelegateProvider.resourceUrlWhiteList([ // Allow same origin resource loads. 'self', // Allow JSONP calls that match this pattern 'https://some.dataserver.com/**.jsonp?**` ]); }]); ``` **Explicitly trusting the URL via the `$sce.trustAsResourceUrl(url)` method** You can pass a trusted object instead of a string as a URL to the `$http` service: ``` var promise = $http.jsonp($sce.trustAsResourceUrl(url)); ```
1 parent 92d01a2 commit a1bf258

File tree

2 files changed

+62
-13
lines changed

2 files changed

+62
-13
lines changed

src/ng/http.js

+24-6
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,8 @@ function $HttpProvider() {
379379
**/
380380
var interceptorFactories = this.interceptors = [];
381381

382-
this.$get = ['$browser', '$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector',
383-
function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) {
382+
this.$get = ['$browser', '$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', '$sce',
383+
function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector, $sce) {
384384

385385
var defaultCache = $cacheFactory('$http');
386386

@@ -881,6 +881,12 @@ function $HttpProvider() {
881881
</file>
882882
<file name="script.js">
883883
angular.module('httpExample', [])
884+
.config(['$sceDelegateProvider', function($sceDelegateProvider) {
885+
$sceDelegateProvider.resourceUrlWhitelist([
886+
'self',
887+
'https://angularjs.org/**'
888+
]);
889+
}])
884890
.controller('FetchController', ['$scope', '$http', '$templateCache',
885891
function($scope, $http, $templateCache) {
886892
$scope.method = 'GET';
@@ -948,7 +954,7 @@ function $HttpProvider() {
948954
throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig);
949955
}
950956

951-
if (!isString(requestConfig.url)) {
957+
if (!isString($sce.valueOf(requestConfig.url))) {
952958
throw minErr('$http')('badreq', 'Http request configuration url must be a string. Received: {0}', requestConfig.url);
953959
}
954960

@@ -1088,7 +1094,12 @@ function $HttpProvider() {
10881094
}
10891095

10901096
// send request
1091-
return sendReq(config, reqData).then(transformResponse, transformResponse);
1097+
try {
1098+
var promise = sendReq(config, reqData);
1099+
} catch (e) {
1100+
return $q.reject(e);
1101+
}
1102+
return promise.then(transformResponse, transformResponse);
10921103
}
10931104

10941105
function transformResponse(response) {
@@ -1249,12 +1260,19 @@ function $HttpProvider() {
12491260
cache,
12501261
cachedResp,
12511262
reqHeaders = config.headers,
1252-
url = buildUrl(config.url, config.paramSerializer(config.params));
1263+
url = config.url;
1264+
1265+
if (lowercase(config.method) === 'jsonp') {
1266+
// This is a pretty sensitive operation where we're allowing a script to have full access to
1267+
// our DOM and JS space. So we require that the URL satisfies SCE.RESOURCE_URL.
1268+
url = $sce.getTrustedResourceUrl(url);
1269+
}
1270+
1271+
url = buildUrl(url, config.paramSerializer(config.params));
12531272

12541273
$http.pendingRequests.push(config);
12551274
promise.then(removePendingReq, removePendingReq);
12561275

1257-
12581276
if ((config.cache || defaults.cache) && config.cache !== false &&
12591277
(config.method === 'GET' || config.method === 'JSONP')) {
12601278
cache = isObject(config.cache) ? config.cache

test/ng/httpSpec.js

+38-7
Original file line numberDiff line numberDiff line change
@@ -289,12 +289,18 @@ describe('$http', function() {
289289

290290

291291
describe('the instance', function() {
292-
var $httpBackend, $http, $rootScope;
292+
var $httpBackend, $http, $rootScope, $sce;
293293

294-
beforeEach(inject(['$httpBackend', '$http', '$rootScope', function($hb, $h, $rs) {
294+
beforeEach(module(function($sceDelegateProvider) {
295+
// Setup a special whitelisted url that we can use in testing JSONP requests
296+
$sceDelegateProvider.resourceUrlWhitelist(['http://special.whitelisted.resource.com/**']);
297+
}));
298+
299+
beforeEach(inject(['$httpBackend', '$http', '$rootScope', '$sce', function($hb, $h, $rs, $sc) {
295300
$httpBackend = $hb;
296301
$http = $h;
297302
$rootScope = $rs;
303+
$sce = $sc;
298304
spyOn($rootScope, '$apply').and.callThrough();
299305
}]));
300306

@@ -602,7 +608,7 @@ describe('$http', function() {
602608
});
603609

604610
$httpBackend.expect('JSONP', '/some').respond(200);
605-
$http({url: '/some', method: 'JSONP'}).then(callback);
611+
$http({url: $sce.trustAsResourceUrl('/some'), method: 'JSONP'}).then(callback);
606612
$httpBackend.flush();
607613
expect(callback).toHaveBeenCalledOnce();
608614
});
@@ -1010,16 +1016,41 @@ describe('$http', function() {
10101016

10111017
it('should have jsonp()', function() {
10121018
$httpBackend.expect('JSONP', '/url').respond('');
1013-
$http.jsonp('/url');
1019+
$http.jsonp($sce.trustAsResourceUrl('/url'));
10141020
});
10151021

10161022

10171023
it('jsonp() should allow config param', function() {
10181024
$httpBackend.expect('JSONP', '/url', undefined, checkHeader('Custom', 'Header')).respond('');
1019-
$http.jsonp('/url', {headers: {'Custom': 'Header'}});
1025+
$http.jsonp($sce.trustAsResourceUrl('/url'), {headers: {'Custom': 'Header'}});
10201026
});
10211027
});
10221028

1029+
describe('jsonp trust', function() {
1030+
it('should throw error if the url is not a trusted resource', function() {
1031+
var success, error;
1032+
$http({method: 'JSONP', url: 'http://example.org/path?cb=JSON_CALLBACK', callback: callback}).catch(
1033+
function(e) { error = e; }
1034+
);
1035+
$rootScope.$digest();
1036+
expect(error.message).toContain("[$sce:insecurl]");
1037+
});
1038+
1039+
it('should not throw error if the url is an explicitly trusted resource', function() {
1040+
expect(function() {
1041+
$httpBackend.expect('JSONP', 'http://example.org/path?cb=JSON_CALLBACK').respond('');
1042+
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path?cb=JSON_CALLBACK'), callback: callback});
1043+
}).not.toThrow();
1044+
});
1045+
1046+
it('jsonp() should allow trusted url', inject(['$sce', function($sce) {
1047+
$httpBackend.expect('JSONP', '/url').respond('');
1048+
$http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url')});
1049+
1050+
$httpBackend.expect('JSONP', '/url?a=b').respond('');
1051+
$http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url'), params: {a: 'b'}});
1052+
}]));
1053+
});
10231054

10241055
describe('callbacks', function() {
10251056

@@ -1481,10 +1512,10 @@ describe('$http', function() {
14811512

14821513
it('should cache JSONP request when cache is provided', inject(function($rootScope) {
14831514
$httpBackend.expect('JSONP', '/url?cb=JSON_CALLBACK').respond('content');
1484-
$http({method: 'JSONP', url: '/url?cb=JSON_CALLBACK', cache: cache});
1515+
$http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url?cb=JSON_CALLBACK'), cache: cache});
14851516
$httpBackend.flush();
14861517

1487-
$http({method: 'JSONP', url: '/url?cb=JSON_CALLBACK', cache: cache}).success(callback);
1518+
$http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url?cb=JSON_CALLBACK'), cache: cache}).success(callback);
14881519
$rootScope.$digest();
14891520

14901521
expect(callback).toHaveBeenCalledOnce();

0 commit comments

Comments
 (0)