From 435a454bd3665d6219a214dfacf15c1c264b18fc Mon Sep 17 00:00:00 2001 From: David Mzareulyan Date: Thu, 20 Aug 2026 12:40:30 +0300 Subject: [PATCH 1/3] fix: resolve text attachment MIME types Fall back to known text extensions and browser-provided MIME types when signature detection returns no result. Add YAML to the attachment allowlist and cover upload metadata behavior with unit tests. --- apps/api/plane/settings/common.py | 1 + packages/services/package.json | 4 ++- packages/services/src/file/helper.test.ts | 41 +++++++++++++++++++++++ packages/services/src/file/helper.ts | 14 ++++++-- pnpm-lock.yaml | 3 ++ 5 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 packages/services/src/file/helper.test.ts diff --git a/apps/api/plane/settings/common.py b/apps/api/plane/settings/common.py index 25a212e7639..3169ca36926 100644 --- a/apps/api/plane/settings/common.py +++ b/apps/api/plane/settings/common.py @@ -533,6 +533,7 @@ def _retention_days(env_var, default): "text/css", "text/javascript", "application/json", + "application/yaml", "text/xml", "text/csv", "application/xml", diff --git a/packages/services/package.json b/packages/services/package.json index c76d4a6c081..c647ef06f0e 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -14,6 +14,7 @@ "scripts": { "build": "tsdown", "dev": "tsdown --watch --no-clean", + "test": "vitest run", "check:lint": "oxlint --max-warnings=6 .", "check:types": "tsc --noEmit", "check:format": "oxfmt --check .", @@ -30,6 +31,7 @@ "devDependencies": { "@plane/typescript-config": "workspace:*", "tsdown": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/services/src/file/helper.test.ts b/packages/services/src/file/helper.test.ts new file mode 100644 index 00000000000..7a6ef404ea5 --- /dev/null +++ b/packages/services/src/file/helper.test.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { describe, expect, it, vi } from "vitest"; + +import { getFileMetaDataForUpload } from "./helper"; + +const metadata = (name: string, type = "", contents: BlobPart[] = ["key: value"]) => + getFileMetaDataForUpload(new File(contents, name, { type })); + +describe("getFileMetaDataForUpload", () => { + it.each([ + ["program.txt", "text/plain"], + ["program.yaml", "application/yaml"], + ["program.yml", "application/yaml"], + ])("detects %s without browser MIME", async (name, expected) => { + expect((await metadata(name)).type).toBe(expected); + }); + + it("falls back to browser MIME", async () => { + expect((await metadata("program.log", "text/plain")).type).toBe("text/plain"); + }); + + it("prefers signature detection", async () => { + const pngHeader = new Uint8Array([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, + ]); + + expect((await metadata("image.yaml", "application/yaml", [pngHeader])).type).toBe("image/png"); + }); + + it("does not apply fallback to an unsafe filename", async () => { + vi.spyOn(console, "warn").mockImplementation(() => undefined); + + expect((await metadata("payload.exe.yaml")).type).toBe(""); + }); +}); diff --git a/packages/services/src/file/helper.ts b/packages/services/src/file/helper.ts index b8e96283986..f775ac8ab74 100644 --- a/packages/services/src/file/helper.ts +++ b/packages/services/src/file/helper.ts @@ -10,6 +10,15 @@ import { fileTypeFromBuffer } from "file-type"; import type { TFileMetaDataLite, TFileSignedURLResponse } from "@plane/types"; import { DANGEROUS_EXTENSIONS } from "@plane/constants"; +const TEXT_MIME_TYPES_BY_EXTENSION: Record = { + txt: "text/plain", + md: "text/markdown", + markdown: "text/markdown", + csv: "text/csv", + yaml: "application/yaml", + yml: "application/yaml", +}; + /** * @description Filename validation - checks for double extensions and dangerous patterns * @param {string} filename @@ -92,6 +101,7 @@ const validateAndDetectFileType = async (file: File): Promise => { const filenameError = validateFilename(file.name); if (filenameError) { console.warn(`File validation warning: ${filenameError}`); + return ""; } try { @@ -103,8 +113,8 @@ const validateAndDetectFileType = async (file: File): Promise => { console.warn("Error detecting file type from signature:", _error); } - // fallback for unknown files - return ""; + const extension = file.name.split(".").pop()?.toLowerCase() ?? ""; + return TEXT_MIME_TYPES_BY_EXTENSION[extension] || file.type || ""; }; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbf11e23d18..fe42621e890 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1665,6 +1665,9 @@ importers: typescript: specifier: 5.8.3 version: 5.8.3 + vitest: + specifier: 'catalog:' + version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.12.0)(@vitest/coverage-v8@4.1.8)(vite@8.0.16(@types/node@22.12.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3)) packages/shared-state: dependencies: From 12a1d0548571fa1239dd3ed7780699edc663b730 Mon Sep 17 00:00:00 2001 From: David Mzareulyan Date: Thu, 20 Aug 2026 13:29:35 +0300 Subject: [PATCH 2/3] Restore the spy so this test is isolated Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/services/src/file/helper.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/services/src/file/helper.test.ts b/packages/services/src/file/helper.test.ts index 7a6ef404ea5..da0e4beeb9d 100644 --- a/packages/services/src/file/helper.test.ts +++ b/packages/services/src/file/helper.test.ts @@ -34,8 +34,12 @@ describe("getFileMetaDataForUpload", () => { }); it("does not apply fallback to an unsafe filename", async () => { - vi.spyOn(console, "warn").mockImplementation(() => undefined); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined); - expect((await metadata("payload.exe.yaml")).type).toBe(""); + try { + expect((await metadata("payload.exe.yaml")).type).toBe(""); + } finally { + warnSpy.mockRestore(); + } }); }); From 397de4c3dec2c222f29d28d82854b4e16badca59 Mon Sep 17 00:00:00 2001 From: David Mzareulyan Date: Thu, 20 Aug 2026 13:35:17 +0300 Subject: [PATCH 3/3] Expand upload MIME type tests for markdown and CSV Add test cases to ensure correct MIME type detection for .md, .markdown, and .csv file uploads. --- packages/services/src/file/helper.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/services/src/file/helper.test.ts b/packages/services/src/file/helper.test.ts index da0e4beeb9d..a5666f7f886 100644 --- a/packages/services/src/file/helper.test.ts +++ b/packages/services/src/file/helper.test.ts @@ -14,6 +14,9 @@ const metadata = (name: string, type = "", contents: BlobPart[] = ["key: value"] describe("getFileMetaDataForUpload", () => { it.each([ ["program.txt", "text/plain"], + ["program.md", "text/markdown"], + ["program.markdown", "text/markdown"], + ["program.csv", "text/csv"], ["program.yaml", "application/yaml"], ["program.yml", "application/yaml"], ])("detects %s without browser MIME", async (name, expected) => {