Skip to content

Commit dbfd744

Browse files
petebacondarwinellimist
authored andcommitted
feat($http): JSONP requests now require a trusted resource URL
The $http service will 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 00e6b38 commit dbfd744

File tree

3 files changed

+101
-25
lines changed

3 files changed

+101
-25
lines changed

docs/content/error/$http/badreq.ngdoc

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
@fullName Bad Request Configuration
44
@description
55

6-
This error occurs when the request configuration parameter passed to the {@link ng.$http `$http`} service is not an object.  `$http` expects a single parameter, the request configuration object, but received a parameter that was not an object.  The error message should provide additional context such as the actual value of the parameter that was received.  If you passed a string parameter, perhaps you meant to call one of the shorthand methods on `$http` such as `$http.get(…)`, etc.
6+
This error occurs when the request configuration parameter passed to the {@link ng.$http `$http`} service is not a valid object.
7+
`$http` expects a single parameter, the request configuration object, but received a parameter that was not an object or did not contain valid properties.
8+
9+
The error message should provide additional context such as the actual value of the parameter that was received.
10+
If you passed a string parameter, perhaps you meant to call one of the shorthand methods on `$http` such as `$http.get(…)`, etc.
711

812
To resolve this error, make sure you pass a valid request configuration object to `$http`.
913

src/ng/http.js

+41-13
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

