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

Conversation

petebacondarwin
Copy link
Contributor

This PR replaces #15143 which is too complicated.

Please check if the PR fulfills these requirements

Copy link
Contributor

@mprobst mprobst left a comment

Choose a reason for hiding this comment

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

LGTM

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

// send request
return sendReq(config, reqData).then(transformResponse, transformResponse);
try {
var promise = sendReq(config, reqData);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I find it a bit more readable to define vars in the block scope that they are used in, even if they are function scoped. That is, move the declaration of var promise up one line?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure thing

@petebacondarwin
Copy link
Contributor Author

petebacondarwin commented Sep 20, 2016

If it is possible to add a parameter with both user generated key and value then it would be possible to completely control the browser when the JSONP response returns, assuming that the server does not do any sanitizing of its callback parameter value.

Given that every JSONP endpoint could define its own way of specifying the callback, i.e. it is not necessarily a param with key callback then it is very hard to generally constrain the param at the client.

It seems to me that in JSONP requests it is more likely that an app developer would inadvertently allow user generated content in the params than to allow user generated content in the URL itself (which is what we are targeting in this PR).

Perhaps we could change the $http JSONP to require that the callback parameter key is specified in the method call. We could then delete any user provided parameters that have this key before we attach our own callback parameter internally? This would remove the need for the clunky JSONP_CALLBACK pattern match and lock out the param attack vector.

An example API would look like:

var promise = $http({
  method: 'JSONP',
  url: $sce.trustAsResourceUrl('https://trusted.com/path'),
  callbackKey: 'callback'
});

Without the callbackKey the call would fail. This param could be specified in the $http.defaults.jsonp.callbackKey='callback' to save it needing to be added to every request.

@petebacondarwin
Copy link
Contributor Author

Of course this puts the onus on the app developer to remember to specify the correct callback param key, since if it doesn't match then the real callback param would still be vulnerable.

@mprobst
Copy link
Contributor

mprobst commented Sep 20, 2016 via email

@petebacondarwin
Copy link
Contributor Author

petebacondarwin commented Sep 20, 2016

If we setup the default callback param key to be callback, which is very much the defacto standard for JSONP, then there would be no breaking change... All the current JSONP calls that look like:

$http.jsonp('some/trusted/url', { callback: 'JSONP_CALLBACK' });

or

$http.jsonp('some/trusted/url?callback=JSONP_CALLBACK');

would simply have that callback deleted before the service adds it back in itself.

We could even log a warning message if we find a param with the callback key name...

@petebacondarwin
Copy link
Contributor Author

The problem comes in the case where the callback key is not callback:

$http.jsonp('some/trusted/url', { cb: 'JSONP_CALLBACK' });

In this case, we would not automatically delete the cb parameter, since by default we would be looking for a callback param.

We could parse the params ones that have a value of JSONP_CALLBACK and log a warning again in that case, that some migration needs to be done.

There might be some rare case where callback is a valid param name for the JSONP call, while the expected callback param key is something else, (e.g. cb). In this case we would delete a valid parameter causing the request to fail in some manner.

Copy link
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

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

Basically LGTM. It's obviously not a full-proof solution, but it is better that what we have now.

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);
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

}).not.toThrow();
});

