So I’ve just seen the 107: Control with your Voice thread and will have to give it a listen later. Voice control is very relevant to my interests as an automation language designer. However there was one comment under it regarding security that deserves a seperate discussion:
“On all platforms (iOS, iPadOS, macOS) the displayed script must be viewed in its entirety prior to the execution control being enabled.”
This statement really infuriates me as pure security theatre; the hacker equivalent of the notorious Quack Miranda, blaming the app’s users for Bad Things happening instead of holding the app responsible for knowingly allowing these bad things to happen in the first place.
Now, Omni’s far from the first or only ones to pull this excuse, so I’m not picking on them in particular. One of the motives for AppleScript’s famously readable (and infamously unwriteable) syntax was so that anyone could read a script to to see what it did before running it, and it worked just as well then as now (i.e. not at all). Or to quote Dr Cook from his HOPL3 AppleScript retrospective:
Finally, readability was no substitute for an effect[ive] security mechanism. Most people just run scripts — they don’t read or write them.
A security hole is a security hole. Blaming users for falling down it is extremely poor form, and I was honestly surprised to see Omni doing it. Omni have done a great job of supporting automation over the last 20 years (a better job than Apple has!), so I don’t think it unreasonable to hold them to those high standards when they do fall short.
However, since Omni have made this mistake (again), this is a good and timely opportunity to discuss the problem and how to correct it.
(If anyone wants to alert Ken Case & co to this thread, please do. I’m sure they can bring some ideas and thoughts of their own to the discussion.)
…
So, background:
There already is a correct way to do app-level security in macOS and iOS: sandboxing and sandbox permissions. And, for standard application bundles, pretty straightforward: the app developer lists the entitlements their app wants/needs to do do its job, and that list gets baked into the app when building it for distribution, and the whole thing cryptographically signed to ensure it can’t be subsequently tampered with without being detected.
When a user launches this app, the OS isolates this new application process in its own dedicated sandbox. That sandbox “physically” process that process accessing all external services—file system, web, other apps, etc—basically anything that has to go via the operating system. The app can still call those operating system APIs if it wants… but the OS just returns an error. To use an old school metaphor: imagine making a phone call, only to find your phone line has been disconnected at the exchange. You can’t call out: it’s impossible. This is security done right: the Principle of Least Privilege.
Of course, a lot of apps do need to access some external services, which is where entitlements come in. The app can tell the OS that it needs to access e.g. your ~/Documents
folder. The OS asks the user if she wishes to allow this, and then either enables that access (and only that access) or permanently blocks it, remembering that choice for future use. Or, if the app wants to access the web, it must request access to a particular domain, e.g. “www.example.com”, and cannot access any other web location (“www.apple.com”, “www.my-phishing-domain.com”, etc are all out). To use the phone metaphor again, the phone company has restricted your ability to make outgoing calls to one or more pre-agreed numbers; you can call those but no-one else.
It’s a great system. In principle. How well it works in practice… it depends. Again, for “pre-baked” apps, it’s great. For scripting and automation it could (and should!) also be great. Unfortunately, right now it’s not.
…
What OmniAutomation (and every other scripting/automation system) should do at this point is ask the OS to create a new sandbox in which the app can run the user’s script, supplying the list of entitlements for that sandbox dynamically (not statically as is done for apps). The OS can then ask the user if she wishes to allow the script to access some/all/none of those services, remembering her preferences for that particular script only. If that script is later modified/replaced, the user will be re-prompted for permissions the next time it is run.
(For scripts which the user is writing herself, this automatic reset may be skipped on the assumption that the user knows what she’s doing and doesn’t want to be pestered incessantly by the OS being “helpful”.)
It’s worth pointing out here that such a system not only protects the user against malicious third-party scripts, it also protects against typos and other potentially harmful mistakes in her own scripts, e.g. a runaway rm -rf SOME PATH
command, where the user forgets to double-quote "SOME PATH"
and consequently deletes a large chunk of her filesystem. (And who amongst us automators has never made an oopsie like that?) If the OS knows to limit the script’s access to a single folder, it’s impossible for that command to delete anything outside it.
This is security that works WITH and FOR the user, which is as it should be. Unfortunately it doesn’t exist right now: there’s no way for an app to set a sandbox’s entitlements dynamically. Because Apple doesn’t see a market demand for it. They’re not wrong in this, but only because scripters and automators don’t realize this security system is something they should need and want, and thus demand. Again, because bad security systems treat scripters and automators like dirt, endlessly obstructing, frustrating, and generally being obnoxious and obtuse. Bad security serves no-one except sloppy/lazy developers evading the blame for dropping their users right in it.
…
OK, so having established that there is a Right Way to do script-level security, which Apple currently does not support, there is a workaround which might be used in the meantime. The workaround is to put a static sandbox around the script, and give this sandbox exactly one entitlement, which is the ability to send messages to the host app via a single XPC service. All access to external services (reading and writing files, accessing Calendars, composing emails, etc, etc) must go through this channel. The user’s script doesn’t interact with this service directly; instead they import libraries which hide the XPC stuff under familiar native scripting APIs.
For example: to read a file, a sandboxed .js
script would import the fs
library and call its fs.readFile()
function as normal. However, where the standard (Node.js) fs
library would call the OS’s fopen()
and fgets()
(now blocked by the sandbox), this sandbox-aware fs
library sends an XPC message asking the host app to read the file for it and send back the file’s data, again over XPC, which the fs.readFile()
function then returns as normal. (We’ll assume here the host app already has whole disk permissions.)
The trick here is for the script to have a standard mechanism to tell the host app precisely what services it requires. I can think of a couple ways this could be done:
-
“magic comments” at the top of the script file, e.g.:
// needs entitlement: read-write, folder: ~/Documents // wants entitlement: read-only, calendar: Work
-
in-code function calls, preferably made when importing each library so the app can gather all the requirements and prompt the user to grant/deny all permissions just once (not one at a time at random intervals while the script runs, which is famously infuriating):
const fs = require('fs').needs('~/Documents', 'rw'); const calendar = require('calendar').wants('Work', 'r');
The only difference to app-level sandboxing (which is handled by the OS) is that this script-level sandboxing is handled by the app. If the script sends an XPC request for an external service for which the user has not granted the script permission, e.g. to send data to “www.my-phishing-domain.com” or delete ~/
, the app refuses to pass on those operations to the OS and sends back an XPC error instead, which the script library can throw as a standard permissions error.
…
Now if Omni or anyone else wants to implement this per-script sandboxing system, it’ll obviously take a bit of work to 1. design a suitable XPC protocol for channelling service requests from sandboxed script to app and back, 2. implement custom fs
, https
, etc libraries to use those protocols, and 3. embed the JavaScriptCore interpreter within a reusable XPC plugin. Plus, most important of all, 4. getting lots of bums on seats, i.e. app developers, scripters, and users all using and loving it.
That said, the framework only needs to be written and documented once: it can then be distributed (free and/or paid) to other app developers who wish to add similar scripting support to their own apps. Do a really good job of designing, building, and promoting it [1], and perhaps Apple might eventually notice it being popular and successful, and copy its design (already field-tested and proven by hundreds of app developers and thousands of scripters) for a future Apple OS Automation framework that puts modern, safe, secure Scripting & Automation back at the forefront of all its platforms instead of being abandoned to moulder quietly at the back as it now is.
HTH
–
[1] Of course, a modern framework for executing scripts safely and securely is not a complete automation solution. There is still the general problem of allowing user scripts to talk to apps other than the one that’s running them, which on macOS historically means Apple events (now on its last legs) and on iOS means tunnelling arbitrary behavior through URL handlers, which is both Evil and Wrong and a whole security nightmare of its own. I suspect Apple quietly turns a blind eye to Omni and other apps abusing URL handlers for arbitrary IPC, if only because there is no official API for doing IPC between unrelated processes so they let it slide. But it only needs one successful malware exploit to make the popular press, and you can bet Apple will stomp that hole permanently overnight. However, solving the general IPC problem is really something that Apple needs to step up and do (e.g. by extending the existing XPC APIs to allow communication between arbitrary applications). And the best way to convince them to do it is by creating demand for it. So it’s a start.