Open Bug 1257989 Opened 8 years ago Updated 2 months ago

WebSocket origin should have deterministic and validated origin header

Categories

(WebExtensions :: General, defect, P5)

46 Branch
defect

Tracking

(Not tracked)

UNCONFIRMED

People

(Reporter: jamie, Unassigned)

References

(Blocks 1 open bug)

Details

(Whiteboard: triaged [sp3])

User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/601.5.17 (KHTML, like Gecko) Version/9.1 Safari/601.5.17

Steps to reproduce:

Packaged 1Password extension code as a WebExtension per https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Packaging_and_installation


Actual results:

Received unknown WebSocket origin moz-extension://e9d08159-f94a-5646-b22f-24082b5f7a75

Asked in IRC #webextensions and evilpie said it is a device-specific random UUID.



Expected results:

The add-on should have a deterministic origin. 1Password uses this for validating the origin of the extension as a valid 1Password extension to guard against rogue extensions attempting to connect to the user's 1Password data. This would also apply to non-local uses of WebSocket that we might use in the future including but not limited to communicating with 1Password for Teams/Families.

In Chrome, the origin includes the extension identifier, which is tied to the code signing certificate, so if an add-on tries to spoof the identity, it will not be validated by Chrome.
This certainly seems like a worthwhile thing to fix, however a rogue xpcom extension could still open a tcp socket and spoof the origin header.  So even if we fix this for websockets opened from webextensions, I don't think 1Password can necessarily trust origin headers coming from firefox...
Priority: -- → P2
Whiteboard: triaged
In looking at the fetch specs[1], it looks like anything with Sec- prefix as well as the non-legacy Origin header should be prevented from being set except by the user agent.  Seems to me this should also apply to WebSocket connections as well. I'm not sure if there's an argument to be made for an xpcom extension to be considered a separate user agent from Firefox… Thoughts?

[1] https://fetch.spec.whatwg.org/#forbidden-header-name
Flags: needinfo?(aswan)
(In reply to Jamie Phelps from comment #2)
> In looking at the fetch specs[1], it looks like anything with Sec- prefix as
> well as the non-legacy Origin header should be prevented from being set
> except by the user agent.  Seems to me this should also apply to WebSocket
> connections as well. I'm not sure if there's an argument to be made for an
> xpcom extension to be considered a separate user agent from Firefox…
> Thoughts?
> 
> [1] https://fetch.spec.whatwg.org/#forbidden-header-name

Right, anything that goes through Firefox HTTP handling (including WebSocket via DOM APIs or via XPCOM) should be prevented from overriding the Origin header.  But a determined attacker could create a raw TCP socket via XPCOM and implement (enough of) the websocket protocol to communicate with your native application with arbitrary headers.
This is of course not a technique we would encourage but if you want to be thorough in considering things an attacker might try to do, be aware of it.
Flags: needinfo?(aswan)
(In reply to Jamie Phelps from comment #0)
> Received unknown WebSocket origin
> moz-extension://e9d08159-f94a-5646-b22f-24082b5f7a75
> 
> Asked in IRC #webextensions and evilpie said it is a device-specific random
> UUID.

WebExtensions are signed and we know therefore that the ID is unique[*]. Couldn't we make the origin moz-extension://<id> instead? Is there some privacy reason for using a per-device UUID? This was also an issue for app:// origins in Firefox OS and I believe they made deterministic-IDs an option in the manifest. Paul, do you remember the issues there?


[*] for most people. If you've got a dev build with signing disabled then an add-on could fake any ID it wanted.
Flags: needinfo?(ptheriault)
Flags: needinfo?(amckay)
(In reply to Daniel Veditz [:dveditz] from comment #4)
> (In reply to Jamie Phelps from comment #0)
> > Received unknown WebSocket origin
> > moz-extension://e9d08159-f94a-5646-b22f-24082b5f7a75
> > 
> > Asked in IRC #webextensions and evilpie said it is a device-specific random
> > UUID.
> 
> WebExtensions are signed and we know therefore that the ID is unique[*].
> Couldn't we make the origin moz-extension://<id> instead? Is there some
> privacy reason for using a per-device UUID? This was also an issue for
> app:// origins in Firefox OS and I believe they made deterministic-IDs an
> option in the manifest. Paul, do you remember the issues there?

Yes, we allowed apps to define their own origin in the manifest, like app://whatever.developer.wants. In theory you were support to use a domain you owned[1], but I think this was only enforced through the review process which is obviously not ideal. 

So long as we can ensure that we never sign two extensions with the same effective origin (whether that be based off id or whatever), that sounds ok to me.


[1] https://developer.mozilla.org/en-US/docs/Archive/Firefox_OS/Firefox_OS_apps/Building_apps_for_Firefox_OS/Manifest#origin
Flags: needinfo?(ptheriault)
Depends on: 1271663
Flags: needinfo?(amckay)
(In reply to Jamie Phelps from comment #0)
> The add-on should have a deterministic origin. 1Password uses this for
> validating the origin of the extension as a valid 1Password extension to
> guard against rogue extensions attempting to connect to the user's 1Password
> data. 

In my opinion this is in conflict with interoperability and openness. I don't see why other extensions should not be allowed to interact with a native service assuming their use is non-malicious, which should be ensured by review and not through vendor-lockin.
Component: WebExtensions: Untriaged → WebExtensions: General
See Also: → 1307852
Hi Jamie,

Is this related to the one that Rob landed in Fx55, bug 1307852?  We were tracking that as a blocker for 1password, but also found this bug.. :) 

I'm hoping bug 1307852 solves the issue and this can be closed as a duplicate.
Flags: needinfo?(jamie)
This can be closed from our perspective. It's not properly a duplicate since the bug 1307852 is regarding Native Messaging and this is about WebSockets. I still think that predictability of the WebSocket Origin header could be useful, but it isn't a blocker for us since our WebExtension will no longer use WebSocket.
Flags: needinfo?(jamie)
Thanks Jamie!
Hi,

We are in process of porting XUL extension of Enpass password manager to WebExtensions. It is using web-sockets, and this issue of sending a random origin ID is still a blocker for us. Until the origin is not deterministic, we can't verify the source of request at the Enpass app <link here> to grant access to Enpass's data.
Enpass extension: https://addons.mozilla.org/en-US/firefox/addon/enpass-password-manager/
Enpass App: https://www.enpass.io/downloads/

In chrome, it is not an issue as their origin consist of chrome extension unique ID i.e chrome-extension://<addon-id>. Please look into this.
(In reply to Vinod Kumar from comment #11)
> we can't verify the source of request at the Enpass app <link here> to grant
> access to Enpass's data.

You should not "verify" an implementation (this reeks of DRM or vendor-lockin), you should *authenticate* the user based on credentials that are securely stored. Security is not achieved by having an implementation self-report its identity.

Personally I would very much prefer of origin IDs were spoofable to enable interoperable implementations.
(In reply to The 8472 from comment #12)
> Personally I would very much prefer of origin IDs were spoofable to enable
> interoperable implementations.

The app web-socket server is running on localhost port only and not visible to outside world. Also, It can verify that connecting client process is valid a Firefox binary or not. It just need to make sure that the connection is initiated by Firefox on behalf of Enpass extension.
(In reply to Vinod Kumar from comment #13)
> The app web-socket server is running on localhost port only and not visible
> to outside world. Also, It can verify that connecting client process is
> valid a Firefox binary or not. It just need to make sure that the connection
> is initiated by Firefox on behalf of Enpass extension.

Then have the user enter some pairing ID to let the app communicate with the extension? I.e., authenticate the user's intent, not code. Otherwise you're implementing an anti-competitive solution that would prevent others from writing a drop-in replacement that interoperates with your app.
(In reply to Vinod Kumar from comment #13)
> The app web-socket server is running on localhost port only and not visible
> to outside world. Also, It can verify that connecting client process is
> valid a Firefox binary or not. It just need to make sure that the connection
> is initiated by Firefox on behalf of Enpass extension.

This is no longer a valid assumption for Firefox (and Chrome for that matter) even if this bug were fixed.
moved back to untriaged with no priority to re-evaluate based on comment 11
Priority: P2 → --
Whiteboard: triaged
Priority: -- → P5
Whiteboard: triaged
Product: Toolkit → WebExtensions

The "Origin" header with the deterministic UUID is matter for the server. It is like a signature, telling the server how trustworthy the client is.

I am talking about the server is owned by the extension developers, not the 3rd-party server, the browser should provides a mechanism to help the the extension developers to protect their server.

Currently in Chrome:

  • if you install the extension from the Chrome Store, the UUID is deterministic, then the server will know the client is unmodified, is trust-able.
  • If you install the extension with the Developer Mode, the UUID is random generated, then the server will know the client maybe modified, is not trust-able.

For example, the client has a restriction that request frequency is 1/min, a cracker and change this restriction to 1/sec in the extension code, and run the extension in the Developer Mode. But the server know the the client modified by the Origin header, so it can reject this client. Now the cracker need to crack the browser as well, he need to modify the browser source and compile it.

Cracking the extension JavaScript code more easier then compile the browser source, it only need to unzip the extension archive file, beautify the JavaScript code, find the restriction configuration keyword and change it. And the browser Store also doesn't allow obfuscate the code, so the extension code is more easier to read and debugged.

The deterministic UUID won't protect the server completely from a cracker, but at least it can prevent the modified client from being distributed to general users by the cracker, because general users also require a cracked browser binary file.

Please consider fix this bug, or make this behavior opt-in for the users.

(In reply to muzuiget from comment #18)

the browser should provides a mechanism to help the the extension developers to protect their server.

The browser is a User Agent, it acts on behalf of the user, not for the server developer. If the user decides to spoof a request to 3rd party software (e.g. by installing an alternative frontend for it) then they should be able to.

Blocks: 1372288
See Also: → 1405971
Severity: normal → S3

I'd like to +1 the suggestion in comment 4 to expose the WebExtension ID in the Origin header. The current design makes it essentially impossible to use CORS to restrict access from unprivileged WebExtensions.

As I understand it, extensions are meant to be unprivileged by default unless they define permissions in their manifest. Thus, an extension with no permissions can be thought of as similar to a website on a random unprivileged origin.

CORS is meant to allow servers to define which origins they trust to share content with. That is why the requesting origin is included in the Origin header, so that the backend can determine if the requesting origin should be allowed to access the resource. The issue here is that the Origin header sent from web extensions is a random ID that does not allow this check to be done. So servers either have to:

  1. Allow requests from all extensions (which is akin to doing Access-Control-Allow-Origin: *)
  2. Allow requests from no extensions (which is artificially limiting)

And I agree with comment 19 about the goal of a browser being a user agent. This is what host permissions enable: They make it possible for a user to purposefully grant an extension access to an origin without CORS restrictions. I agree that this is an important feature that should be preserved, but I think what we're discussing here is extensions that are meant to be unprivileged and thus fit into the wider web threat model.

(In reply to David Dworken from comment #20)

And I agree with comment 19 about the goal of a browser being a user agent. This is what host permissions enable: They make it possible for a user to purposefully grant an extension access to an origin without CORS restrictions. I agree that this is an important feature that should be preserved, but I think what we're discussing here is extensions that are meant to be unprivileged and thus fit into the wider web threat model.

Extensions are not really "unprivileged" with regards to the normal web security model. When granted access to all URLs they can completely bypass cors, they can access privileged headers. And they may eventually regain TCP/UDP socket access (legacy extensions used to have those).
Security restrictions on extensions are about protecting the user, not about protecting.

And extensions can have native components that can do about anything, including issuing arbitrary HTTP requests.

but I think what we're discussing here is extensions that are meant to be unprivileged and thus fit into the wider web threat model.

The issue is about the server trying to "validate" requests coming from specific extensions, i.e. locking services to specific client-side code. That's not the same as preventing unprivileged extensions from accessing sites they don't have permissions for.

Extensions are not really "unprivileged" with regards to the normal web security model. When granted access to all URLs they can completely bypass cors, they can access privileged headers. And they may eventually regain TCP/UDP socket access (legacy extensions used to have those).

I agree that when extensions have been granted host permissions, they become highly privileged. But when I say unprivileged I'm trying to refer to the default extension with no special permissions.

And extensions can have native components that can do about anything, including issuing arbitrary HTTP requests.

Though that requires the nativeMessaging permission, so that isn't something they can do by default, right?

Security restrictions on extensions are about protecting the user, not about protecting.

I wholeheartedly agree. :) To me, the goal here is to make it possible for servers to protect user data from unprivileged extensions. This is what CORS enables (exposing a resource only to specific origins) and I believe is something that should also be possible with unprivileged extensions.

The issue is about the server trying to "validate" requests coming from specific extensions, i.e. locking services to specific client-side code. That's not the same as preventing unprivileged extensions from accessing sites they don't have permissions for.

I think the second piece is what I'd say the goal should be. Making it possible for a server to:

  1. Block unprivileged extensions from accessing sensitive resources
  2. Allow specific trusted extensions to access those resources

To me, this is akin to a server having a CORS policy that blocks all origins except *.example.com.

And in this scenario the user is still in full control: They can always grant an extension host permissions.

(In reply to David Dworken from comment #22)

Then host permissions already cover that? If the extension has an <all_urls> permission then it can access everything. If it only has access to https://somedomain.org then it can't access https://importantservice.com. This is ensured on the client side. The server doesn't need to check anything.

(In reply to The 8472 from comment #23)

Then host permissions already cover that? If the extension has an <all_urls> permission then it can access everything. If it only has access to https://somedomain.org then it can't access https://importantservice.com. This is ensured on the client side. The server doesn't need to check anything.

I don't believe this is true. If an extension only has host permissions to https://somedomain.org, then it can still access other domains. It just can't do so in a way that bypasses standard web platform security features. To quote the docs, host permissions allow:

XMLHttpRequest and fetch access to those origins without cross-origin restrictions (even for requests made from content scripts)

So even without the permission, requests are allowed, they just have standard cross-origin restrictions applied to them.

(In reply to David Dworken from comment #24)

(In reply to The 8472 from comment #23)

Then host permissions already cover that? If the extension has an <all_urls> permission then it can access everything. If it only has access to https://somedomain.org then it can't access https://importantservice.com. This is ensured on the client side. The server doesn't need to check anything.

I don't believe this is true. If an extension only has host permissions to https://somedomain.org, then it can still access other domains. It just can't do so in a way that bypasses standard web platform security features. To quote the docs, host permissions allow:

XMLHttpRequest and fetch access to those origins without cross-origin restrictions (even for requests made from content scripts)

So even without the permission, requests are allowed, they just have standard cross-origin restrictions applied to them.

Right, but those restrictions are considered sufficient under the web model. If the extension wants to talk to importantservice.com and actually see the response that can be accomplished by adding the host permission for importantservice.com.

(In reply to The 8472 from comment #25)

Right, but those restrictions are considered sufficient under the web model. If the extension wants to talk to importantservice.com and actually see the response that can be accomplished by adding the host permission for importantservice.com.

They're sufficient if the goal is to protect the user. But I don't think they're sufficient if the goal is to enable unprivileged extensions to be first class citizens of the web platform. As you said, right now the only way to allow an extension to talk to importantservice.com is via a host permission. This gives the extension full access to importantservice.com.

But suppose that this extension only needs to be able to call importantservice.com/api/endpoint, an endpoint that is exposed via CORS to an enumerated set of origins.

  • With the current model, the extension author must request the host permission which gives them a far wider set of permissions than they need. And this will make users less willing to install the extension since it is requesting such a privileged permission.
  • With the model proposed in comment #4, the extension can be added to the CORS allowlist for just that endpoint. Thus the extension doesn't need to request a powerful scary host permission in order to be able to function.

At its core: the question is whether extensions should behave similarly to the web (and be able to use CORS in a meaningful way) or should they retain the current limited behavior.

(In reply to David Dworken from comment #26)

As you said, right now the only way to allow an extension to talk to importantservice.com is via a host permission. This gives the extension full access to importantservice.com.
But suppose that this extension only needs to be able to call importantservice.com/api/endpoint, an endpoint that is exposed via CORS to an enumerated set of origins.

Host permissions can be scoped to paths, they don't have to cover the full domain. And the permission is not really "scary", it primarily enables interacting with exactly the specified site, which the extension is intentionally going to do anyway. There shouldn't be an issue with making it explicit that the extension uses that API.

If the API is under the extension developer's control they can also choose to provide a separate host or endpoint just for extension access, to make it clear that it's ok to use that. Like importantservice.com/browserextensionaccess/.

At its core: the question is whether extensions should behave similarly to the web (and be able to use CORS in a meaningful way) or should they retain the current limited behavior.

The issue is that it would make extension requests identifiable. It could be abused both for vendor lock-in and to lock out extensions acting on behalf of the user. I'm sure there are some people who'd love to be able to block extensions just on the principle that it's not coming from "their authorized webpage".

Imo the browser should only send a null origin for CORS requests from an extension context if it doesn't have host permission, the host's origin if it does have host permission or the website's origin when doing it from a contentscript context

Whiteboard: triaged → triaged [sp3]
You need to log in before you can comment on or make changes to this bug.