Document responses carried no CSP, X-Frame-Options, Referrer-Policy, or Permissions-Policy — leaving clickjacking and zero script-injection defense in depth (security audit T4). - svelte.config.js: enable kit.csp hash mode. SvelteKit hashes its own inline hydration scripts; the inline theme <script> in app.html isn't part of %sveltekit.head%, so its sha256 is pinned by hand (csp-config.js) and added to script-src alongside object-src 'none', base-uri 'self', frame-ancestors 'none'. Styles stay unconstrained (Svelte emits dynamic inline style= attrs). - hooks.server.ts: applySecurityHeaders() sets X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, X-Content-Type-Options: nosniff, and a Permissions-Policy locking down unused features, only when the response hasn't already set them. - src/csp-theme-hash.test.ts: drift guard against editing the theme script without updating THEME_SCRIPT_HASH. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
26 lines
1.3 KiB
TypeScript
26 lines
1.3 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { readFileSync } from 'node:fs';
|
|
import { createHash } from 'node:crypto';
|
|
import { join } from 'node:path';
|
|
import { THEME_SCRIPT_HASH } from '../csp-config.js';
|
|
|
|
// The theme-flash-prevention <script> in app.html runs inline, so the CSP
|
|
// script-src must allow it by hash. SvelteKit's kit.csp hash mode does NOT
|
|
// cover app.html template scripts (only what it injects into %sveltekit.head%),
|
|
// so we pin the hash by hand in svelte.config.js. This test recomputes the hash
|
|
// from the actual app.html bytes and fails if THEME_SCRIPT_HASH drifted — i.e.
|
|
// someone edited the theme script without updating the CSP, which would silently
|
|
// CSP-block it (theme flash returns, only a console error to show for it).
|
|
describe('CSP theme-script hash', () => {
|
|
it('matches the current inline theme script in app.html', () => {
|
|
// vitest runs with cwd at the frontend package root.
|
|
const html = readFileSync(join(process.cwd(), 'src/app.html'), 'utf-8');
|
|
const match = html.match(/<script>(.*?)<\/script>/s);
|
|
expect(match, 'app.html must contain an inline <script>').not.toBeNull();
|
|
const digest = createHash('sha256')
|
|
.update(match![1], 'utf-8')
|
|
.digest('base64');
|
|
expect(THEME_SCRIPT_HASH).toBe(`'sha256-${digest}'`);
|
|
});
|
|
});
|