From 2dbdf76c75852f14fee9564b7b29be070754ed5b Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sat, 27 Dec 2025 00:36:21 +0000 Subject: fix: reject spoofed content types on uploads --- packages/api/package.json | 1 + packages/api/utils/upload.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) (limited to 'packages/api') diff --git a/packages/api/package.json b/packages/api/package.json index c9f34bcb..b5d90f03 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -22,6 +22,7 @@ "@karakeep/trpc": "workspace:*", "@trpc/server": "^11.4.3", "drizzle-orm": "^0.44.2", + "file-type": "^21.2.0", "hono": "^4.10.6", "prom-client": "^15.1.3", "rss": "^1.2.2", diff --git a/packages/api/utils/upload.ts b/packages/api/utils/upload.ts index a843e29c..b82bc855 100644 --- a/packages/api/utils/upload.ts +++ b/packages/api/utils/upload.ts @@ -3,6 +3,7 @@ import * as os from "os"; import * as path from "path"; import { Readable } from "stream"; import { pipeline } from "stream/promises"; +import { fileTypeFromBlob, supportedMimeTypes } from "file-type"; import { assets, AssetTypes } from "@karakeep/db/schema"; import { QuotaService, StorageQuotaError } from "@karakeep/shared-server"; @@ -58,7 +59,16 @@ export async function uploadAsset( data = formData.image; } - const contentType = data.type; + const detectedType = await fileTypeFromBlob(data); + const fallbackType = + data.type && data.type.trim().length > 0 ? data.type : null; + // Security: reject browser-provided MIME when we cannot sniff a valid type. + if (fallbackType && supportedMimeTypes.has(fallbackType) && !detectedType) { + return { error: "Unsupported asset type", status: 400 }; + } + const contentType = + detectedType?.mime ?? fallbackType ?? "application/octet-stream"; + // Replace all non-ascii characters with underscores const fileName = data.name.replace(/[^\x20-\x7E]/g, "_"); if (!SUPPORTED_UPLOAD_ASSET_TYPES.has(contentType)) { -- cgit v1.2.3-70-g09d2