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

Commit

Permalink
feat($httpBackend): JSONP requests now require trusted resource
Browse files Browse the repository at this point in the history
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 #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));
```
  • Loading branch information
petebacondarwin committed Sep 20, 2016
1 parent 4d5a8ff commit fcd8fa9
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 13 deletions.
30 changes: 24 additions & 6 deletions src/ng/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,8 @@ function $HttpProvider() {
**/
var interceptorFactories = this.interceptors = [];

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

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

Expand Down Expand Up @@ -881,6 +881,12 @@ function $HttpProvider() {
</file>
<file name="script.js">
angular.module('httpExample', [])
.config(['$sceDelegateProvider', function($sceDelegateProvider) {
$sceDelegateProvider.resourceUrlWhitelist([
'self',
'https://angularjs.org/**'
]);
}])
.controller('FetchController', ['$scope', '$http', '$templateCache',
function($scope, $http, $templateCache) {
$scope.method = 'GET';
Expand Down Expand Up @@ -948,7 +954,7 @@ function $HttpProvider() {
throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig);
}

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

Expand Down Expand Up @@ -1088,7 +1094,12 @@ function $HttpProvider() {
}

// send request
return sendReq(config, reqData).then(transformResponse, transformResponse);
try {
var promise = sendReq(config, reqData);
} catch (e) {
return $q.reject(e);
}
return promise.then(transformResponse, transformResponse);
}

function transformResponse(response) {
Expand Down Expand Up @@ -1249,12 +1260,19 @@ function $HttpProvider() {
cache,
cachedResp,
reqHeaders = config.headers,
url = buildUrl(config.url, config.paramSerializer(config.params));
url = config.url;

if (lowercase(config.method) === 'jsonp') {
// This is a pretty sensitive operation where we're allowing a script to have full access to
// our DOM and JS space. So we require that the URL satisfies SCE.RESOURCE_URL.
url = $sce.getTrustedResourceUrl(url);
}

url = buildUrl(url, config.paramSerializer(config.params));

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


if ((config.cache || defaults.cache) && config.cache !== false &&
(config.method === 'GET' || config.method === 'JSONP')) {
cache = isObject(config.cache) ? config.cache
Expand Down
45 changes: 38 additions & 7 deletions test/ng/httpSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,12 +289,18 @@ describe('$http', function() {


describe('the instance', function() {
var $httpBackend, $http, $rootScope;
var $httpBackend, $http, $rootScope, $sce;

beforeEach(inject(['$httpBackend', '$http', '$rootScope', function($hb, $h, $rs) {
beforeEach(module(function($sceDelegateProvider) {
// Setup a special whitelisted url that we can use in testing JSONP requests
$sceDelegateProvider.resourceUrlWhitelist(['http://special.whitelisted.resource.com/**']);
}));

beforeEach(inject(['$httpBackend', '$http', '$rootScope', '$sce', function($hb, $h, $rs, $sc) {
$httpBackend = $hb;
$http = $h;
$rootScope = $rs;
$sce = $sc;
spyOn($rootScope, '$apply').and.callThrough();
}]));

Expand Down Expand Up @@ -602,7 +608,7 @@ describe('$http', function() {
});

$httpBackend.expect('JSONP', '/some').respond(200);
$http({url: '/some', method: 'JSONP'}).then(callback);
$http({url: $sce.trustAsResourceUrl('/some'), method: 'JSONP'}).then(callback);
$httpBackend.flush();
expect(callback).toHaveBeenCalledOnce();
});
Expand Down Expand Up @@ -1010,16 +1016,41 @@ describe('$http', function() {

it('should have jsonp()', function() {
$httpBackend.expect('JSONP', '/url').respond('');
$http.jsonp('/url');
$http.jsonp($sce.trustAsResourceUrl('/url'));
});


it('jsonp() should allow config param', function() {
$httpBackend.expect('JSONP', '/url', undefined, checkHeader('Custom', 'Header')).respond('');
$http.jsonp('/url', {headers: {'Custom': 'Header'}});
$http.jsonp($sce.trustAsResourceUrl('/url'), {headers: {'Custom': 'Header'}});
});
});

describe('jsonp trust', function() {
it('should throw error if the url is not a trusted resource', function() {
var success, error;
$http({method: 'JSONP', url: 'http://example.org/path?cb=JSON_CALLBACK', callback: callback}).catch(
function(e) { error = e; }
);
$rootScope.$digest();
expect(error.message).toContain("[$sce:insecurl]");
});

it('should not throw error if the url is an explicitly trusted resource', function() {
expect(function() {
$httpBackend.expect('JSONP', 'http://example.org/path?cb=JSON_CALLBACK').respond('');
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path?cb=JSON_CALLBACK'), callback: callback});
}).not.toThrow();
});

it('jsonp() should allow trusted url', inject(['$sce', function($sce) {
$httpBackend.expect('JSONP', '/url').respond('');
$http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url')});

$httpBackend.expect('JSONP', '/url?a=b').respond('');
$http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url'), params: {a: 'b'}});
}]));
});

describe('callbacks', function() {

Expand Down Expand Up @@ -1481,10 +1512,10 @@ describe('$http', function() {

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

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

expect(callback).toHaveBeenCalledOnce();
Expand Down

0 comments on commit fcd8fa9

Please sign in to comment.