Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions services/platform/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,20 @@ describe('security headers', () => {
"connect-src 'self' https://acc.r2.cloudflarestorage.com",
);
});

test('content-hashed assets bypass the CSP/nonce middleware', async () => {
const app = createApp(baseEnv);
// A content-hashed asset path skips secureHeaders (no per-request CSP +
// crypto nonce), so its response carries no CSP — while a normal path still
// does (control). Independent of whether the file exists on disk.
const asset = await app.fetch(
new Request('http://localhost/assets/vendor-katex-DUoGyCxW.js'),
);
expect(asset.headers.get('content-security-policy')).toBeNull();

const normal = await app.fetch(new Request('http://localhost/api/health'));
expect(normal.headers.get('content-security-policy')).not.toBeNull();
});
});

describe('POST /canvas-preview', () => {
Expand Down
18 changes: 15 additions & 3 deletions services/platform/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,12 @@ export function createApp(
if (c.req.path === '/canvas-preview') return next();
if (c.req.path.startsWith('/dav/') || c.req.path === '/dav')
return secureForDav(c, next);
// Content-hashed JS/CSS chunks are static and never use the nonce'd CSP, so
// skip the per-request CSP + crypto-nonce build for them — a cold load's
// ~150-asset burst would otherwise pay it on the single event loop. The
// static file handler re-adds the one header that matters for a script
// response, `X-Content-Type-Options: nosniff`.
if (IMMUTABLE_ASSET.test(c.req.path)) return next();
return currentSecure()(c, next);
});

Expand Down Expand Up @@ -913,10 +919,16 @@ export function createApp(
if (filePath.startsWith(distDir)) {
const file = Bun.file(filePath);
if (await file.exists()) {
// Bun infers Content-Type from the file extension; we only add the
// caching directive (immutable for content-hashed chunks).
// Bun infers Content-Type from the file extension; we add the caching
// directive plus `nosniff`. Content-hashed assets skip the security-
// headers middleware above, so we re-assert the one header that
// matters for a script/style byte stream here (harmless duplicate for
// the other static files, which also get it from that middleware).
return new Response(file, {
headers: { 'Cache-Control': cacheControlForStaticPath(pathname) },
headers: {
'Cache-Control': cacheControlForStaticPath(pathname),
'X-Content-Type-Options': 'nosniff',
},
});
}
}
Expand Down
Loading