Open martinthomson opened 2 years ago
Is the proposal to add a new mode for Sec-Fetch-Mode
, which would be added to all browser requests within the FedCM API? That seems reasonable to me.
Hmm yeah looking at https://fetch.spec.whatwg.org/#concept-request-mode it seems like adding a fedcm method might be best. @annevk do you have any thoughts on that?
I'm not sure about this. The fetch mode concept is currently used to figure out major modes of the processing model for fetch, in terms of what results in network errors, what kind of security properties are required, how credentials are processed, whether early hints are respected, whether responses are opaque, etc.
I can't find anywhere in the spec that actually invokes fetch in a rigorous way, e.g. says what request input is used to the fetch algorithm (this seems very bad!!). But I am hoping that the fetch that this spec does is just a normal "cors"-mode fetch. So inventing a new mode doesn't seem right, in that case.
Hmm thanks for the context. @martinthomson based on the above I suspect we should keep the new header? WDYT?
I can't find anywhere in the spec that actually invokes fetch in a rigorous way,
This was one of Anne's complaints as well. I think that it started to get defined in sections like check the root manifest, but isn't quite finished.
This spec doesn't actually do a normal "cors"-mode fetch- it has two types of request that are more specific. One is credential-less and referrer-less and another is credentialed.
One way forward that makes sense to me is making two new Sec-Fetch-Mode values, "fedcm-credentialless" and "fedcm-credentialed", which each specify the correct behavior within the constraints of Fetch. This also removes the need for the Sec-FedCM-CSRF header.
I hope neither of those proposed fetches bypass CORS, and I hope even more ardently that neither of them rise to the level of behavioral differences that would require an entirely new fetch mode!!
Instead, it would make most sense if they were normal, "cors"-mode fetches, which set the referrer policy and credentials mode fields of the request appropriately.
Instead, it would make most sense if they were normal, "cors"-mode fetches, which set the referrer policy and credentials mode fields of the request appropriately.
Okay, that would work. Then we do still need the Sec-FedCM-CSRF header.
Looking at this more closely, I don't think that the header is needed.
A fetch with the "cors" mode ensures that the content of a resource is not readable to a random site that invokes fetch()
unless the (IDP) site deliberately enables access. To ensure that only the browser reads the response, the site needs to do exactly nothing. The site can use fetch headers to perform checks, and some amount of defense-in-depth is sensible here[^1], but the extra check is redundant.
For requests that mutate state (logout, token acquisition), we can insist upon a preflight by explicitly setting the use-CORS-preflight flag. But that doesn't get us what is needed. As far as I can tell, this is where things get a little sketchy because sites can set a referrer policy that prevents the inclusion of an Origin[^2]. So a missing Origin is not a strong signal that the request comes from the UA.
An alternative is to use the CSRF token defense used by most sites: include a value in an earlier request that proves that the request is genuine.
In all cases, this only requires that the endpoint that the browser interacts have a URL with sufficient high entropy as to be sufficient hard to guess by a random site that invokes fetch[^3]. I might even claim that this provides better defense than a passive signal like Sec-FedCM-CSRF because it doesn't require an additional check, though I guess that you could build an endpoint that doesn't pay any attention to a high-entropy URL component.
[^1]: You probably want to add X-Content-Type-Options: nosniff
to the response. Then check Origin
and Sec-Fetch-Dest
and so forth.
[^2]: I can't see why we would allow script to make a cross-site request without Origin
, so I might be missing something: the fetch spec is, as always, inscrutable.
[^3]: The simplest way to implement something like this is to have a per-user URL, with the URL encoding the identity of the user, a nonce, and an authentication tag; see also https://www.w3.org/TR/capability-urls/. Add a key identifier or version number and you can use the same symmetric key for many users.
How would you provide an endpoint with a high-entropy URL, given that sites can just read it using a server-side proxying script?
@domenic since all of these fetches are browser-mediated and also we don't expose info to the website unless the user gives permission (by selecting an account), we do bypass CORS
Also, in practice, even if we did use CORS, I'm sure IDPs will have to give access to everyone anyway since there's lots of IDPs and I doubt they want to run their CORS checks against the list every time.
Bypassing CORS is VERY BAD and MUST NOT BE DONE for new features.
I don't want this to seem facetious, but we bypass CORS for DoH. That said, I agree with @domenic in this case: this can use CORS.
Well, that is a very strong statement. We can probably support CORS, but why do you say that not using it is so bad in this case?
Also, should we require CORS even for the uncredentialed requests?
To be honest, requiring CORS would make security worse here IMO because then the accounts endpoint becomes world-readable from websites instead of only to the request mediated by the UA.
requiring CORS [...] the accounts endpoint becomes world-readable
With CORS, the resource decides what is and isn't readable. By default (except under some narrow conditions), content is not readable by any site. The browser can see it, but it won't pass that information on unless the site explicitly permits it.
We don't want the IDP to know about the RP when fetching the accounts so at least CORS cannot be used there. In addition, since the "*" wildcard cannot be specified when responding to a credentialed requests request, the token request cannot use CORS, right? Otherwise the IDP must specify ALL the RP origins.
More importantly, there are 3 special things about FedCM API:
Therefore I think the current "Sec-" header without CORS solution makes more sense in this case.
I'd encourage you to involve the security teams at your browsers if you are thinking of introducing another hole in the same origin model by bypassing CORS. We've previously gotten consensus across the web community that such violations are not acceptable, and I'm disappointed to see people trying to bypass it yet again. I don't really have the energy to continue litigating these issues, so I am just going to hope that such features get blocked in per-browser security review and cross-browser TAG review, in accordance with previous resolutions.
We don't want the IDP to know about the RP when fetching the accounts so at least CORS cannot be used there. In addition, since the "*" wildcard cannot be specified when responding to a credentialed requests request, the token request cannot use CORS, right? Otherwise the IDP must specify ALL the RP origins.
CORS preflight requests are sent with Referer-Policy "same-origin" which seems acceptable.
The Origin
header will only be sent if the response tainting gets set to "cors", which I think we can manage to avoid for our non-credentialed requests.
Also, Access-Control-Allow-Origin
may be taken from the Origin
request header.
I think it is entirely possible for this request to be CORS-protected, and it makes sense to use it since the result does end up in the RP's content process.
Scrolling back, I think I agree with Martin that we don't even need the Sec-FedCM-CSRF header. We already have a high-entropy component in the request to fetch a token (nonce), so all of that discussion of high-entropy URL components is moot. What is the property we are defending by making sure the endpoints are only hit by the browser API? I can't come up with a good answer, but I admit I haven't gone to the scrollback of the original inclusion.
CORS preflight requests are sent with Referer-Policy "same-origin" which seems acceptable. The
Origin
header will only be sent if the response tainting gets set to "cors", which I think we can manage to avoid for our non-credentialed requests.
Looking more closely, I don't think this is actually true. It looks like our non-credentialed requests would hit this fallthrough case. Unless there is a mechanism for suppressing the Origin
header in CORS that I am missing, this is bad news.
-Mode
indeed seems wrongish, but what about -Dest
? This does seem like it might warrant a new destination type.
It looks like our non-credentialed requests would hit this fallthrough case. Unless there is a mechanism for suppressing the
Origin
header in CORS that I am missing, this is bad news.
Continuing the conversation with myself. I think this is fine if we perform the uncredentialed fetches with a de-priveleged client: one whose origin is opaque. This would set the Origin header to null
for those requests and be fetch-spec compatible. We have the idea of a Null security principal in Gecko that is useful for de-privileging.
-Mode
indeed seems wrongish, but what about-Dest
? This does seem like it might warrant a new destination type.
I like -Dest
the most of the proposed options (-Dest
, -Mode
, and Sec-FedCM-CSRF
). This also adds the ability for a developer to only allow "credential" requests and not arbitrary fetches using their CSP.
Drive-by:
I think this is fine if we perform the uncredentialed fetches with a de-priveleged client: one whose origin is opaque. This would set the Origin header to null for those requests and be fetch-spec compatible.
Out of curiosity, what would be the need for nulling out the Origin
for the uncredentialed request? IIUC, this is the request with which we're specifically OK with exposing the RP to the IDP (via the client id), right? So is there a harm in keeping the origin too? Does that expose any more information?
I agree that there should not be a problem with the uncredentialed requests here, and I think they can use regular fetch. The complicated one is the credentialed one, which is one where we might need fetch expert help. I don't think doing a regular CORS fetch (with Origin
being the RP) would work. As Christian pointed out, that would give the RPs the power to do those fetches outside of FedCM and read the accounts information.
Drive-by:
I think this is fine if we perform the uncredentialed fetches with a de-priveleged client: one whose origin is opaque. This would set the Origin header to null for those requests and be fetch-spec compatible.
Out of curiosity, what would be the need for nulling out the
Origin
for the uncredentialed request? IIUC, this is the request with which we're specifically OK with exposing the RP to the IDP (via the client id), right? So is there a harm in keeping the origin too? Does that expose any more information?
I was specifically talking about the requests for the manifests, which should not expose the RP to the IDP.
I agree that there should not be a problem with the uncredentialed requests here, and I think they can use regular fetch. The complicated one is the credentialed one, which is one where we might need fetch expert help. I don't think doing a regular CORS fetch (with
Origin
being the RP) would work. As Christian pointed out, that would give the RPs the power to do those fetches outside of FedCM and read the accounts information.
@npm1: That makes a lot of sense. Thanks for the clarification. If we add a new Sec-fetch-dest value of "credential", the IDP can validate that header in the request with an identical guarantee that the client is not initiating the request except through this API. And it has the added benefit of integrating into the Content Security Policy so the RP can make restrictive policy decisions as well.
The same-origin policy does not care about whether a request is credentialed. (CORS does, but that's almost entirely about the level of opt-in we require.) I'm not sure why you all are saying that is significant? Note in particular that IP-address-authenticated servers are still a thing, to my knowledge.
The point was raised in https://github.com/fedidcg/FedCM/issues/320#issuecomment-1220163833 and I think it's central to this discussion. As to my knowledge, CORS in order to work requires the server to have knowledge of the request Origin
. A central privacy guarantee in FedCM for at least one of the credentialed requests is that it happens without any association to the originating site (i.e. with a no-referrer
policy).
Not necessarily, if the origin is an opaque origin, you could respond with `null
`, even with credentialed fetches. Normally you wouldn't really do that, but perhaps for a dedicated endpoint it's okay.
With regard to @domenic 's request to talk to the browser's security team, I want to be clear that we did do that and they were explicitly OK with not using CORS.
I think my comment that using CORS would make the accounts endpoint/token endpoint was misunderstood, I want to elaborate:
Not necessarily, if the origin is an opaque origin, you could respond with
null
, even with credentialed fetches. Normally you wouldn't really do that, but perhaps for a dedicated endpoint it's okay.
What's the difference between that and no-cors, i.e. the tangible security gain for the IDP?
@cbiesinger I suspect that "security OK" does rely on a particular implementation whereby responses from the IDP are kept out of the RP process. (Similar to the DoH comment from @martinthomson above. Depending on the characteristics there might be cases where that is okay, though it seems safer to avoid altogether.)
@johannhof it means you cannot attack a non-IDP JSON resource. Either through a request you cannot normally make due to the same-origin policy or being able to read the response through a covert channel.
it means you cannot attack a non-IDP JSON resource. Either through a request you cannot normally make due to the same-origin policy or being able to read the response through a covert channel.
@annevk Sorry, but can you elaborate on this? I don't follow.
So I think that this entire CORS issue is bound up in confusion, partly because we have a bunch of very different requests that might be involved.
My understanding is that the accounts endpoint is something that the browser itself hits. The Origin value in that request would be null
(or probably absent) in order to maintain our privacy goal (that is, the IdP does not learn about potential RPs, only RPs that the user chose). The response to that request is consumed by the browser, so there is no cross-site interaction involved. This request is also likely safe, so there is no inherent need for preflight. (You might even consider the origin of this request as the IdP itself if that helps.)
The most interesting one is the fetch that a browser makes when a login is initiated. This is a POST request that results in the IdP generating a token that can be used by the RP. For this, the IdP probably needs to know the identity of the RP, so Origin can be populated (or the body of the POST can contain the RP identity). But I don't see any reason why the RP needs to consume the response. The browser can consume the response and then provide the necessary information to the RP.
This means - in both cases - the IdP doesn't need to concern itself with the various Access-Control-Allow-...
headers on the response. Those are for when the browser passes information to another site. In this case, the IdP can send responses without Access-Control-Allow-Foo
and be safe in the knowledge that the response will only be usable by the browser.
As for the exceptions, which seem to be largely a preflight exemption, I don't see any reason to exempt FedCM. There is always a performance angle that argues for preflight exemption, but that really doesn't hold much water here. Pay the round trip.
Regarding the accounts endpoint request, my understanding matches @martinthomson's. Honestly, that endpoint seems almost like a .well-known
endpoint. Maybe it's different since it is supposed to be hit with credentials (do normal .well-known
requests have credentials included? FWIW one in this spec is currently using no-cors, but maybe that's bad), but the response:
That makes CORS feel like an odd property for the request since in order to use credentials it would have to know who is requesting it. Or, yeah I guess it could magically know to always return null
if the requesting Origin is null
, as Anne mentioned. That almost feels like a misuse of CORS, but maybe not!
do normal .well-known requests have credentials included?
A .well-known
endpoint isn't special, at least as far as CORS is concerned. So the question would have to be answered based on who is asking, where, etc...
I've been innocently believing that fetching FedCM related resources could happen as a first-party, because it's a separate process than the RP's. However, reading through this thread made me believe that treating those requests as third-party / cross-origin makes sense.
I tried to summarize my understanding here.
FedCM sends five different requests:
GET
Accept: application/json
GET
Accept: application/json
GET
Accept: application/json
GET
Accept: application/json
POST
Accept: application/x-www-form-urlencoded
Some premises:
Accept
is other than application/x-www-form-urlencoded
, multipart/form-data
or text/plain
, the request must be preflight. This means most of FedCM endpoints must support CORS preflight.Origin
that reveals the requester, but this can be null
in some condition?Having said these:
Origin
as null
For 3. Client metadata endpoint
Origin
as the RP's origin.For 4. Accounts list endpoint
Origin
as null
Access-Control-Allow-Origin
response header with the RP's origin from the IdP, but there is no way for the IdP to learn the origin because it's intentionally hidden.For 5. Assertion endpoint
Origin
as the RP's origin.I don't think this summary answers questions in this thread but hopefully organizes them better.
See https://fetch.spec.whatwg.org/#cors-protocol-exceptions, https://educatedguesswork.org/posts/web-security-model-cors/, and https://educatedguesswork.org/posts/web-security-model-side-channels/.
Sorry @annevk, can you be more specific as to how the IDP benefits from using CORS when the only possible response is null
and without any knowledge on who triggered the request? I agree with @domfarolino that this feels like misuse.
I have a proposal to help move this forward. Here is the rationale and proposal:
These are requests which are initiated by the browser process in Chrome’s implementation, and the responses never reach the RP's renderer process, apart from the final token, once the user has gone through the UI flow.
The main line of defense of these user-agent-initiated requests is the forbidden header. Whether it be Sec-FedCM-CSRF or some new value for Sec-Fetch-Dest. This is needed because the IDP wants to allow RPs to use FedCM, but not allow them to access the user account data, i.e. not allow an RP-initiated fetch to succeed. From the thread, it sounds like it would be better to use Sec-Fetch-Dest: web-identity
instead of our own FedCM-specific header.
I chatted with @domfarolino and he said it would be a good idea to also enforce a response header in addition to the user agent sending a Sec- header in the request. This ensures the IDP is aware of this request being used for FedCM, and acknowledges they've checked for the header. We could use Support-Loading-Mode: web-identity
or some other header (ideas welcome).
For the manifest list, manifest, and account list fetches, we do not want to include a referrer as that would leak the RP to the IDP, which is undesirable. The only option would be to use CORS with Origin
set to null. It was not clear from this thread what the benefits would be of using null, but if the benefits become clear then we can use CORS with null.
Client metadata and ID token fetches could use CORS. But this also does not add extra protection on top of what the IDP can achieve by relying on the fact that the fetches include the forbidden header and the response acknowledgement. Thus, we propose not adding CORS to these. The responses are not shared with the RP at all, except for the ID token, and that one requires the forbidden header, the header in the response, and user going through the UI flow. This means CORS does not add any additional protection.
I plan to send a PR for these changes soon, hopefully by next week. Let me know if you have any questions or thoughts!
For the request in the original title of this issue, I have filed issue #353, since this issue has morphed into general discussions around CORS which is largely orthogonal.
OK, we have had extensive discussions on what the best way to proceed is, and here is our proposal.
First, let me summarize the fetches that are done when navigator.credentials.get() is called by the RP:
Second, some context for the proposed solution below:
Access-Control-Allow-Origin: null
or *
. Websites can easily send requests even with a null Origin using a data: URL. (Servers could defend against this by checking Sec-FedCM-XSRF/Sec-Fetch-Dest but since this is a really bad failure case, we’d prefer not to rely on that)Access-Control-Allow-Origin: null
on the manifest as an “opt-in” to FedCM. But this is a pretty unspecific way to opt-in. And we already have a much more specific way to opt-in, in the form of the well-known file on the eTLD+1 (e.g. https://google.com/.well-known/web-identity) which needs to list the specific allowed configURL.Given the above, we think CORS adds no value over the Sec-FedCM-XSRF
(soon to be changed to Sec-Fetch-Dest: webidentity
) header. Concretely, our proposal is:
Support-Image-Loading
as the well-known file is sufficient opt-inSec-FedCM-XSRF
to Sec-Fetch-Dest: webidentity
to enable IDPs to know when a request comes from the user agent via FedCM.This is a pretty comprehensive take, but only from the perspective of the client. CORS doesn't exist primarily to protect the client; it exists so that servers can be protected from clients. Clients are allowed to be unhappy about CORS requirements when they are not the beneficiaries of the protections that it provides, but that doesn't change the fact that CORS exists to protect servers that are sometimes unaware of the mechanisms by which they are being protected.
Can you perhaps provide an analysis that supports your conclusion from this perspective?
This is a pretty comprehensive take, but only from the perspective of the client. CORS doesn't exist primarily to protect the client; it exists so that servers can be protected from clients. Clients are allowed to be unhappy about CORS requirements when they are not the beneficiaries of the protections that it provides, but that doesn't change the fact that CORS exists to protect servers that are sometimes unaware of the mechanisms by which they are being protected.
Can you perhaps provide an analysis that supports your conclusion from this perspective?
From the perspective of protecting servers, the fetches we initiate from FedCM would not trigger preflights if we added CORS. We'd have to actually force preflights to happen, but the fact they do not happen by default means that there are existing mechanisms that can perform similar fetches without preflights. Thus, forcing preflights on FedCM would not introduce any benefits to such servers but it would introduce a lot of issues for clients as mentioned above.
From a server protection perspective:
CORS would protect against a) "weird" requests that could break servers, and b) unexpected POST requests that could break servers. In principle it would also protect against requests from unexpected origins, but not here, since we would send a null origin.
However, we already have an opt-in requirement for FedCM -- the well-known file (top-level domain manifest). If it doesn't exist, or if it doesn't list the provider manifest URL, we only make two requests (we make them in parallel in Chrome): 1) A GET request to the tld manifest 2) A GET request to the provider manifest
These are standard GET requests with nothing unusual about them and so they should not affect servers in anyway.
If the well-known file does exist and list the provider manifest, then the server opted in to FedCM and it is safe to make the other requests; CORS would not offer additional protections.
As npm points out, FedCM would not trigger preflight in any case since none of our requests are special enough.
I agree that for the manifests, CORS with Origin: null
is probably better as no-CORS.
However I retain reservations about not having CORS on credentialed requests (and see no reason not to use it on Origin-ful requests).
[...] as long as third-party cookies still exist [...], this would make the endpoints readable to the entire web since it would require the server to send Access-Control-Allow-Origin: null or *.
Credentialed requests with Access-Control-Allow-Origin: *
must not have their results shared, per https://fetch.spec.whatwg.org/#cors-protocol-and-credentials. So third parties wouldn't be able to read the results of the requests.
Websites can easily send requests even with a null Origin using a data: URL.
Sure, but those requests would fail a CORS Check. Which means that either the request will have mode "cors" and have a network error or "no-cors" and the response will be an opaque filtered response. Tbf, I haven't thought about the "websocket" mode.
Credentialed requests with
Access-Control-Allow-Origin: *
must not have their results shared, per https://fetch.spec.whatwg.org/#cors-protocol-and-credentials. So third parties wouldn't be able to read the results of the requests.
Well this is an IDP we are talking about so they will have many RPs. They can reply with ACAO: RP
, sure, but that does not remove the concern of RPs reading the data outside of the FedCM API.
Sure, but those requests would fail a CORS Check. Which means that either the request will have mode "cors" and have a network error or "no-cors" and the response will be an opaque filtered response. Tbf, I haven't thought about the "websocket" mode.
They wouldn't fail the CORS check. Null is when the header is not sent, which is different from the string "null". The check will succeed if the Origin value is the string "null" and the origin of the fetch is also the string "null".
The provider manifest is provided by the get() call
Later on in the discussion it's suggested the provider manifest is at a .well-known
location. Which is it?
Making .well-known
some kind of same-origin policy extension is an interesting idea, but one that I think would warrant a (much) wider discussion than just this issue/group. In that if you decide to host a resource at a .well-known
location you opt-in to a different security model. For server administrators that also seems a bit fraught though. Who knows how well the .well-known
URL space is guarded.
The provider manifest is provided by the get() call
Later on in the discussion it's suggested the provider manifest is at a
.well-known
location. Which is it?
Both. get()
provides the config URL. We fetch the file in the .well-known
location and check that the config URL specified in that file matches what the get()
call provided. If it does not, then we reject the get()
call without performing any additional fetching (i.e. we only end up fetching the file from .well-known
and the config URL).
They wouldn't fail the CORS check. Null is when the header is not sent, which is different from the string "null". The check will succeed if the Origin value is the string "null" and the origin of the fetch is also the string "null".
Right, 🤦♂️, thank you. Good catch. I didn't realize Fetch treats opaque origins identically in this way. Seems not great, but that is why ACAO: null
is warned against I suppose.
Well this is an IDP we are talking about so they will have many RPs. They can reply with
ACAO: RP
, sure, but that does not remove the concern of RPs reading the data outside of the FedCM API.
Fair enough. It just makes more sense to me that if a service has its RPs set up to use CORS properly it would manage to validate Sec-Fetch-Dest as well. But you are right that it is a foot-gun.
I'll defer to colleagues with more direct involvement in the Fetch spec as to whether this is an appropriate use case for "no-cors".
Sec-Fetch-Mode seems purpose-built for this sort of thing. Adding another header field doesn't really help a lot.
(A server will naturally ignore either a new Sec-Fetch-Mode value or the Sec-FedCM-CSRF thing. The value of the former is that it will compress better and it reuses an existing mechanism.