| author | |
| committer | |
| log | 2abe66800cceeafea759d4db782c72298cc1e1e1 |
| tree | 9c1113fe93ca28f137817e9807d09d9cb7284c5f |
| parent | 0d38866dcf63d133b2ed732bbb303dc533b5934f |
| signature | Signed by PGP key B5690EEEBB952194 |
2 files changed, 33 insertions(+), 5 deletions(-)
apps/server/src/http.test.ts+20-1| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | import { expect, it } from "@effect/vitest"; |
| 2 | 2 | import { describe } from "vite-plus/test"; |
| 3 | 3 | |
| 4 | import { isLoopbackHostname, resolveDevRedirectUrl } from "./http.ts"; | |
| 4 | import { assetResponseHeaders, isLoopbackHostname, resolveDevRedirectUrl } from "./http.ts"; | |
| 5 | 5 | |
| 6 | 6 | describe("http dev routing", () => { |
| 7 | 7 | it("treats localhost and loopback addresses as local", () => { |
| ... | ... | @@ -26,3 +26,22 @@ describe("http dev routing", () => { |
| 26 | 26 | ); |
| 27 | 27 | }); |
| 28 | 28 | }); |
| 29 | ||
| 30 | describe("assetResponseHeaders", () => { | |
| 31 | it("sandboxes SVG assets", () => { | |
| 32 | expect(assetResponseHeaders("/attachments/user-image.svg")).toMatchObject({ | |
| 33 | "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox", | |
| 34 | "X-Content-Type-Options": "nosniff", | |
| 35 | }); | |
| 36 | expect(assetResponseHeaders("/attachments/user-image.SVG")).toHaveProperty( | |
| 37 | "Content-Security-Policy", | |
| 38 | ); | |
| 39 | }); | |
| 40 | ||
| 41 | it("does not apply document policy to raster images", () => { | |
| 42 | expect(assetResponseHeaders("/attachments/user-image.png")).toEqual({ | |
| 43 | "Cache-Control": "private, max-age=3600", | |
| 44 | "X-Content-Type-Options": "nosniff", | |
| 45 | }); | |
| 46 | }); | |
| 47 | }); |
apps/server/src/http.ts+13-4| ... | ... | @@ -43,6 +43,18 @@ import { browserApiCorsAllowedHeaders, browserApiCorsAllowedMethods } from "./ht |
| 43 | 43 | const OTLP_TRACES_PROXY_PATH = "/api/observability/v1/traces"; |
| 44 | 44 | const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]); |
| 45 | 45 | const DESKTOP_RENDERER_ORIGINS = ["t3code://app", "t3code-dev://app"]; |
| 46 | const SVG_CONTENT_SECURITY_POLICY = "default-src 'none'; style-src 'unsafe-inline'; sandbox"; | |
| 47 | ||
| 48 | export function assetResponseHeaders(filePath: string): Record<string, string> { | |
| 49 | return { | |
| 50 | "Cache-Control": "private, max-age=3600", | |
| 51 | "X-Content-Type-Options": "nosniff", | |
| 52 | ...(filePath.toLowerCase().endsWith(".svg") | |
| 53 | ? { "Content-Security-Policy": SVG_CONTENT_SECURITY_POLICY } | |
| 54 | : {}), | |
| 55 | }; | |
| 56 | } | |
| 57 | ||
| 46 | 58 | export const httpCompressionLayer = HttpRouter.middleware(HttpMiddleware.compression(), { |
| 47 | 59 | global: true, |
| 48 | 60 | }); |
| ... | ... | @@ -207,10 +219,7 @@ export const assetRouteLayer = HttpRouter.add( |
| 207 | 219 | } |
| 208 | 220 | return yield* HttpServerResponse.file(asset.path, { |
| 209 | 221 | status: 200, |
| 210 | headers: { | |
| 211 | "Cache-Control": "private, max-age=3600", | |
| 212 | "X-Content-Type-Options": "nosniff", | |
| 213 | }, | |
| 222 | headers: assetResponseHeaders(asset.path), | |
| 214 | 223 | }).pipe( |
| 215 | 224 | Effect.orElseSucceed(() => HttpServerResponse.text("Internal Server Error", { status: 500 })), |
| 216 | 225 | ); |