History

fix(server): sandbox user-provided SVGs (#5916)

Theo Browne committed 3 months ago

Commit 2abe6681 parent 0d388662 files · +33 −5Browse files

Unified · Split

2 files
  1. apps/server/src/http.test.ts +20 −1
  2. apps/server/src/http.ts +13 −4
apps/server/src/http.test.ts+20−1
@@ -1,7 +1,7 @@
1import { expect, it } from "@effect/vitest";1import { expect, it } from "@effect/vitest";
2import { describe } from "vite-plus/test";2import { describe } from "vite-plus/test";
3 3
4import { isLoopbackHostname, resolveDevRedirectUrl } from "./http.ts";4import { assetResponseHeaders, isLoopbackHostname, resolveDevRedirectUrl } from "./http.ts";
5 5
6describe("http dev routing", () => {6describe("http dev routing", () => {
7 it("treats localhost and loopback addresses as local", () => {7 it("treats localhost and loopback addresses as local", () => {
@@ -26,3 +26,22 @@ describe("http dev routing", () => { · 18 unmodified lines
26 );26 );
27 });27 });
28});28});
29
30describe("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 · 42 unmodified lines
43const OTLP_TRACES_PROXY_PATH = "/api/observability/v1/traces";43const OTLP_TRACES_PROXY_PATH = "/api/observability/v1/traces";
44const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);44const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);
45const DESKTOP_RENDERER_ORIGINS = ["t3code://app", "t3code-dev://app"];45const DESKTOP_RENDERER_ORIGINS = ["t3code://app", "t3code-dev://app"];
46const SVG_CONTENT_SECURITY_POLICY = "default-src 'none'; style-src 'unsafe-inline'; sandbox";
47
48export 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
46export const httpCompressionLayer = HttpRouter.middleware(HttpMiddleware.compression(), {58export const httpCompressionLayer = HttpRouter.middleware(HttpMiddleware.compression(), {
47 global: true,59 global: true,
48});60});
@@ -207,10 +219,7 @@ export const assetRouteLayer = HttpRouter.add( · 158 unmodified lines
207 }219 }
208 return yield* HttpServerResponse.file(asset.path, {220 return yield* HttpServerResponse.file(asset.path, {
209 status: 200,221 status: 200,
210 headers: {222 headers: assetResponseHeaders(asset.path),
211 "Cache-Control": "private, max-age=3600",
212 "X-Content-Type-Options": "nosniff",
213 },
214 }).pipe(223 }).pipe(
215 Effect.orElseSucceed(() => HttpServerResponse.text("Internal Server Error", { status: 500 })),224 Effect.orElseSucceed(() => HttpServerResponse.text("Internal Server Error", { status: 500 })),
216 );225 );