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>
38 lines
1.8 KiB
TypeScript
38 lines
1.8 KiB
TypeScript
import { test, expect } from './fixtures';
|
|
|
|
// Defense-in-depth response headers on document navigations (T4 in the security
|
|
// audit). The CSP is emitted by SvelteKit's kit.csp config; the clickjacking /
|
|
// referrer / permissions headers by hooks.server.ts. We assert on the raw
|
|
// response of a document navigation, and separately confirm the inline theme
|
|
// script still executes under the CSP (its sha256 is allowlisted) by checking
|
|
// the data-theme attribute it sets — a CSP block would leave it unset.
|
|
|
|
test('document responses carry the security headers', async ({ page }) => {
|
|
const response = await page.goto('/');
|
|
expect(response, 'navigation returned a response').not.toBeNull();
|
|
const headers = response!.headers();
|
|
|
|
// Clickjacking: both the legacy header and the CSP directive.
|
|
expect(headers['x-frame-options']).toBe('DENY');
|
|
expect(headers['content-security-policy']).toContain("frame-ancestors 'none'");
|
|
// Script-injection surface reduction.
|
|
expect(headers['content-security-policy']).toContain("object-src 'none'");
|
|
expect(headers['content-security-policy']).toContain('script-src');
|
|
// The non-CSP hardening headers.
|
|
expect(headers['referrer-policy']).toBe('strict-origin-when-cross-origin');
|
|
expect(headers['x-content-type-options']).toBe('nosniff');
|
|
expect(headers['permissions-policy']).toContain('geolocation=()');
|
|
});
|
|
|
|
test('the inline theme script executes under the CSP (hash is allowlisted)', async ({
|
|
page
|
|
}) => {
|
|
// If the theme script were CSP-blocked, data-theme would never be set.
|
|
// Its presence proves the allowlisted sha256 matches the served script.
|
|
await page.goto('/');
|
|
const theme = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-theme')
|
|
);
|
|
expect(theme === 'light' || theme === 'dark').toBe(true);
|
|
});
|