it('jsonp() should allow trusted url', inject(['$sce', function($sce) {
Copy link
Member

Choose a reason for hiding this comment

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

This description is misleading.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed

@rjamet
Copy link
Contributor

rjamet commented Sep 20, 2016

LGTM too, both for the current and the discussed modified version. It's still JSONP, so it's still going to result in bugs, but at least the framework does what it can that way. Thanks a lot !

@petebacondarwin
Copy link
Contributor Author

I've added the callback parameter checking feature in 308f3bf.
@mprobst, @rjamet, @gkalpak and @IgorMinar - PTAL

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.

@Narretz
Copy link
Contributor

Narretz commented Sep 21, 2016

Some change suggestions to the commit message:

feat($http): specify the JSONP callback via the callbackParam config value

The query parameter that will be used to transmit the JSONP callback to the
server is now specified via the callbackParam config value of the $httpProvider, instead of
using the JSON_CALLBACK placeholder.
instead of adding a query param to the url that has the JSON_CALLBACK placeholder value?

  • 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:

// Default callback param
$http.jsonp('trusted/url?callback=JSON_CALLBACK');
// Custom callback param
$http.jsonp('other/trusted/url', {params:cb:'JSON_CALLBACK'});

After this change:

// Default callback param is taken from the provider config, no need to add it
$http.jsonp('trusted/url');
// Custom callback param must be added via http config option
$http.jsonp('other/trusted/url', {callbackParam:'cb'});
...
// Or set a custom callback param via httpProvider config
$httpProvider.callbackParam = 'cb';

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

callbackParam: 'callback'
Copy link
Contributor

@Narretz Narretz Sep 21, 2016

Choose a reason for hiding this comment

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

I think this should be jsonpCallbackParam to make it explicit, because httpProvider is bundling a bunch of different config options, and the bulk of the jsonp customization is actually part of the $jsonpCallbacks service

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK

Copy link
Contributor Author

@petebacondarwin petebacondarwin Sep 21, 2016

Choose a reason for hiding this comment

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

Done in 022fc44

@Narretz
Copy link
Contributor

Narretz commented Sep 21, 2016

The Travis failure is real, and is caused by the jsonp e2e tests in the docs

@petebacondarwin
Copy link
Contributor Author

petebacondarwin commented Sep 21, 2016

@Narretz I fixed the e2e test that was failing in 9be46a7

@petebacondarwin
Copy link
Contributor Author

@Narretz regarding the changes to the commit message. I agree with adding the code comments to the examples.

In the earlier description though:

The query parameter that will be used to transmit the JSONP callback to the
server is now specified via the callbackParam config value of the $httpProvider, instead of
using the JSON_CALLBACK placeholder.
instead of adding a query param to the url that has the JSON_CALLBACK placeholder value?

I mostly disagree - because it was possible before that the callback was passed in some other form, such as part of the URL (e.g. https://some-domain.org/end/point/JSON_CALLBACK). I know that this probably never happened but it was possible and we supported it. Now we will indeed only support a parameter. If you have to deal with the breaking change then you will already understand what the purpose and use of JSON_CALLBACK was so I don't think that the way I described it was confusing.

expect(function() {
$httpBackend.expect('GET', '/url').respond('');
$http({url: $sce.trustAsResourceUrl('/url')});
}).not.toThrowMinErr('$http','badreq', 'Http request configuration url must be a string. Received: false');
Copy link
Member

Choose a reason for hiding this comment

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

This assertion is too weak (because a negation of a too specific assertion).
Better use the stricter assertion possible. E.g. in order of preference:

  • .not.toThrow()
  • .not.toThrow('$http')
  • .not.toThrow('$http', 'badreq')

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, OK, OK. In fact even better is to remove the not.toThrow altogether!

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

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` JSON requests need to attach a callback query parameter to the URL. The name of this parameter is specified in the configuration
Copy link
Member

Choose a reason for hiding this comment

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

JSON --.> JSONP

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

@@ -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 callback in a JSONP
* request. The value of this parameter will be replaced with the expression generated by the {@link jsonpCallbacks}
Copy link
Member

Choose a reason for hiding this comment

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

Does this link work? Shouldn't it be $jsonpCallbacks or something?

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

@@ -1146,11 +1168,34 @@ function $HttpProvider() {
*
* @description
* Shortcut method to perform `JSONP` request.
*
* Note that, since JSONP requests are sensitive because the response is given full acces to the browser,
Copy link
Member

Choose a reason for hiding this comment

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

acces --> access

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

* 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 trusted the URL via {@link $sce#trustAsResourceUrl `$sce.trustAsResourceUrl(url)`}.
Copy link
Member

Choose a reason for hiding this comment

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

trusted --> trusting

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

* 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
Copy link
Member

Choose a reason for hiding this comment

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

as as --> as

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

* ```
*
* You can also specify a global callback parameter key in `$http.defaults.jsonpCallbackParam`.
* By default this is set to `callback`.
Copy link
Member

Choose a reason for hiding this comment

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

(callback --> 'callback')

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

* ```
*
* You can also specify a global callback parameter key in `$http.defaults.jsonpCallbackParam`.
* By default this is set to `callback`.
Copy link
Member

Choose a reason for hiding this comment

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

(callback --> 'callback')

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

throw $httpMinErr('badjsonp', 'Illegal use of JSON_CALLBACK in url, "{0}"', url);
}

var callbackParamRegex = new RegExp('([&?]' + key + '=)');
Copy link
Member

Choose a reason for hiding this comment

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

Why the parenthesis?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because I was originally using it in a replace. Good spot.

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

Copy link
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

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

LGTM (other than a couple of nitpicks and the occasional >100chars-long lines 😛)

@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`
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Wrapping lines at 100 chars is sooo cool 😁

Copy link
Contributor Author

Choose a reason for hiding this comment

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

K

@@ -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 callback in a JSONP
Copy link
Member

Choose a reason for hiding this comment

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

Maybe "that passes the name of the callback" would be more clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

K

try {
return sendReq(config, reqData).then(transformResponse, transformResponse);
} catch (e) {
return $q.reject(e);
Copy link
Member

Choose a reason for hiding this comment

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

This won't be necessary if #15213 lands. Maybe remove it and wait for #15213 or add a TODO note to remove it later.

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'll wait. I have reviewed #15213 - I expect we can land that today

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually this is needed. We are not trying to catch exceptions from the then handlers, we are catching exceptions thrown synchronously inside sendReq. E.g. url = $sce.getTrustedResourceUrl(url); can throw.

Copy link
Member

Choose a reason for hiding this comment

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

This is inside serverRequest, which is passed as an "onFulfilled" callback to promise: promise.then(serverRequest).
So, even synchronous errors in sendReq() will be converted to rejections.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh OK. I'll remove it after 15213 then

* $http.jsonp('some/trusted/url', {jsonpCallbackParam: 'callback'})
* ```
*
* You can also specify a global callback parameter key in `$http.defaults.jsonpCallbackParam`.
Copy link
Member

Choose a reason for hiding this comment

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

global sounds a little confusing to me. How about default?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

how about "application-wide"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

default is OK.

Copy link
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

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

LGTM

The mock calls to `valueOf(v)` and `getTrustedResourceUrl(v)` were not dealing
with the case where `v` was null.
The $http service will 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 angular#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));
```
… 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.

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'});
```
ellimist pushed a commit to ellimist/angular.js that referenced this pull request Mar 15, 2017
… 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 angular#15161
Closes angular#15143
Closes angular#11352
Closes angular#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'});
```
brad4d pushed a commit to google/closure-compiler that referenced this pull request Apr 11, 2017
Yannic pushed a commit to Yannic/com_google_closure_compiler that referenced this pull request Jul 6, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants