-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Description
Disclaimer
First of all, I understand that providing APIs like Deno.permissions.grant({...})
as is would just completely defeat the permissions system: malicious 3rd party code would be able to call it and grant itself any set of permissions. I’m explaining further down below what is my actual proposal, if feasible.
Current context
Currently, the only way to add permissions in a running program (as opposed to granting them when spawning the process) is through Deno.permissions.request({...})
, which relies on the console to ask the end user for confirmation (granting or rejecting). This has quite a couple of issues/limitations:
- current quirks:
- displayed message is not ideal for some file system paths. For example:
Deno requests read access to "."
does not make it very explicit what path it is (any relative path could suffer from that confusion) - the suggested command line like
Run again with --allow-read to bypass this prompt
for instance is also not ideal, since it’s too general. It should include the actual path (or context like env var, network address, etc.) (though I understand achieving proper quoting and/or escaping may be hard since any shell or API could be used to spawn the process)
- displayed message is not ideal for some file system paths. For example:
- current limitations
- this system will work only if Deno is running through an interactive shell, and if the end user has access to it. Even when those conditions are met, it may not be very user friendly.
- there’s not even an event or an API to be able to trace what were the dynamically granted (or rejected) permissions, so that the developer can have a full report on which permissions to update. I think that’s what issue Ability to log all runtime granted permissions, e.g. when running with
-A
#20228 is requesting
From the listed limitations, you may have already guessed that I’m not assessing Deno for a classic SaaS use case, but rather for a desktop application. And that desktop application would be highly dynamic regarding what permissions are needed, especially for the file system, where it would be able to aggregate a lot of content from very different places.
With that context set, I can detail the limitations:
- such desktop application would indeed have Deno as a local backend, but would only display the GUI to the end user: one does not expect to see a weird console spawning next to it, and sometimes showing a bunch of permission requests that can easily be missed. Especially since the target clients would not necessarily be developers
- the application would allow adding external content from anywhere on the file system, as well as configuring root folders for scanning some content, etc. etc. If the end user has already done some steps in the GUI to explicitly add those paths as sources of content, it would feel surprising to have an additional permission request step to handle
- on next start of the application, previously allowed permissions would have to be restored based on the user configuration, with the application making a link between his business logic and the resulting required permissions
Despite all that, I still see the permissions system as a strong, very valuable point of Deno, especially when it is running on the end user computer.
How could we handle that?
Given the high level of flexibility those use cases require, I personally don’t see any solution that would not involve adding APIs for full control over permissions.
But as said in the disclaimer, that would defeat the purpose of this whole security system.
That’s why I am suggesting an idea that I hope makes sense to you, but most of all I hope that it is technically feasible (it would not be easy though): be able to flag a portion of code as authorized to access the full permissions API, including granting new permissions or hooking permission prompts.
What would be a “portion of code”? That is a tricky part indeed. It could be conceptually seen as either:
- all application code
- excludes 3rd party dependencies, which also means dynamically loaded ones
- excludes end user evaluated code (which should be sandboxed anyways)
- etc.
- a single source file from that application code
- seems safer than authorizing the whole application code (the less the better here)
- would not work properly if application code is bundled, since filename would change, but more importantly it would include more than the authorized file, and potentially 3rd parties. The developer would have to make sure that the authorized file remains outside of the bundle.
- a single function (or a set of functions)
In all cases, that raises two questions:
- how to identify the origin of the call and link it to that authorization system: stack trace? internal JavaScript engine tracing? etc. Plus it would have to make sure to be compatible which cached bytecode. In all cases it would also have to be a safe way not subject to spoofing
- how to make sure, on both the Deno side and the developer side, that the authorization is not “leaking” (e.g. that an authorized function is not called by 3rd party code)
While the former would be purely a technical challenge linked to the way JavaScript engines work, the latter is rather a matter of good practices regarding security when coding.
If I take again the example of the desktop application:
- let’s say a single function is authorized to access the full permissions API
- my GUI, running for instance in Electron, would make a call to the local Deno backend
- that call would reach an HTTP endpoint handler, which would in turn call business logic
- at some point, some part of the business logic would need to add permissions and therefore call the authorized function, which is imported through an ES import
- if ever a situation occurs where the permission is not anticipated, but Deno faces the need for “prompting” the end user, my backend would have to catch this prompt and send an event to the GUI which would display a custom piece of UI to ask the end user to grant or reject that permission. The result would also have to travel all the way back to the authorized function to answer the prompt on the Deno side.
I see in this scenario a lot more exposed entities:
- the authorized function being exported, and then imported in different ES modules
- permission requests traveling between the Deno backend and the web frontend, through a network layer
- the end user prompt being exposed to client side code breaches (malicious code automating the answering of the request by always granting)
Security and flexibility
Now, the topic of security is always tricky… It’s a balance between theory and practice, convenience and safety, etc.
I guess the existing system with CLI prompts can also be hacked to always grant permissions.
I also think that if one cannot reasonably use permissions (like in the case of the desktop application described here), they would simply bypass it by “allowing all”, which is not good either.
Is a more flexible security system with potentially more attack vectors better than a system where the security is completely disabled? In my opinion I would say “yes, as long as the more robust, less flexible system is still available and that there are options to switch between the two models”.
Of course, in an ideal world a more flexible, capable system without a higher vulnerability would be an epic win.