Something went wrong. Try again.
All-purpose web tiles library
Something went wrong. Try again.
7.3 kB · 159 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
import { equal, ok, deepStrictEqual, match } from 'node:assert';import express from 'express';import request from 'supertest';import { runtimeCSP, deadCSP } from '@dasl/tile-lexicon';import { createTileLoadingRouter } from '../dist/index.js';
const BASE = 'tiles.example';
function app (options) { const a = express(); a.set('trust proxy', 'loopback'); a.use(createTileLoadingRouter(BASE, options)); // What a site mounting the router serves on its own host. a.get('/', (req, res) => res.send('the site')); return a;}
// Turn a CSP header string into { directive: [values...] }.function parseCSP (csp) { const map = {}; csp.split(';').map(s => s.trim()).filter(Boolean).forEach(d => { const [name, ...vals] = d.split(/\s+/); map[name] = vals; }); return map;}
// On a tile origin: a non-"load." subdomain, so we hit the static handler// rather than the redirect.async function getAsset (path = '/.well-known/web-tiles/worker.js', host = `abc.${BASE}`) { return request(app()).get(path).set('Host', host);}
describe('tile-server – origin isolation', () => { it('redirects load.<host> to a fresh random subdomain (303) preserving the path', async () => { const res = await request(app()).get('/.well-known/web-tiles/').set('Host', `load.${BASE}`); equal(res.status, 303); const loc = new URL(res.headers.location); match(loc.hostname, new RegExp(`^[a-z]{20}\\.${BASE.replace('.', '\\.')}$`), 'redirects to a 20-letter random subdomain of the base host'); equal(loc.pathname, '/.well-known/web-tiles/', 'path is preserved'); equal(loc.port, '', 'no port when the request had none'); });
it('keeps the port in the redirect (a desktop app on *.localhost:<port>)', async () => { const res = await request(app()).get('/.well-known/web-tiles/').set('Host', `load.${BASE}:4321`); equal(res.status, 303); const loc = new URL(res.headers.location); equal(loc.port, '4321'); match(loc.hostname, new RegExp(`^[a-z]{20}\\.${BASE.replace('.', '\\.')}$`)); });
it('hands out a different subdomain each time (per-tile origin isolation)', async () => { const a = app(); const one = await request(a).get('/x').set('Host', `load.${BASE}`); const two = await request(a).get('/x').set('Host', `load.${BASE}`); const h1 = new URL(one.headers.location).hostname; const h2 = new URL(two.headers.location).hostname; ok(h1 !== h2, 'each load gets its own origin'); });
it('serves the loader runtime on tile origins', async () => { const worker = await getAsset('/.well-known/web-tiles/worker.js'); equal(worker.status, 200); const shuttle = await getAsset('/.well-known/web-tiles/shuttle.js'); equal(shuttle.status, 200); });
it('never serves the runtime on the base host (no worker at / on a site origin)', async () => { // The runtime registers a service worker at scope `/`; on the site's own // origin that would let any page framing it hijack the site for that user. const res = await getAsset('/.well-known/web-tiles/worker.js', BASE); equal(res.status, 404); ok(!('service-worker-allowed' in res.headers)); const site = await getAsset('/', BASE); equal(site.status, 200); equal(site.text, 'the site', 'the base host is left to the site'); });
it('answers every other path on a tile origin with a dead end', async () => { for (const path of ['/', '/index.html', '/anything/at/all', '/.well-known/web-tiles/missing.js']) { const res = await getAsset(path); equal(res.status, 404, path); equal(res.headers['content-security-policy'], deadCSP, path); equal(res.headers['x-content-type-options'], 'nosniff'); equal(res.headers['referrer-policy'], 'no-referrer'); ok(!('service-worker-allowed' in res.headers), `${path}: cannot register a worker`); match(res.headers['content-type'], /^text\/plain/); } });
it('lets a deployment name its own tile origins', async () => { const a = app({ isTileOrigin: (h) => /^t-[a-f0-9]+\.load\.tiles\.example$/.test(h) }); equal((await request(a).get('/.well-known/web-tiles/worker.js').set('Host', 't-abc123.load.tiles.example')).status, 200); equal((await request(a).get('/.well-known/web-tiles/worker.js').set('Host', 'abc.tiles.example')).status, 404); });});
describe('tile-server – isolation & anti-exfiltration headers', () => { it('sets the cross-origin isolation and hardening headers', async () => { const res = await getAsset(); equal(res.headers['service-worker-allowed'], '/'); equal(res.headers['origin-agent-cluster'], '?1'); equal(res.headers['cross-origin-resource-policy'], 'cross-origin'); equal(res.headers['cross-origin-opener-policy'], 'same-origin'); equal(res.headers['x-content-type-options'], 'nosniff'); equal(res.headers['referrer-policy'], 'no-referrer'); equal(res.headers['x-dns-prefetch-control'], 'off'); match(res.headers['permissions-policy'], /interest-cohort=\(\)/); match(res.headers['permissions-policy'], /browsing-topics=\(\)/); match(res.headers['x-robots-tag'], /noai/); });
it('serves the runtime with the shared runtime policy, verbatim', async () => { equal((await getAsset()).headers['content-security-policy'], runtimeCSP); });
it('CSP leaves the runtime no network at all', async () => { const csp = parseCSP((await getAsset()).headers['content-security-policy']); deepStrictEqual(csp['default-src'], ["'self'"]); deepStrictEqual(csp['connect-src'], ["'none'"]); deepStrictEqual(csp['frame-src'], ["'self'"]); for (const d of Object.keys(csp)) { if (d === 'sandbox') continue; ok(!csp[d].some(v => v === '*' || /^https?:$/.test(v) || v.includes('.')), `${d} names no remote source`); } });
it('CSP locks down objects, base URI, forms, and inline attribute handlers', async () => { const csp = parseCSP((await getAsset()).headers['content-security-policy']); deepStrictEqual(csp['object-src'], ["'none'"]); deepStrictEqual(csp['base-uri'], ["'none'"]); deepStrictEqual(csp['script-src-attr'], ["'none'"]); deepStrictEqual(csp['form-action'], ["'none'"]); deepStrictEqual(csp.webrtc, ["'block'"]); ok(!('form-src' in csp), 'the old form-src typo is gone'); });
it('CSP sandbox grants only the intended capabilities and NOT host-hijacking ones', async () => { const csp = parseCSP((await getAsset()).headers['content-security-policy']); const sandbox = csp['sandbox'] || []; for (const cap of ['allow-scripts', 'allow-same-origin', 'allow-forms', 'allow-modals', 'allow-popups', 'allow-popups-to-escape-sandbox', 'allow-downloads']) { ok(sandbox.includes(cap), `sandbox should allow ${cap}`); } // A tile must not be able to navigate/steer the host or grab powerful capabilities. for (const forbidden of [ 'allow-top-navigation', 'allow-top-navigation-by-user-activation', 'allow-top-navigation-to-custom-protocols', 'allow-pointer-lock', 'allow-presentation', 'allow-orientation-lock', 'allow-storage-access-by-user-activation', ]) { ok(!sandbox.includes(forbidden), `sandbox must NOT allow ${forbidden}`); } });});