diff --git a/src/ng/http.js b/src/ng/http.js index 0cbc72571a32..a5a115784f70 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -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'); @@ -881,6 +881,12 @@ function $HttpProvider() { 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'; @@ -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); } @@ -1249,12 +1255,14 @@ function $HttpProvider() { cache, cachedResp, reqHeaders = config.headers, - url = buildUrl(config.url, config.paramSerializer(config.params)); + needsTrustedUrl = (lowercase(config.method) === 'jsonp'), + url = needsTrustedUrl ? $sce.getTrustedResourceUrl(config.url) : config.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 @@ -1293,7 +1301,7 @@ function $HttpProvider() { reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; } - $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, + $httpBackend(config.method, needsTrustedUrl ? $sce.trustAsResourceUrl(url) : url, reqData, done, reqHeaders, config.timeout, config.withCredentials, config.responseType, createApplyHandlers(config.eventHandlers), createApplyHandlers(config.uploadEventHandlers)); diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js index 501c1de86c73..1b85c6362e10 100644 --- a/src/ng/httpBackend.js +++ b/src/ng/httpBackend.js @@ -49,17 +49,19 @@ function $xhrFactoryProvider() { * $httpBackend} which can be trained with responses. */ function $HttpBackendProvider() { - this.$get = ['$browser', '$jsonpCallbacks', '$document', '$xhrFactory', function($browser, $jsonpCallbacks, $document, $xhrFactory) { - return createHttpBackend($browser, $xhrFactory, $browser.defer, $jsonpCallbacks, $document[0]); + this.$get = ['$sce', '$browser', '$jsonpCallbacks', '$document', '$xhrFactory', function($sce, $browser, $jsonpCallbacks, $document, $xhrFactory) { + return createHttpBackend($sce, $browser, $xhrFactory, $browser.defer, $jsonpCallbacks, $document[0]); }]; } -function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { +function createHttpBackend($sce, $browser, createXhr, $browserDefer, callbacks, rawDocument) { // TODO(vojta): fix the signature return function(method, url, post, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) { - url = url || $browser.url(); if (lowercase(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) || $browser.url(); var callbackPath = callbacks.createCallback(url); var jsonpDone = jsonpReq(url, callbackPath, function(status, text) { // jsonpReq only ever sets status to 200 (OK), 404 (ERROR) or -1 (WAITING) @@ -69,6 +71,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc }); } else { + url = url || $browser.url(); var xhr = createXhr(method, url); xhr.open(method, url, true); diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 2591716bd998..1d5d688c6b2d 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -1296,7 +1296,7 @@ angular.mock.dump = function(object) { ``` */ angular.mock.$HttpBackendProvider = function() { - this.$get = ['$rootScope', '$timeout', createHttpBackendMock]; + this.$get = ['$sce', '$rootScope', '$timeout', createHttpBackendMock]; }; /** @@ -1313,7 +1313,7 @@ angular.mock.$HttpBackendProvider = function() { * @param {Object=} $browser Auto-flushing enabled if specified * @return {Object} Instance of $httpBackend mock */ -function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { +function createHttpBackendMock($sce, $rootScope, $timeout, $delegate, $browser) { var definitions = [], expectations = [], responses = [], @@ -1337,6 +1337,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { expectation = expectations[0], wasExpected = false; + url = $sce.valueOf(url); xhr.$$events = eventHandlers; xhr.upload.$$events = uploadEventHandlers; @@ -2666,7 +2667,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { */ angular.mock.e2e = {}; angular.mock.e2e.$httpBackendDecorator = - ['$rootScope', '$timeout', '$delegate', '$browser', createHttpBackendMock]; + ['$sce', '$rootScope', '$timeout', '$delegate', '$browser', createHttpBackendMock]; /** diff --git a/test/ng/httpBackendSpec.js b/test/ng/httpBackendSpec.js index 1fce7c92a602..ec36fa0d03c4 100644 --- a/test/ng/httpBackendSpec.js +++ b/test/ng/httpBackendSpec.js @@ -3,11 +3,16 @@ describe('$httpBackend', function() { - var $backend, $browser, $jsonpCallbacks, + var $sce, $backend, $browser, $jsonpCallbacks, xhr, fakeDocument, callback; - beforeEach(inject(function($injector) { + 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(function($injector) { + $sce = $injector.get('$sce'); $browser = $injector.get('$browser'); fakeDocument = { @@ -48,7 +53,7 @@ describe('$httpBackend', function() { } }; - $backend = createHttpBackend($browser, createMockXhr, $browser.defer, $jsonpCallbacks, fakeDocument); + $backend = createHttpBackend($sce, $browser, createMockXhr, $browser.defer, $jsonpCallbacks, fakeDocument); callback = jasmine.createSpy('done'); })); @@ -273,7 +278,7 @@ describe('$httpBackend', function() { it('should call $xhrFactory with method and url', function() { var mockXhrFactory = jasmine.createSpy('mockXhrFactory').and.callFake(createMockXhr); - $backend = createHttpBackend($browser, mockXhrFactory, $browser.defer, $jsonpCallbacks, fakeDocument); + $backend = createHttpBackend($sce, $browser, mockXhrFactory, $browser.defer, $jsonpCallbacks, fakeDocument); $backend('GET', '/some-url', 'some-data', noop); expect(mockXhrFactory).toHaveBeenCalledWith('GET', '/some-url'); }); @@ -334,20 +339,20 @@ describe('$httpBackend', function() { var SCRIPT_URL = /([^\?]*)\?cb=(.*)/; - it('should add script tag for JSONP request', function() { callback.and.callFake(function(status, response) { expect(status).toBe(200); expect(response).toBe('some-data'); }); - $backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback); + $backend('JSONP', 'http://special.whitelisted.resource.com/path?cb=JSON_CALLBACK', null, callback); + expect(fakeDocument.$$scripts.length).toBe(1); var script = fakeDocument.$$scripts.shift(), url = script.src.match(SCRIPT_URL); - expect(url[1]).toBe('http://example.org/path'); + expect(url[1]).toBe('http://special.whitelisted.resource.com/path'); $jsonpCallbacks[url[2]]('some-data'); browserTrigger(script, 'load'); @@ -358,7 +363,8 @@ describe('$httpBackend', function() { it('should clean up the callback and remove the script', function() { spyOn($jsonpCallbacks, 'removeCallback').and.callThrough(); - $backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback); + $backend('JSONP', 'http://special.whitelisted.resource.com/path?cb=JSON_CALLBACK', null, callback); + expect(fakeDocument.$$scripts.length).toBe(1); @@ -375,6 +381,7 @@ describe('$httpBackend', function() { it('should set url to current location if not specified or empty string', function() { $backend('JSONP', undefined, null, callback); + expect(fakeDocument.$$scripts[0].src).toBe($browser.url()); fakeDocument.$$scripts.shift(); @@ -390,7 +397,8 @@ describe('$httpBackend', function() { expect(status).toBe(-1); }); - $backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback, null, 2000); + $backend('JSONP', 'http://special.whitelisted.resource.com/path?cb=JSON_CALLBACK', null, callback, null, 2000); + expect(fakeDocument.$$scripts.length).toBe(1); expect($browser.deferredFns[0].time).toBe(2000); @@ -405,6 +413,18 @@ describe('$httpBackend', function() { }); + it('should throw error if the url is not a trusted resource', function() { + expect(function() { + $backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback); + }).toThrowMinErr('$sce', 'insecurl'); + }); + + it('should not throw error if the url is an explicitly trusted resource', function() { + expect(function() { + $backend('JSONP', $sce.trustAsResourceUrl('http://example.org/path?cb=JSON_CALLBACK'), null, callback); + }).not.toThrow(); + }); + // TODO(vojta): test whether it fires "async-start" // TODO(vojta): test whether it fires "async-end" on both success and error }); @@ -420,7 +440,7 @@ describe('$httpBackend', function() { } beforeEach(function() { - $backend = createHttpBackend($browser, createMockXhr); + $backend = createHttpBackend($sce, $browser, createMockXhr); }); diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index 09a0cfb3199d..7dfb7a07bc66 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -1018,6 +1018,14 @@ describe('$http', function() { $httpBackend.expect('JSONP', '/url', undefined, checkHeader('Custom', 'Header')).respond(''); $http.jsonp('/url', {headers: {'Custom': 'Header'}}); }); + + it('jsonp() should allow trusted url', inject(['$sce', function($sce) { + $httpBackend.expect('JSONP', '/url').respond(''); + $http.jsonp($sce.trustAsResourceUrl('/url')); + + $httpBackend.expect('JSONP', '/url?a=b').respond(''); + $http.jsonp($sce.trustAsResourceUrl('/url'), {params: {a: 'b'}}); + }])); }); diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js index dd325e10e580..029627d5ed99 100644 --- a/test/ngRoute/routeSpec.js +++ b/test/ngRoute/routeSpec.js @@ -1016,9 +1016,10 @@ describe('$route', function() { $routeProvider = _$routeProvider_; $provide.decorator('$sce', function($delegate) { + function getVal(v) { return v.getVal ? v.getVal() : v; } $delegate.trustAsResourceUrl = function(url) { return new MySafeResourceUrl(url); }; - $delegate.getTrustedResourceUrl = function(v) { return v.getVal(); }; - $delegate.valueOf = function(v) { return v.getVal(); }; + $delegate.getTrustedResourceUrl = function(v) { return getVal(v); }; + $delegate.valueOf = function(v) { return getVal(v); }; return $delegate; }); });