Files
supabase/apps/studio/security-headers.ts
T
Alaister Young 7f8b8dbe06 fix(studio): migrate security headers (CSP etc.) to the TanStack build
The Next build sets X-Frame-Options / X-Content-Type-Options / HSTS /
Content-Security-Policy / Referrer-Policy via next.config.ts `headers()`. The
TanStack build never carried these over — vercel.ts only set cache-control, so
the deployed dashboard shipped with no CSP.

The TanStack deploy serves a static shell (no server to attach headers), so the
headers go in the Vercel config. Add security-headers.ts as the shared source of
truth (reuses getCSP(), env-gated exactly like next.config) and:
- vercel.ts: apply to every response (source '/(.*)'), all base-path prefixes —
  full getCSP() + HSTS on platform, the deployed surface that matters.
- scripts/serve.js: apply the non-platform set (frame-ancestors 'none') for the
  self-hosted server, matching next.config's non-platform branch.

Note: font-src in the CSP is 'self' + cloudflare + supabase-assets (no
fonts.gstatic.com), so vendoring the fonts locally in the previous commit is what
keeps them CSP-compliant. X-Content-Type-Options keeps next.config's 'no-sniff'
value verbatim for parity.
2026-07-07 00:20:32 +08:00

38 lines
1.5 KiB
TypeScript

import { getCSP } from './csp'
// Security response headers for the app. On the Next build these are applied via
// `next.config.ts` `headers()`; the TanStack build has no such hook and (on
// Vercel) serves a static shell with no server to attach them, so they're
// applied through `vercel.ts` (deploy) and `scripts/serve.js` (self-hosted)
// instead. Keep this in sync with the `/(.*?)` header block in next.config.ts.
//
// Env-gated exactly like next.config:
// - CSP: full `getCSP()` on platform, else just `frame-ancestors 'none'`.
// - HSTS: only on platform + Vercel (never for self-hosted / previews on a
// bare IP), so it's omitted rather than sent empty.
export function getSecurityHeaders(): Array<{ key: string; value: string }> {
const isPlatform = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true'
const isVercel = process.env.VERCEL === '1'
const headers = [
{ key: 'X-Frame-Options', value: 'DENY' },
// Mirrors next.config.ts (note: the effective value browsers honour is
// `nosniff`; kept identical to the Next build to avoid a behaviour split).
{ key: 'X-Content-Type-Options', value: 'no-sniff' },
{
key: 'Content-Security-Policy',
value: isPlatform ? getCSP() : "frame-ancestors 'none';",
},
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
]
if (isPlatform && isVercel) {
headers.push({
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload',
})
}
return headers
}