@@ -802,7 +802,8 @@ function $HttpProvider() {
802802
* processed. The object has following properties:
803803
*
804804
* - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc)
805-
* - **url** – `{string}` – Absolute or relative URL of the resource that is being requested.
805+
* - **url** – `{string|TrustedObject}` – Absolute or relative URL of the resource that is being requested;
806+
* or an object created by a call to `$sce.trustAsResourceUrl(url)`.
806807
* - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be serialized
807808
* with the `paramSerializer` and appended as GET parameters.
808809
* - **data** – `{string|Object}` – Data to be sent as the request message data.
@@ -881,6 +882,13 @@ function $HttpProvider() {
881882
</file>
882883
<file name="script.js">
883884
angular.module('httpExample', [])
885+
.config(['$sceDelegateProvider', function($sceDelegateProvider) {
886+
// We must whitelist the JSONP endpoint that we are using to show that we trust it
887+
$sceDelegateProvider.resourceUrlWhitelist([
888+
'self',
889+
'https://angularjs.org/**'
890+
]);
891+
}])
884892
.controller('FetchController', ['$scope', '$http', '$templateCache',
885893
function($scope, $http, $templateCache) {
886894
$scope.method = 'GET';
@@ -948,8 +956,8 @@ function $HttpProvider() {
948956
throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig);
949957
}
950958

951-
if (!isString(requestConfig.url)) {
952-
throw minErr('$http')('badreq', 'Http request configuration url must be a string. Received: {0}', requestConfig.url);
959+
if (!isString($sce.valueOf(requestConfig.url))) {
960+
throw minErr('$http')('badreq', 'Http request configuration url must be a string or a $sce trusted object. Received: {0}', requestConfig.url);
953961
}
954962

955963
var config = extend({
@@ -1111,7 +1119,8 @@ function $HttpProvider() {
11111119
* @description
11121120
* Shortcut method to perform `GET` request.
11131121
*
1114-
* @param {string} url Relative or absolute URL specifying the destination of the request
1122+
* @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested;
1123+
* or an object created by a call to `$sce.trustAsResourceUrl(url)`.
11151124
* @param {Object=} config Optional configuration object
11161125
* @returns {HttpPromise} Future object
11171126
*/
@@ -1123,7 +1132,8 @@ function $HttpProvider() {
11231132
* @description
11241133
* Shortcut method to perform `DELETE` request.
11251134
*
1126-
* @param {string} url Relative or absolute URL specifying the destination of the request
1135+
* @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested;
1136+
* or an object created by a call to `$sce.trustAsResourceUrl(url)`.
11271137
* @param {Object=} config Optional configuration object
11281138
* @returns {HttpPromise} Future object
11291139
*/
@@ -1135,7 +1145,8 @@ function $HttpProvider() {
11351145
* @description
11361146
* Shortcut method to perform `HEAD` request.
11371147
*
1138-
* @param {string} url Relative or absolute URL specifying the destination of the request
1148+
* @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested;
1149+
* or an object created by a call to `$sce.trustAsResourceUrl(url)`.
11391150
* @param {Object=} config Optional configuration object
11401151
* @returns {HttpPromise} Future object
11411152
*/
@@ -1146,11 +1157,18 @@ function $HttpProvider() {
11461157
*
11471158
* @description
11481159
* Shortcut method to perform `JSONP` request.
1149-
* If you would like to customize where and how the callbacks are stored then try overriding
1160+
*
1161+
* Note that, since JSONP requests are sensitive because the response is given full acces to the browser,
1162+
* the url must be declared, via {@link $sce} as a trusted resource URL.
1163+
* You can trust a URL by adding it to the whitelist via
1164+
* {@link $sceDelegateProvider#resourceUrlWhitelist `$sceDelegateProvider.resourceUrlWhitelist`} or
1165+
* by explicitly trusted the URL via {@link $sce#trustAsResourceUrl `$sce.trustAsResourceUrl(url)`}.
1166+
*
1167+
* If you would like to customise where and how the callbacks are stored then try overriding
11501168
* or decorating the {@link $jsonpCallbacks} service.
11511169
*
1152-
* @param {string} url Relative or absolute URL specifying the destination of the request.
1153-
* The name of the callback should be the string `JSON_CALLBACK`.
1170+
* @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested;
1171+
* or an object created by a call to `$sce.trustAsResourceUrl(url)`.
11541172
* @param {Object=} config Optional configuration object
11551173
* @returns {HttpPromise} Future object
11561174
*/
@@ -1249,12 +1267,22 @@ function $HttpProvider() {
12491267
cache,
12501268
cachedResp,
12511269
reqHeaders = config.headers,
1252-
url = buildUrl(config.url, config.paramSerializer(config.params));
1270+
url = config.url;
1271+
1272+
if (lowercase(config.method) === 'jsonp') {
1273+
// JSONP is a pretty sensitive operation where we're allowing a script to have full access to
1274+
// our DOM and JS space. So we require that the URL satisfies SCE.RESOURCE_URL.
1275+
url = $sce.getTrustedResourceUrl(url);
1276+
} else if (!isString(url)) {
1277+
// If it is not a string then the URL must be a $sce trusted object
1278+
url = $sce.valueOf(url);
1279+
}
1280+
1281+
url = buildUrl(url, config.paramSerializer(config.params));
12531282

12541283
$http.pendingRequests.push(config);
12551284
promise.then(removePendingReq, removePendingReq);
12561285

1257-
12581286
if ((config.cache || defaults.cache) && config.cache !== false &&
12591287
(config.method === 'GET' || config.method === 'JSONP')) {
12601288
cache = isObject(config.cache) ? config.cache

test/ng/httpSpec.js

+55-11
Original file line numberDiff line numberDiff line change
@@ -289,27 +289,48 @@ 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

301307
it('should throw error if the request configuration is not an object', function() {
302308
expect(function() {
303-
$http('/url');
309+
$http('/url');
304310
}).toThrowMinErr('$http','badreq', 'Http request configuration must be an object. Received: /url');
305311
});
306312

307-
it('should throw error if the request configuration url is not a string', function() {
313+
it('should throw error if the request configuration url is not a string nor a trusted object', function() {
314+
expect(function() {
315+
$http({url: false});
316+
}).toThrowMinErr('$http','badreq', 'Http request configuration url must be a string or a $sce trusted object. Received: false');
317+
expect(function() {
318+
$http({url: null});
319+
}).toThrowMinErr('$http','badreq', 'Http request configuration url must be a string or a $sce trusted object. Received: null');
320+
expect(function() {
321+
$http({url: 42});
322+
}).toThrowMinErr('$http','badreq', 'Http request configuration url must be a string or a $sce trusted object. Received: 42');
308323
expect(function() {
309-
$http({url: false});
310-
}).toThrowMinErr('$http','badreq', 'Http request configuration url must be a string. Received: false');
324+
$http({});
325+
}).toThrowMinErr('$http','badreq', 'Http request configuration url must be a string or a $sce trusted object. Received: undefined');
311326
});
312327

328+
it('should accept a $sce trusted object for the request configuration url', function() {
329+
expect(function() {
330+
$httpBackend.expect('GET', '/url').respond('');
331+
$http({url: $sce.trustAsResourceUrl('/url')});
332+
}).not.toThrowMinErr('$http','badreq', 'Http request configuration url must be a string. Received: false');
333+
});
313334

314335
it('should send GET requests if no method specified', function() {
315336
$httpBackend.expect('GET', '/url').respond('');
@@ -602,7 +623,7 @@ describe('$http', function() {
602623
});
603624

604625
$httpBackend.expect('JSONP', '/some').respond(200);
605-
$http({url: '/some', method: 'JSONP'}).then(callback);
626+
$http({url: $sce.trustAsResourceUrl('/some'), method: 'JSONP'}).then(callback);
606627
$httpBackend.flush();
607628
expect(callback).toHaveBeenCalledOnce();
608629
});
@@ -1010,16 +1031,39 @@ describe('$http', function() {
10101031

10111032
it('should have jsonp()', function() {
10121033
$httpBackend.expect('JSONP', '/url').respond('');
1013-
$http.jsonp('/url');
1034+
$http.jsonp($sce.trustAsResourceUrl('/url'));
10141035
});
10151036

10161037

10171038
it('jsonp() should allow config param', function() {
10181039
$httpBackend.expect('JSONP', '/url', undefined, checkHeader('Custom', 'Header')).respond('');
1019-
$http.jsonp('/url', {headers: {'Custom': 'Header'}});
1040+
$http.jsonp($sce.trustAsResourceUrl('/url'), {headers: {'Custom': 'Header'}});
10201041
});
10211042
});
10221043

1044+
describe('jsonp trust', function() {
1045+
it('should throw error if the url is not a trusted resource', function() {
1046+
var success, error;
1047+
$http({method: 'JSONP', url: 'http://example.org/path?cb=JSON_CALLBACK'}).catch(
1048+
function(e) { error = e; }
1049+
);
1050+
$rootScope.$digest();
1051+
expect(error.message).toContain('[$sce:insecurl]');
1052+
});
1053+
1054+
it('should accept an explicitly trusted resource url', function() {
1055+
$httpBackend.expect('JSONP', 'http://example.org/path?cb=JSON_CALLBACK').respond('');
1056+
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path?cb=JSON_CALLBACK')});
1057+
});
1058+
1059+
it('jsonp() should accept explictly trusted urls', function() {
1060+
$httpBackend.expect('JSONP', '/url').respond('');
1061+
$http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url')});
1062+
1063+
$httpBackend.expect('JSONP', '/url?a=b').respond('');
1064+
$http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url'), params: {a: 'b'}});
1065+
});
1066+
});
10231067

10241068
describe('callbacks', function() {
10251069

@@ -1481,10 +1525,10 @@ describe('$http', function() {
14811525

14821526
it('should cache JSONP request when cache is provided', inject(function($rootScope) {
14831527
$httpBackend.expect('JSONP', '/url?cb=JSON_CALLBACK').respond('content');
1484-
$http({method: 'JSONP', url: '/url?cb=JSON_CALLBACK', cache: cache});
1528+
$http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url?cb=JSON_CALLBACK'), cache: cache});
14851529
$httpBackend.flush();
14861530

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

14901534
expect(callback).toHaveBeenCalledOnce();

0 commit comments

Comments
 (0)