-
Notifications
You must be signed in to change notification settings - Fork 27.4k
feat($httpBackend): JSONP requests now require trusted resource #15161
Conversation
There was a problem hiding this 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); |
There was a problem hiding this comment.
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 var
s 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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure thing
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 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 An example API would look like:
Without the |
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. |
That sounds reasonable, and arguably like a nicer API than JSONP_CALLBACK.
But wouldn't this cause quite some migration pain?
|
If we setup the default callback param key to be
or
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... |
The problem comes in the case where the callback key is not
In this case, we would not automatically delete the We could parse the params ones that have a value of There might be some rare case where |
There was a problem hiding this 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); |
There was a problem hiding this comment.
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);
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is: https://github.com/angular/angular.js/blob/master/src/ng/sce.js#L389.
(it's just weirdly undocumented ?)
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This description is misleading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed
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 ! |
I've added the callback parameter checking feature in 308f3bf. |
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Some change suggestions to the commit message: feat($http): specify the JSONP callback via the The query parameter that will be used to transmit the JSONP callback to the
This is to prevent malicious attack via the response from an app inadvertently BREAKING CHANGE You can no longer use the Before this change: // Default callback param After this change: // Default callback param is taken from the provider config, no need to add it |
paramSerializer: '$httpParamSerializer' | ||
paramSerializer: '$httpParamSerializer', | ||
|
||
callbackParam: 'callback' |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 022fc44
The Travis failure is real, and is caused by the jsonp e2e tests in the docs |
@Narretz regarding the changes to the commit message. I agree with adding the code comments to the examples. In the earlier description though:
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. |
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'); |
There was a problem hiding this comment.
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')
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSON --.> JSONP
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
acces --> access
There was a problem hiding this comment.
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)`}. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
trusted --> trusting
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as as --> as
There was a problem hiding this comment.
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`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(callback
--> 'callback'
)
There was a problem hiding this comment.
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`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(callback
--> 'callback'
)
There was a problem hiding this comment.
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 + '=)'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the parenthesis?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
25d72c4
to
a69729d
Compare
There was a problem hiding this 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` |
There was a problem hiding this comment.
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 😁
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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`. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about "application-wide"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default is OK.
There was a problem hiding this 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'}); ```
cb2db20
to
3c1cd3b
Compare
… 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'}); ```
…ed types Follows angular/angular.js#15161 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=152795936
…ed types Follows angular/angular.js#15161 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=152795936
This PR replaces #15143 which is too complicated.
Please check if the PR fulfills these requirements