Isolation: why a game cannot touch your computer
Where a game's script runs, what it can and cannot do, the barriers of the portal, the launcher and the editor, the test with a malicious package and the risk that remains.
DescribesFormat 1, API 1 For everyone
On this page
In Green Games anyone can make games, and a game ships a script: code written by someone else that runs on your computer. That is why the design starts from one rule: a game's script never runs in the page or in the application that shows it. It runs locked in, and it can only ask for things through a short list of functions.
What a game is, seen from security#
A game is a package of data (the game card and the scenes, in JSON) and a script. It is not a web page and not an installable program. The only code with access to the browser or to your computer is the game player, which belongs to Green Games and is the same for every game.
The script runs inside the room: the simulation of the match. The game player sends it what you do with the controls and receives, frame by frame, what moved and what to show. Nothing else crosses from one side to the other.
What a game can and cannot do#
| A game can | A game cannot |
|---|---|
| Create, move and remove pieces of its own world | Read or write files on your computer |
| Move the players of the match and read their controls (a direction and three buttons) | Use the network: there is no fetch, and no connections of any kind |
| Show texts, stopwatches, menus and scoreboards through the game player | Touch the page or the window: there is no DOM |
| Play synthesized sounds | Read cookies or the storage of the site or the application |
| Save its own progress (up to 64 KB per game, data only; an open-world game, in addition, up to 320 KB of changes to its terrain) and its scores | Read another game's progress |
| Use the match's clock and randomness | Use the browser's timers, create threads or run code built as text (eval) |
| Call the internal functions of the launcher or the editor | |
| Read the whole keyboard, the position of the mouse arrow or what you type |
What the game shows also goes through a filter: the game player writes texts as text (never as HTML), limits quantities and sizes, and validates again what it saves.
The barriers of the engine#
There are four, from the outside in. They are the ones that protect a match in the portal.
- The validator. Before loading, it rejects the package if the script names network, page, storage, timers, threads or
eval. It is not a security barrier, because it can be fooled by building the names in parts: it is there so that an honest author finds out sooner. The ones that follow are. - An isolated frame. The game player creates an
iframewithsandbox, whose origin is opaque: it shares no cookies and no storage with the site. That frame carries a content policy that starts withdefault-src 'none': the browser blocks every network connection, andevaltoo. - A Web Worker inside that frame. A worker is a thread with no page: the DOM does not exist there. It inherits the frame's policy. The game player talks to it over a direct message channel.
- A trimmed environment. Before running the script, the room deletes from the environment
fetch,XMLHttpRequest,WebSocket,importScripts,indexedDB,caches,Worker,postMessage,setTimeout,WebAssembly,navigatorand dozens more.Math.randomstarts coming from the match's seed andconsolewrites to the game's log.
After that, what is left in the script's environment is standard JavaScript and the greengames object with the API services.
Against abuse of time#
A script can also do harm without leaving its enclosure: by burning processor time. The room measures the milliseconds of script in each frame and stops the match if the limits are exceeded (4 ms on average, 100 ms in one frame). If the script enters an endless loop, the room stops sending frames and the game player closes it after 3 seconds. The numbers: Limits.
The barriers of the launcher#
In the launcher more is at stake than in a web page: the application can read and write on your disk. A game must never be able to call those functions. There are five barriers.
- The script runs in a Web Worker. It is the engine's room. The channels the interface uses to talk to the native part of the application do not exist there, nor does the DOM; and the engine deletes
fetch, storage, timers and the rest before running it. - The game player lives in a separate frame, with a different origin. It is loaded from a local server of the application itself (
http://127.0.0.1, on a random port and with a secret path), inside aniframewithsandbox. Its origin is opaque and different from that of the launcher's interface: it does not see its content, its storage or its session token, and it cannot open windows or take the main window to another address. - A content policy with no network. The game player's page is served with a policy that forbids everything by default: no connections, no child frames, scripts only from that secret folder of the local server, nothing written inline, no
eval. No network destination is allowed. The worker inherits that policy. - A seal. The first script of the game player's page closes, in that frame, the native channels of the WebView. The game player does not need them: it talks to the interface only through messages, and the interface validates each message.
- A session token. Every native function of the launcher demands a 256-bit secret that the application hands over only once, to its main page. The game's frame never receives it; any call without it is rejected before anything is done.
In addition, before installing a .ggx that you import, the launcher validates it twice (with the engine's validator and again in its native part, without running anything) and, every time it opens an installed game, it checks that the file has not changed (by its SHA-256 hash).
The barriers of the editor#
In the editor, Play uses the same game player as the engine: it puts the room in an isolated frame with an opaque origin, with a content policy with no network, and inside it in a Web Worker with the trimmed environment. They are barriers 2, 3 and 4 of the engine. The script you write, or the one the rules generate, does not run in the editor's page.
The editor's validator warns you as you type if the script names something that does not exist in the room:
fetch as an error: games have no network. With errors, the editor does not let you export.The test with a malicious package#
The launcher's barriers are checked with a real attack, which runs against the real application on Linux, on Windows and on macOS.
First part: a malicious game. It is a package that passes the validator, because it builds the forbidden names in parts. Its script tries 45 things:
- reaching the objects the interface uses to talk to the native part, and the window that contains it;
- using the network through
fetch,XMLHttpRequest,WebSocket,importScriptsand through indirect paths; - running code built as text (
evalandFunction); - using storage and files;
- creating threads and timers;
- abusing the
progresoservice with a key shaped like a file path and a 300 KB value.
Second part: an attacking page. It assumes something worse: that the attacker already managed to run code in the game player's page, not only in the worker. Served just like the game player, that page tries to reach the application's objects, the parent window and the session token; it makes 27 raw calls over the native channel (one for each function of the launcher, with no token and with a made-up token); it tries storage; it tries to reach a server of another origin by nine paths (a server that counts what reaches it); it tries eval, child frames, undoing the seal and taking its own frame to another address.
The native part counts the network requests that went out and the rejected calls, and the state of the launcher before and after is compared.
On each system the launcher uses the WebView that system provides, and the test runs on it.
| Measure | Linux | Windows | macOS |
|---|---|---|---|
| Successful attempts | 0 | 0 | 0 |
| Network requests that went out | 0 | 0 | 0 |
| State of the launcher afterward | Unchanged | Unchanged | Unchanged |
| Of the 27 raw calls without the seal, how many reach the native part | 27 | 0 | 0 |
| Of those that arrive, how many are rejected | 27 | None arrive | None arrive |
These are the results of launcher 0.4.0. On macOS the test runs on a computer with Apple Silicon, with the native build and with the Intel build, which the system runs translated there. It was not tested on a computer with an Intel processor.
On all three systems the WebView's native channel is visible from the game's frame: that is why the seal exists. On Windows and on macOS no call made from that frame reaches the native part. On Linux they do arrive, and the session token rejects them: it is the risk explained below.
The risk that remains#
There is one left, only on Linux, and it is worth stating in full.
In the Linux WebView, the native channel is visible from any frame, and the framework the launcher is built with checks only the origin of the main page. That is why, in the test without the seal, the 27 calls reach the native part. The launcher's functions reject all of them, because of the session token.
But there are internal functions of that framework that carry no token: moving or closing the window, the clipboard, opening an address in the browser. To reach them, someone would have to achieve two things at once:
- run their own code in the game player's page, not in the worker. That would take a flaw in the engine when it draws the data that comes from the room;
- and also undo the seal.
The test found no way to do it. Closing it completely requires that framework to check, on Linux, the origin of the frame that sends the message, as it already does on Windows and on macOS.
Two more limits, so as not to promise too much:
greengames probardoes not isolate. In the command-line tool, the script runs in an empty Node context. It reproduces the environment of the room, but it is not a security barrier: do not test games from strangers there.- The binaries are not signed. Isolation protects you from games, not from an altered copy of the launcher. Download only from the official site and check the checksum: Download and install.
What it means if you import someone else's .ggx#
- You can open it. The game runs with all the barriers above: it does not read your files, does not use the network and does not see the rest of the launcher.
- If the file is damaged or invalid, the launcher rejects it and tells you why, before saving anything.
- The worst a game can do inside its enclosure is be a bad game: burn processor time until the game player cuts it off, show annoying texts or fill its own save space (in the launcher, up to 384 KB per game: 64 KB of progress and 320 KB of changes to the cube terrain).
- What a game saves is only its own: its progress and its scores, on your computer.
- No telemetry. The launcher sends no usage data, and without a configured online catalog it makes no network request at all.
How to import: Import a .ggx.
Next#
- The .ggx format: what is inside the file you import.
- Scripts: the same enclosure, seen by whoever writes the game.
- Determinism: why the room uses its own randomness and its own clock.