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

Commit

Permalink
feat($http): specify the JSONP callback via the callbackParam confi…
Browse files Browse the repository at this point in the history
…g value

The query parameter that will be used to transmit the JSONP callback to the
server is now specified via the `callbackParam` config value, instead of
using the `JSON_CALLBACK` placeholder.

* Any use of `JSON_CALLBACK` in a JSONP request URL will cause an error.
* Any request that provides a parameter with the same name as that given
by the `callbackParam` config property will cause an error.

This is to prevent malicious attack via the response from an app inadvertently
allowing untrusted data to be used to generate the callback parameter.

BREAKING CHANGE

You can no longer use the `JSON_CALLBACK` placeholder in your JSONP requests.
Instead you must provide the name of the query parameter that will pass the
callback via the `callbackParam` property of the config object, or app-wide via
the `$http.defaults.callbackParam` property, which is `callback` by default.

Before this change:

```
$http.json('trusted/url?callback=JSON_CALLBACK');
$http.json('other/trusted/url', {params:cb:'JSON_CALLBACK'});
```

After this change:

```
$http.json('trusted/url');
$http.json('other/trusted/url', {callbackParam:'cb'});
```
  • Loading branch information
petebacondarwin committed Sep 20, 2016
1 parent 3d1512b commit 308f3bf
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 17 deletions.
18 changes: 18 additions & 0 deletions docs/content/error/$http/badjsonp.ngdoc
@@ -0,0 +1,18 @@
@ngdoc error
@name $http:badjsonp
@fullName Bad JSONP Request Configuration
@description

This error occurs when the URL generated from the configuration object contains a parameter with the same name as the configured `callbackParam`
property; or when it contains a parameter whose value is `JSON_CALLBACK`.

`$http` JSON requests need to attach a callback query parameter to the URL. The name of this parameter is specified in the configuration
object (or in the defaults) via the `callbackParam` property. You must not provide your own parameter with this name in the configuration
of the request.

In previous versions of Angular, you specified where to add the callback parameter value via the `JSON_CALLBACK` placeholder. This is no longer
allowed.

To resolve this error, remove any parameters that have the same name as the `callbackParam`; and/or remove any parameters that have a value of `JSON_CALLBACK`.

