jevstrudel.git / website / src / jev / sandboxPolicy.mjs

What the sandbox a listener's song plays in may do (sandbox.mjs), as headers: one source for every place that serves it.

  • pages/jev/sandbox.astro puts the policy in a meta tag, so it holds on any static host.
  • The build writes _headers (astro.config.mjs, jevHeaders), which Cloudflare applies to the deploy's static files: the page gets the policy as a header too, with sandbox (so even opened on its own, the page has an opaque origin) and frame-ancestors (only this site may frame it), which a meta tag cannot carry.
  • The dev server sends the same headers (astro.config.mjs, jevSandboxDev).

The frame has an opaque origin, so everything it loads from the site is a cross-origin request with Origin: null: its scripts (module scripts and AudioWorklets are fetched with CORS) and the sample pack. Those public files are served with Access-Control-Allow-Origin: *, which sends no credentials.

Hosts songs load sounds from: Strudel's CDN (the default banks, prebakeSounds.mjs), GitHub's raw files (samples('github:…'), as the site's songs load tidalcycles/dirt-samples), and the soundfonts'.

23export const SAMPLE_HOSTS = [
24  'https://strudel.b-cdn.net',
25  'https://raw.githubusercontent.com',
26  'https://felixroos.github.io/webaudiofontdata/',
27];
29export const SANDBOX_PAGE = '/jev/sandbox/';

meta: the policy as a meta tag may hold it (no sandbox, frame-ancestors); otherwise the header's.

33export function sandboxCsp({ meta = false } = {}) {
34  const directives = [
35    ...(meta ? [] : ['sandbox allow-scripts']),
36    "default-src 'none'",
37    // the site's own scripts; a song is code, evaluated (unsafe-eval), and
38    // AudioWorklets load from blob: (the meter) and data: (superdough's dough
39    // worklet in dev); their bundled file is 'self' in production
40    "script-src 'self' 'unsafe-eval' blob: data:",
41    `connect-src 'self' ${SAMPLE_HOSTS.join(' ')}`,
42    "style-src 'unsafe-inline'",
43    "img-src 'none'",
44    "media-src 'none'",
45    "font-src 'none'",
46    "object-src 'none'",
47    "frame-src 'none'",
48    "manifest-src 'none'",
49    // AudioWorklet module scripts are governed by worker-src
50    "worker-src 'self' blob: data:",
51    "form-action 'none'",
52    // the page sets <base href="/"> so a song's root-relative sample URL
53    // resolves to the site root, as it does on the main page; 'self' is the
54    // origin the frame was served from (the site), not its opaque runtime
55    // origin, so the tag is allowed but cannot be pointed off-site
56    "base-uri 'self'",
57    ...(meta ? [] : ["frame-ancestors 'self'"]),
58  ];
59  return directives.join('; ');
60}
62export const sandboxHeaders = () => ({
63  'Content-Security-Policy': sandboxCsp(),
64  'Referrer-Policy': 'no-referrer',
65  'X-Content-Type-Options': 'nosniff',
66});

Paths the sandbox loads from the site, served to it with CORS.

69export const CORS_PATHS = ['/_astro/*', '/jev-samples/*'];

The deploy's _headers (Workers static assets).

72export function headersFile() {
73  const block = (path, headers) => [path, ...Object.entries(headers).map(([k, v]) => `  ${k}: ${v}`)].join('\n');
74  return [
75    '# Written by the build (website/src/jev/sandboxPolicy.mjs); do not edit.',
76    block(`${SANDBOX_PAGE}*`, sandboxHeaders()),
77    ...CORS_PATHS.map((path) => block(path, { 'Access-Control-Allow-Origin': '*' })),
78    '',
79  ].join('\n\n');
80}