@dasl/containment-tile #
A red-team tile, and the pieces to run it against a tile host.
The tile (tile/) tries every way out of the web-tiles sandbox it knows and
reports, attempt by attempt, whether containment held: network egress over
fetch, XHR, WebSocket, EventSource, beacons, WebRTC, external scripts,
styles, images, fonts, frames, forms and dynamic imports; scripting or
navigating the host; reaching host runtime globals; registering or
unregistering its service worker; and powerful features such as camera,
clipboard, geolocation and fullscreen. It is honest about what the sandbox
permits on purpose (a tile running its own code, modals, storage on its
throwaway origin, postMessage to the host) and reports those as permitted
by policy, not as escapes. Its full report also goes to the host over the
tp-data protocol, so a host can run it and
read the verdict without looking.
Load it on your own tile host to check your deployment: https://webtil.es/check/ does exactly that for any load domain you give it.
The pieces #
tile/is the tile source:manifest.json,index.html,tile.js,style.css.npm run bundlewrites it todist/containment.tilewithatile;atile publish tilepublishes it to the AT Protocol.probe.jsexportscreateProbeRouter(), the Express router the tile's network probes aim at (/pinganswers JSON,/pixel.gifan image,/style.css,/script.js,/module.js,/font.woff2,/echo.htmla page that posts back to its frame's parent,/eventsa stream), andattachProbeSocket(server, mount)for the WebSocket echo at<mount>/ws. Every answer is something no tile resource could be, so the tile can tell "reached the network" from "the service worker answered with tile content". webtil.es mounts them at/probe, which is the tile's default target; a host sends{ probeBase }over tp-data to aim it elsewhere.check.jsexportsrunCheck(), the host side: render the tile through a loading server and resolve with its report. It is browser ESM that imports@dasl/tile-loader.
Talking to the tile #
Over tp-data, the host may send { probeBase, run: true } for a fresh run and
{ deep: true } to include the destructive probes (the tile unregisters its own
service worker to see what the origin serves afterwards; it stops working until
reloaded). Every run answers with { report }:
{
"verdict": "contained",
"origin": "https://xxxxxxxxxxxxxxxxxxxx.webtil.es",
"summary": "21/22 escape attempts contained; 0 breach(es); 1 inconclusive",
"breaches": [], "inconclusive": ["sendBeacon"], "elevated": ["Popups"],
"violations": [{ "directive": "connect-src", "blocked": "https://…" }],
"results": [{ "category": "Network egress", "name": "WebSocket", "status": "contained", "outcome": "contained", "detail": "refused by CSP connect-src" }]
}
The test #
npm test runs the tile end to end in a real Chromium (CHROME_PATH, or
Chrome/Chromium/Brave in /Applications; skipped without one): a loading
server on *.localhost, a host page on 127.0.0.1, the probe endpoints on a
third origin. It asserts that the current runtime contains everything, and, as
a negative control, that the pre-2.2 runtime leaks (WebSocket, WebRTC, frames,
forms, and the unconfined page a tile gets by unregistering its worker), so
that the probes are known to have teeth. CONTAINMENT_REPORT=1 prints the
reports.