For more information, see the {@link ng.$http#jsonp `$http.jsonp()`} method API documentation.
2 changes: 1 addition & 1 deletion docs/content/guide/concepts.ngdoc
Expand Up @@ -326,7 +326,7 @@ The following example shows how this is done with Angular:
var YAHOO_FINANCE_URL_PATTERN =
'//query.yahooapis.com/v1/public/yql?q=select * from ' +
'yahoo.finance.xchange where pair in ("PAIRS")&format=json&' +
'env=store://datatables.org/alltableswithkeys&callback=JSON_CALLBACK';
'env=store://datatables.org/alltableswithkeys';
var currencies = ['USD', 'EUR', 'CNY'];
var usdToForeignRates = {};

Expand Down
53 changes: 50 additions & 3 deletions src/ng/http.js
Expand Up @@ -286,6 +286,10 @@ function $HttpProvider() {
* If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}.
* Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}.
*
* - **`defaults.callbackParam`** - `{string} - the name of the query parameter that passes the callback in a JSONP
* request. The value of this parameter will be replaced with the expression generated by the {@link jsonpCallbacks}
* service. Defaults to `'callback'`.
*
**/
var defaults = this.defaults = {
// transform incoming response data
Expand All @@ -309,7 +313,9 @@ function $HttpProvider() {
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',

paramSerializer: '$httpParamSerializer'
paramSerializer: '$httpParamSerializer',

callbackParam: 'callback'
};

var useApplyAsync = false;
Expand Down Expand Up @@ -964,7 +970,8 @@ function $HttpProvider() {
method: 'get',
transformRequest: defaults.transformRequest,
transformResponse: defaults.transformResponse,
paramSerializer: defaults.paramSerializer
paramSerializer: defaults.paramSerializer,
callbackParam: defaults.callbackParam
}, requestConfig);

config.headers = mergeHeaders(requestConfig);
Expand Down Expand Up @@ -1168,6 +1175,22 @@ function $HttpProvider() {
* {@link $sceDelegateProvider#resourceUrlWhitelist `$sceDelegateProvider.resourceUrlWhitelist`} or
* by explicitly trusted the URL via {@link $sce#trustAsResourceUrl `$sce.trustAsResourceUrl(url)`}.
*
* JSONP requests must specify a callback to be used in the response from the server. This callback
* is passed as as a query parameter in the request. You must specify the name of this parameter by
* setting the `callbackParam` property on the request config object.
*
* ```
* $http.jsonp('some/trusted/url', {callbackParam: 'callback'})
* ```
*
* You can also specify a global callback parameter key in `$http.defaults.callbackParam`.
* By default this is set to `callback`.
*
* <div class="alert alert-danger">
* You can no longer use the `JSON_CALLBACK` string as a placeholder for specifying where the callback
* parameter value should go.
* </div>
*
* If you would like to customise where and how the callbacks are stored then try overriding
* or decorating the {@link $jsonpCallbacks} service.
*
Expand Down Expand Up @@ -1271,9 +1294,10 @@ function $HttpProvider() {
cache,
cachedResp,
reqHeaders = config.headers,
isJsonp = lowercase(config.method) === 'jsonp',
url = config.url;

if (lowercase(config.method) === 'jsonp') {
if (isJsonp) {
// JSONP 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);
Expand All @@ -1284,6 +1308,11 @@ function $HttpProvider() {

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

if (isJsonp) {
// Check the url and add the JSONP callback placeholder
url = sanitizeJsonpCallbackParam(url, config.callbackParam);
}

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

Expand Down Expand Up @@ -1418,5 +1447,23 @@ function $HttpProvider() {
}
return url;
}

function sanitizeJsonpCallbackParam(url, key) {
if (/[&?][^=]+=JSON_CALLBACK/.test(url)) {
// Throw if the url already contains a reference to JSON_CALLBACK
throw $httpMinErr('badjsonp', 'Illegal use of JSON_CALLBACK in url, "{0}"', url);
}

var callbackParamRegex = new RegExp('([&?]' + key + '=)');
if (callbackParamRegex.test(url)) {
// Throw if the callback param was already provided
throw $httpMinErr('badjsonp', 'Illegal use of callback param, "{0}", in url, "{1}"', key, url);
}

// Add in the JSON_CALLBACK callback param value
url += ((url.indexOf('?') === -1) ? '?' : '&') + key + '=JSON_CALLBACK';

return url;
}
}];
}
67 changes: 54 additions & 13 deletions test/ng/httpSpec.js
Expand Up @@ -622,7 +622,7 @@ describe('$http', function() {
expect(r.headers()).toEqual(Object.create(null));
});

$httpBackend.expect('JSONP', '/some').respond(200);
$httpBackend.expect('JSONP', '/some?callback=JSON_CALLBACK').respond(200);
$http({url: $sce.trustAsResourceUrl('/some'), method: 'JSONP'}).then(callback);
$httpBackend.flush();
expect(callback).toHaveBeenCalledOnce();
Expand Down Expand Up @@ -1030,39 +1030,80 @@ describe('$http', function() {
});

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


it('jsonp() should allow config param', function() {
$httpBackend.expect('JSONP', '/url', undefined, checkHeader('Custom', 'Header')).respond('');
$httpBackend.expect('JSONP', '/url?callback=JSON_CALLBACK', undefined, checkHeader('Custom', 'Header')).respond('');
$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'}).catch(
function(e) { error = e; }
);
$http({method: 'JSONP', url: 'http://example.org/path'})
.catch(function(e) { error = e; });
$rootScope.$digest();
expect(error.message).toContain('[$sce:insecurl]');
});

it('should accept an explicitly trusted resource url', 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')});
$httpBackend.expect('JSONP', 'http://example.org/path?callback=JSON_CALLBACK').respond('');
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path')});
});

it('jsonp() should accept explictly trusted urls', function() {
$httpBackend.expect('JSONP', '/url').respond('');
$httpBackend.expect('JSONP', '/url?callback=JSON_CALLBACK').respond('');
$http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url')});

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

it('should error if the URL contains a JSON_CALLBACK parameter', function() {
var error;
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path?callback=JSON_CALLBACK')})
.catch(function(e) { error = e; });
$rootScope.$digest();
expect(error.message).toContain('[$http:badjsonp]');

error = undefined;
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path?other=JSON_CALLBACK')})
.catch(function(e) { error = e; });
$rootScope.$digest();
expect(error.message).toContain('[$http:badjsonp]');
});

it('should error if a param contains a JSON_CALLBACK value', function() {
var error;
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path'), params: {callback: 'JSON_CALLBACK'}})
.catch(function(e) { error = e; });
$rootScope.$digest();
expect(error.message).toContain('[$http:badjsonp]');

error = undefined;
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path'), params: {other: 'JSON_CALLBACK'}})
.catch(function(e) { error = e; });
$rootScope.$digest();
expect(error.message).toContain('[$http:badjsonp]');
});

it('should error if there is already a param matching the callbackParam key', function() {
var error;
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path'), params: {callback: 'evilThing'}})
.catch(function(e) { error = e; });
$rootScope.$digest();
expect(error.message).toContain('[$http:badjsonp]');

error = undefined;
$http({ method: 'JSONP', callbackParam: 'cb', url: $sce.trustAsResourceUrl('http://example.org/path'), params: {cb: 'evilThing'}})
.catch(function(e) { error = e; });
$rootScope.$digest();
expect(error.message).toContain('[$http:badjsonp]');
});
});

describe('callbacks', function() {
Expand Down Expand Up @@ -1524,11 +1565,11 @@ 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: $sce.trustAsResourceUrl('/url?cb=JSON_CALLBACK'), cache: cache});
$httpBackend.expect('JSONP', '/url?callback=JSON_CALLBACK').respond('content');
$http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url'), cache: cache});
$httpBackend.flush();

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

expect(callback).toHaveBeenCalledOnce();
Expand Down

0 comments on commit 308f3bf

Please sign in to comment.