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

Commit fb66341

Browse files
feat($http): JSONP callback must be specified by jsonpCallbackParam config
The query parameter that will be used to transmit the JSONP callback to the server is now specified via the `jsonpCallbackParam` 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 `jsonpCallbackParam` 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. Closes #15161 Closes #15143 Closes #11352 Closes #11328 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 `jsonpCallbackParam` property of the config object, or app-wide via the `$http.defaults.jsonpCallbackParam` 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'}); ```
1 parent 6476af8 commit fb66341

File tree

4 files changed

+131
-25
lines changed

4 files changed

+131
-25
lines changed
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
@ngdoc error
2+
@name $http:badjsonp
3+
@fullName Bad JSONP Request Configuration
4+
@description
5+
6+
This error occurs when the URL generated from the configuration object contains a parameter with the
7+
same name as the configured `jsonpCallbackParam` property; or when it contains a parameter whose
8+
value is `JSON_CALLBACK`.
9+
10+
`$http` JSONP requests need to attach a callback query parameter to the URL. The name of this
11+
parameter is specified in the configuration object (or in the defaults) via the `jsonpCallbackParam`
12+
property. You must not provide your own parameter with this name in the configuratio of the request.
13+
14+
In previous versions of Angular, you specified where to add the callback parameter value via the
15+
`JSON_CALLBACK` placeholder. This is no longer allowed.
16+
17+
To resolve this error, remove any parameters that have the same name as the `jsonpCallbackParam`;
18+
and/or remove any parameters that have a value of `JSON_CALLBACK`.
19+
20+
For more information, see the {@link ng.$http#jsonp `$http.jsonp()`} method API documentation.

docs/content/guide/concepts.ngdoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ The following example shows how this is done with Angular:
326326
var YAHOO_FINANCE_URL_PATTERN =
327327
'//query.yahooapis.com/v1/public/yql?q=select * from ' +
328328
'yahoo.finance.xchange where pair in ("PAIRS")&format=json&' +
329-
'env=store://datatables.org/alltableswithkeys&callback=JSON_CALLBACK';
329+
'env=store://datatables.org/alltableswithkeys';
330330
var currencies = ['USD', 'EUR', 'CNY'];
331331
var usdToForeignRates = {};
332332

src/ng/http.js

+54-7
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ function $HttpProvider() {
286286
* If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}.
287287
* Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}.
288288
*
289+
* - **`defaults.jsonpCallbackParam`** - `{string} - the name of the query parameter that passes the name of the
290+
* callback in a JSONP request. The value of this parameter will be replaced with the expression generated by the
291+
* {@link $jsonpCallbacks} service. Defaults to `'callback'`.
292+
*
289293
**/
290294
var defaults = this.defaults = {
291295
// transform incoming response data
@@ -309,7 +313,9 @@ function $HttpProvider() {
309313
xsrfCookieName: 'XSRF-TOKEN',
310314
xsrfHeaderName: 'X-XSRF-TOKEN',
311315

312-
paramSerializer: '$httpParamSerializer'
316+
paramSerializer: '$httpParamSerializer',
317+
318+
jsonpCallbackParam: 'callback'
313319
};
314320

315321
var useApplyAsync = false;
@@ -869,11 +875,11 @@ function $HttpProvider() {
869875
<button id="samplegetbtn" ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button>
870876
<button id="samplejsonpbtn"
871877
ng-click="updateModel('JSONP',
872-
'https://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">
878+
'https://angularjs.org/greet.php?name=Super%20Hero')">
873879
Sample JSONP
874880
</button>
875881
<button id="invalidjsonpbtn"
876-
ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist&callback=JSON_CALLBACK')">
882+
ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist')">
877883
Invalid JSONP
878884
</button>
879885
<pre>http status code: {{status}}</pre>
@@ -964,7 +970,8 @@ function $HttpProvider() {
964970
method: 'get',
965971
transformRequest: defaults.transformRequest,
966972
transformResponse: defaults.transformResponse,
967-
paramSerializer: defaults.paramSerializer
973+
paramSerializer: defaults.paramSerializer,
974+
jsonpCallbackParam: defaults.jsonpCallbackParam
968975
}, requestConfig);
969976

970977
config.headers = mergeHeaders(requestConfig);
@@ -1158,11 +1165,27 @@ function $HttpProvider() {
11581165
* @description
11591166
* Shortcut method to perform `JSONP` request.
11601167
*
1161-
* Note that, since JSONP requests are sensitive because the response is given full acces to the browser,
1168+
* Note that, since JSONP requests are sensitive because the response is given full access to the browser,
11621169
* the url must be declared, via {@link $sce} as a trusted resource URL.
11631170
* You can trust a URL by adding it to the whitelist via
11641171
* {@link $sceDelegateProvider#resourceUrlWhitelist `$sceDelegateProvider.resourceUrlWhitelist`} or
1165-
* by explicitly trusted the URL via {@link $sce#trustAsResourceUrl `$sce.trustAsResourceUrl(url)`}.
1172+
* by explicitly trusting the URL via {@link $sce#trustAsResourceUrl `$sce.trustAsResourceUrl(url)`}.
1173+
*
1174+
* JSONP requests must specify a callback to be used in the response from the server. This callback
1175+
* is passed as a query parameter in the request. You must specify the name of this parameter by
1176+
* setting the `jsonpCallbackParam` property on the request config object.
1177+
*
1178+
* ```
1179+
* $http.jsonp('some/trusted/url', {jsonpCallbackParam: 'callback'})
1180+
* ```
1181+
*
1182+
* You can also specify a default callback parameter name in `$http.defaults.jsonpCallbackParam`.
1183+
* Initially this is set to `'callback'`.
1184+
*
1185+
* <div class="alert alert-danger">
1186+
* You can no longer use the `JSON_CALLBACK` string as a placeholder for specifying where the callback
1187+
* parameter value should go.
1188+
* </div>
11661189
*
11671190
* If you would like to customise where and how the callbacks are stored then try overriding
11681191
* or decorating the {@link $jsonpCallbacks} service.
@@ -1267,9 +1290,10 @@ function $HttpProvider() {
12671290
cache,
12681291
cachedResp,
12691292
reqHeaders = config.headers,
1293+
isJsonp = lowercase(config.method) === 'jsonp',
12701294
url = config.url;
12711295

1272-
if (lowercase(config.method) === 'jsonp') {
1296+
if (isJsonp) {
12731297
// JSONP is a pretty sensitive operation where we're allowing a script to have full access to
12741298
// our DOM and JS space. So we require that the URL satisfies SCE.RESOURCE_URL.
12751299
url = $sce.getTrustedResourceUrl(url);
@@ -1280,6 +1304,11 @@ function $HttpProvider() {
12801304

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

1307+
if (isJsonp) {
1308+
// Check the url and add the JSONP callback placeholder
1309+
url = sanitizeJsonpCallbackParam(url, config.jsonpCallbackParam);
1310+
}
1311+
12831312
$http.pendingRequests.push(config);
12841313
promise.then(removePendingReq, removePendingReq);
12851314

@@ -1414,5 +1443,23 @@ function $HttpProvider() {
14141443
}
14151444
return url;
14161445
}
1446+
1447+
function sanitizeJsonpCallbackParam(url, key) {
1448+
if (/[&?][^=]+=JSON_CALLBACK/.test(url)) {
1449+
// Throw if the url already contains a reference to JSON_CALLBACK
1450+
throw $httpMinErr('badjsonp', 'Illegal use of JSON_CALLBACK in url, "{0}"', url);
1451+
}
1452+
1453+
var callbackParamRegex = new RegExp('[&?]' + key + '=');
1454+
if (callbackParamRegex.test(url)) {
1455+
// Throw if the callback param was already provided
1456+
throw $httpMinErr('badjsonp', 'Illegal use of callback param, "{0}", in url, "{1}"', key, url);
1457+
}
1458+
1459+
// Add in the JSON_CALLBACK callback param value
1460+
url += ((url.indexOf('?') === -1) ? '?' : '&') + key + '=JSON_CALLBACK';
1461+
1462+
return url;
1463+
}
14171464
}];
14181465
}

test/ng/httpSpec.js

+56-17
Original file line numberDiff line numberDiff line change
@@ -326,10 +326,8 @@ describe('$http', function() {
326326
});
327327

328328
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');
329+
$httpBackend.expect('GET', '/url').respond('');
330+
$http({url: $sce.trustAsResourceUrl('/url')});
333331
});
334332

335333
it('should send GET requests if no method specified', function() {
@@ -622,7 +620,7 @@ describe('$http', function() {
622620
expect(r.headers()).toEqual(Object.create(null));
623621
});
624622

625-
$httpBackend.expect('JSONP', '/some').respond(200);
623+
$httpBackend.expect('JSONP', '/some?callback=JSON_CALLBACK').respond(200);
626624
$http({url: $sce.trustAsResourceUrl('/some'), method: 'JSONP'}).then(callback);
627625
$httpBackend.flush();
628626
expect(callback).toHaveBeenCalledOnce();
@@ -1030,39 +1028,80 @@ describe('$http', function() {
10301028
});
10311029

10321030
it('should have jsonp()', function() {
1033-
$httpBackend.expect('JSONP', '/url').respond('');
1031+
$httpBackend.expect('JSONP', '/url?callback=JSON_CALLBACK').respond('');
10341032
$http.jsonp($sce.trustAsResourceUrl('/url'));
10351033
});
10361034

10371035

10381036
it('jsonp() should allow config param', function() {
1039-
$httpBackend.expect('JSONP', '/url', undefined, checkHeader('Custom', 'Header')).respond('');
1037+
$httpBackend.expect('JSONP', '/url?callback=JSON_CALLBACK', undefined, checkHeader('Custom', 'Header')).respond('');
10401038
$http.jsonp($sce.trustAsResourceUrl('/url'), {headers: {'Custom': 'Header'}});
10411039
});
10421040
});
10431041

10441042
describe('jsonp trust', function() {
10451043
it('should throw error if the url is not a trusted resource', function() {
10461044
var success, error;
1047-
$http({method: 'JSONP', url: 'http://example.org/path?cb=JSON_CALLBACK'}).catch(
1048-
function(e) { error = e; }
1049-
);
1045+
$http({method: 'JSONP', url: 'http://example.org/path'})
1046+
.catch(function(e) { error = e; });
10501047
$rootScope.$digest();
10511048
expect(error.message).toContain('[$sce:insecurl]');
10521049
});
10531050

10541051
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')});
1052+
$httpBackend.expect('JSONP', 'http://example.org/path?callback=JSON_CALLBACK').respond('');
1053+
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path')});
10571054
});
10581055

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

1063-
$httpBackend.expect('JSONP', '/url?a=b').respond('');
1060+
$httpBackend.expect('JSONP', '/url?a=b&callback=JSON_CALLBACK').respond('');
10641061
$http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url'), params: {a: 'b'}});
10651062
});
1063+
1064+
it('should error if the URL contains a JSON_CALLBACK parameter', function() {
1065+
var error;
1066+
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path?callback=JSON_CALLBACK')})
1067+
.catch(function(e) { error = e; });
1068+
$rootScope.$digest();
1069+
expect(error.message).toContain('[$http:badjsonp]');
1070+
1071+
error = undefined;
1072+
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path?other=JSON_CALLBACK')})
1073+
.catch(function(e) { error = e; });
1074+
$rootScope.$digest();
1075+
expect(error.message).toContain('[$http:badjsonp]');
1076+
});
1077+
1078+
it('should error if a param contains a JSON_CALLBACK value', function() {
1079+
var error;
1080+
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path'), params: {callback: 'JSON_CALLBACK'}})
1081+
.catch(function(e) { error = e; });
1082+
$rootScope.$digest();
1083+
expect(error.message).toContain('[$http:badjsonp]');
1084+
1085+
error = undefined;
1086+
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path'), params: {other: 'JSON_CALLBACK'}})
1087+
.catch(function(e) { error = e; });
1088+
$rootScope.$digest();
1089+
expect(error.message).toContain('[$http:badjsonp]');
1090+
});
1091+
1092+
it('should error if there is already a param matching the jsonpCallbackParam key', function() {
1093+
var error;
1094+
$http({ method: 'JSONP', url: $sce.trustAsResourceUrl('http://example.org/path'), params: {callback: 'evilThing'}})
1095+
.catch(function(e) { error = e; });
1096+
$rootScope.$digest();
1097+
expect(error.message).toContain('[$http:badjsonp]');
1098+
1099+
error = undefined;
1100+
$http({ method: 'JSONP', jsonpCallbackParam: 'cb', url: $sce.trustAsResourceUrl('http://example.org/path'), params: {cb: 'evilThing'}})
1101+
.catch(function(e) { error = e; });
1102+
$rootScope.$digest();
1103+
expect(error.message).toContain('[$http:badjsonp]');
1104+
});
10661105
});
10671106

10681107
describe('callbacks', function() {
@@ -1524,11 +1563,11 @@ describe('$http', function() {
15241563
}));
15251564

15261565
it('should cache JSONP request when cache is provided', inject(function($rootScope) {
1527-
$httpBackend.expect('JSONP', '/url?cb=JSON_CALLBACK').respond('content');
1528-
$http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url?cb=JSON_CALLBACK'), cache: cache});
1566+
$httpBackend.expect('JSONP', '/url?callback=JSON_CALLBACK').respond('content');
1567+
$http({method: 'JSONP', url: $sce.trustAsResourceUrl('/url'), cache: cache});
15291568
$httpBackend.flush();
15301569

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

15341573
expect(callback).toHaveBeenCalledOnce();

0 commit comments

Comments
 (0)