diff --git a/convex/httpApiV1.handlers.test.ts b/convex/httpApiV1.handlers.test.ts index 6e17fa97..1b05a7cd 100644 --- a/convex/httpApiV1.handlers.test.ts +++ b/convex/httpApiV1.handlers.test.ts @@ -337,7 +337,7 @@ describe('httpApiV1 handlers', () => { expect(await response.text()).toBe('hello') expect(response.headers.get('Content-Type')).toBe('text/plain; charset=utf-8') expect(response.headers.get('X-Content-Type-Options')).toBe('nosniff') - expect(response.headers.get('Content-Security-Policy')).toBe('sandbox; default-src \'none\'') + expect(response.headers.get('Content-Security-Policy')).toBe("sandbox; default-src 'none'") expect(response.headers.get('X-Content-SHA256')).toBe('abcd') }) @@ -383,7 +383,7 @@ describe('httpApiV1 handlers', () => { expect(await response.text()).toBe('hello') expect(response.headers.get('Content-Type')).toBe('text/plain; charset=utf-8') expect(response.headers.get('X-Content-Type-Options')).toBe('nosniff') - expect(response.headers.get('Content-Security-Policy')).toBe('sandbox; default-src \'none\'') + expect(response.headers.get('Content-Security-Policy')).toBe("sandbox; default-src 'none'") expect(response.headers.get('X-Content-SHA256')).toBe('abcd') }) diff --git a/convex/httpApiV1.ts b/convex/httpApiV1.ts index 62e10288..c70eabd0 100644 --- a/convex/httpApiV1.ts +++ b/convex/httpApiV1.ts @@ -15,7 +15,7 @@ const RATE_LIMITS = { } as const const MAX_RAW_FILE_BYTES = 200 * 1024 const RAW_FILE_CONTENT_TYPE = 'text/plain; charset=utf-8' -const RAW_FILE_CSP = 'sandbox; default-src \'none\'' +const RAW_FILE_CSP = "sandbox; default-src 'none'" type SearchSkillEntry = { score: number diff --git a/server/middleware/securityHeaders.ts b/server/middleware/securityHeaders.ts new file mode 100644 index 00000000..74a662c9 --- /dev/null +++ b/server/middleware/securityHeaders.ts @@ -0,0 +1,28 @@ +import { defineEventHandler, getRequestHeader, setHeader } from 'h3' + +const CSP = [ + "default-src 'self'", + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", + "object-src 'none'", + "script-src 'self' 'unsafe-inline'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: https: blob:", + "font-src 'self' data:", + "connect-src 'self' https: wss:", +].join('; ') + +export default defineEventHandler((event) => { + const accept = getRequestHeader(event, 'accept') ?? '' + const isHtml = accept.includes('text/html') + + if (isHtml && process.env.NODE_ENV === 'production') { + setHeader(event, 'Content-Security-Policy', CSP) + } + + setHeader(event, 'Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()') + setHeader(event, 'Referrer-Policy', 'strict-origin-when-cross-origin') + setHeader(event, 'X-Content-Type-Options', 'nosniff') + setHeader(event, 'X-Frame-Options', 'DENY') +})