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

feat($httpBackend): JSONP requests now require trusted resource #15161

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/content/error/$http/badjsonp.ngdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@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 `jsonpCallbackParam` property; or when it contains a parameter whose
value is `JSON_CALLBACK`.

`$http` JSONP 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 `jsonpCallbackParam`
property. You must not provide your own parameter with this name in the configuratio 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 `jsonpCallbackParam`;
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.
6 changes: 5 additions & 1 deletion docs/content/error/$http/badreq.ngdoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
@fullName Bad Request Configuration
@description

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.
This error occurs when the request configuration parameter passed to the {@link ng.$http `$http`} service is not a valid object.
`$http` expects a single parameter, the request configuration object, but received a parameter that was not an object or did not contain valid properties.

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.

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

Expand Down
2 changes: 1 addition & 1 deletion docs/content/guide/concepts.ngdoc
Original file line number Diff line number Diff line change
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
109 changes: 92 additions & 17 deletions src/ng/http.js
Original file line number Diff line number Diff line change
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.jsonpCallbackParam`** - `{string} - the name of the query parameter that passes the name of 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',

jsonpCallbackParam: 'callback'
};

var useApplyAsync = false;
Expand Down Expand Up @@ -379,8 +385,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 @@ -802,7 +808,8 @@ function $HttpProvider() {
* processed. The object has following properties:
*
* - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc)
* - **url** – `{string}` – Absolute or relative URL of the resource that is being requested.
* - **url** – `{string|TrustedObject}` – Absolute or relative URL of the resource that is being requested;
* or an object created by a call to `$sce.trustAsResourceUrl(url)`.
* - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be serialized
* with the `paramSerializer` and appended as GET parameters.
* - **data** – `{string|Object}` – Data to be sent as the request message data.
Expand Down Expand Up @@ -868,11 +875,11 @@ function $HttpProvider() {
<button id="samplegetbtn" ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button>
<button id="samplejsonpbtn"
ng-click="updateModel('JSONP',
'https://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">
'https://angularjs.org/greet.php?name=Super%20Hero')">
Sample JSONP
</button>
<button id="invalidjsonpbtn"
ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist&callback=JSON_CALLBACK')">
ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist')">
Invalid JSONP
</button>
<pre>http status code: {{status}}</pre>
Expand All @@ -881,6 +888,13 @@ function $HttpProvider() {
</file>
<file name="script.js">
angular.module('httpExample', [])
.config(['$sceDelegateProvider', function($sceDelegateProvider) {
// We must whitelist the JSONP endpoint that we are using to show that we trust it
$sceDelegateProvider.resourceUrlWhitelist([
'self',
'https://angularjs.org/**'
]);
}])
.controller('FetchController', ['$scope', '$http', '$templateCache',
function($scope, $http, $templateCache) {
$scope.method = 'GET';
Expand Down Expand Up @@ -948,15 +962,16 @@ function $HttpProvider() {
throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig);
}

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

var config = extend({
method: 'get',
transformRequest: defaults.transformRequest,
transformResponse: defaults.transformResponse,
paramSerializer: defaults.paramSerializer
paramSerializer: defaults.paramSerializer,
jsonpCallbackParam: defaults.jsonpCallbackParam
}, requestConfig);

config.headers = mergeHeaders(requestConfig);
Expand Down Expand Up @@ -1111,7 +1126,8 @@ function $HttpProvider() {
* @description
* Shortcut method to perform `GET` request.
*
* @param {string} url Relative or absolute URL specifying the destination of the request
* @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested;
* or an object created by a call to `$sce.trustAsResourceUrl(url)`.
* @param {Object=} config Optional configuration object
* @returns {HttpPromise} Future object
*/
Expand All @@ -1123,7 +1139,8 @@ function $HttpProvider() {
* @description
* Shortcut method to perform `DELETE` request.
*
* @param {string} url Relative or absolute URL specifying the destination of the request
* @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested;
* or an object created by a call to `$sce.trustAsResourceUrl(url)`.
* @param {Object=} config Optional configuration object
* @returns {HttpPromise} Future object
*/
Expand All @@ -1135,7 +1152,8 @@ function $HttpProvider() {
* @description
* Shortcut method to perform `HEAD` request.
*
* @param {string} url Relative or absolute URL specifying the destination of the request
* @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested;
* or an object created by a call to `$sce.trustAsResourceUrl(url)`.
* @param {Object=} config Optional configuration object
* @returns {HttpPromise} Future object
*/
Expand All @@ -1146,11 +1164,34 @@ function $HttpProvider() {
*
* @description
* Shortcut method to perform `JSONP` request.
* If you would like to customize where and how the callbacks are stored then try overriding
*
* Note that, since JSONP requests are sensitive because the response is given full access to the browser,
* the url must be declared, via {@link $sce} as a trusted resource URL.
* You can trust a URL by adding it to the whitelist via
* {@link $sceDelegateProvider#resourceUrlWhitelist `$sceDelegateProvider.resourceUrlWhitelist`} or
* by explicitly trusting 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 a query parameter in the request. You must specify the name of this parameter by
* setting the `jsonpCallbackParam` property on the request config object.
*
* ```
* $http.jsonp('some/trusted/url', {jsonpCallbackParam: 'callback'})
* ```
*
* You can also specify a default callback parameter name in `$http.defaults.jsonpCallbackParam`.
* Initially 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.
*
* @param {string} url Relative or absolute URL specifying the destination of the request.
* The name of the callback should be the string `JSON_CALLBACK`.
* @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested;
* or an object created by a call to `$sce.trustAsResourceUrl(url)`.
* @param {Object=} config Optional configuration object
* @returns {HttpPromise} Future object
*/
Expand Down Expand Up @@ -1249,12 +1290,28 @@ function $HttpProvider() {
cache,
cachedResp,
reqHeaders = config.headers,
url = buildUrl(config.url, config.paramSerializer(config.params));
isJsonp = lowercase(config.method) === 'jsonp',
url = config.url;

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if I understand correctly, config.url must be a string for all non-JSONP requests (i.e. it can't be an $sce-wrapped value - not that it needs to). And for JSONP requests it can be either an $sce-trusted resourceUrl or matched by a rule in the resourceUrlWhiteList (but not in the blackList). Right?

It might be more staight-forward if we allowed wrapped values for other methods too. E.g.:

if (... === 'jsonp') {
  ...
} else {
  url = $sce.valueOf(url);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1: accepting safe types where they make sense but aren't technically needed is a good thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly there is no $sce.valueOf method but I will implement this suggestion.

Copy link
Contributor

@rjamet rjamet Sep 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

} else if (!isString(url)) {
// If it is not a string then the URL must be a $sce trusted object
url = $sce.valueOf(url);
}

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

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

$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 Expand Up @@ -1386,5 +1443,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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to throw here? Couldn't we just remove the superfluous param?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The aim of throwing is to a) make it more obvious where something has been missed in a migration and b) to indicate that a malicious attacker is trying to inject their own callback.

But perhaps this is a bit harsh?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should go with the harsh BC of throwing and always rollback to just ignoring in a patch release if we get lots of complaints.

}

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

return url;
}
}];
}
Loading