diff --git a/astro.config.mjs b/astro.config.mjs index d68b538..ddfdf86 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -10,22 +10,27 @@ export default defineConfig({ // CSRF: handled by our own SameSite=Lax session cookie. Astro's checkOrigin // breaks multipart uploads behind a reverse proxy, because the proxy speaks // http to the app while the browser spoke https and the origins no longer match. - security: { checkOrigin: false }, + // // Content-Security-Policy with hashes instead of 'unsafe-inline': Astro computes // the hash of every inline script and style it emits, at build time. The remaining // directives are set by the middleware (src/middleware.ts). - // Graduated out of `experimental` in Astro 7. - csp: { - directives: [ - "default-src 'self'", - "img-src 'self' data: blob:", - "font-src 'self'", - "connect-src 'self'", - "object-src 'none'", - "base-uri 'self'", - "form-action 'self'", - "frame-ancestors 'none'", - ], + // Astro 7 graduated this out of `experimental` and it lives under `security` now. + // Careful: a top-level `csp` key is swallowed without a warning and the site then + // runs with no policy at all. + security: { + checkOrigin: false, + csp: { + directives: [ + "default-src 'self'", + "img-src 'self' data: blob:", + "font-src 'self'", + "connect-src 'self'", + "object-src 'none'", + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", + ], + }, }, server: { host: true, port: Number(process.env.PORT) || 4321 }, vite: { diff --git a/tests/config.test.ts b/tests/config.test.ts new file mode 100644 index 0000000..6bb7a91 --- /dev/null +++ b/tests/config.test.ts @@ -0,0 +1,27 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import config from '../astro.config.mjs'; + +/** + * Astro's config schema ignores keys it does not know — silently, with no + * warning. That is how the Content-Security-Policy went missing once: Astro 7 + * moved `csp` from `experimental` to `security`, a top-level `csp` key kept + * type-checking and building, and the deployed site served no policy at all. + * Nothing in the build output said so. This test does. + */ +test('The CSP lives where Astro 7 reads it', () => { + const security = (config as any).security; + assert.ok(security, 'astro.config.mjs has no `security` block'); + assert.ok(security.csp, 'the CSP must sit at `security.csp` — a top-level `csp` is ignored'); + assert.ok(Array.isArray(security.csp.directives), '`security.csp.directives` must be a list'); + + // The directives the middleware does not add itself. + for (const d of ["default-src 'self'", "object-src 'none'", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'"]) { + assert.ok(security.csp.directives.includes(d), `missing directive: ${d}`); + } +}); + +test('No stray top-level `csp` or `experimental.csp` key', () => { + assert.equal((config as any).csp, undefined, 'a top-level `csp` key is ignored by Astro 7'); + assert.equal((config as any).experimental?.csp, undefined, '`experimental.csp` is gone in Astro 7'); +});