All-purpose web tiles library
README.md

@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 bundle writes it to dist/containment.tile with atile; atile publish tile publishes it to the AT Protocol.
  • probe.js exports createProbeRouter(), the Express router the tile's network probes aim at (/ping answers JSON, /pixel.gif an image, /style.css, /script.js, /module.js, /font.woff2, /echo.html a page that posts back to its frame's parent, /events a stream), and attachProbeSocket(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.js exports runCheck(), 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.