diff --git a/apps/api/v1/pages/api/users/[userId]/_patch.ts b/apps/api/v1/pages/api/users/[userId]/_patch.ts index f073c05b35..3f1cf43a04 100644 --- a/apps/api/v1/pages/api/users/[userId]/_patch.ts +++ b/apps/api/v1/pages/api/users/[userId]/_patch.ts @@ -3,8 +3,6 @@ import type { NextApiRequest } from "next"; import { HttpError } from "@calcom/lib/http-error"; import { uploadAvatar } from "@calcom/lib/server/avatar"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import { validateBase64Image } from "@calcom/lib/server/imageValidation"; -import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image"; import prisma from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; @@ -126,25 +124,10 @@ export async function patchHandler(req: NextApiRequest) { } if (avatar) { - const validation = validateBase64Image(avatar); - if (!validation.isValid) { - throw new HttpError({ - statusCode: 400, - message: `Invalid avatar image: ${validation.error}`, - }); - } - - try { - body.avatarUrl = await uploadAvatar({ - userId: query.userId, - avatar: await resizeBase64Image(avatar), - }); - } catch (error) { - throw new HttpError({ - statusCode: 400, - message: error instanceof Error ? error.message : "Failed to upload avatar", - }); - } + body.avatarUrl = await uploadAvatar({ + userId: query.userId, + avatar: await (await import("@calcom/lib/server/resizeBase64Image")).resizeBase64Image(avatar), + }); } const data = await prisma.user.update({ diff --git a/apps/web/app/api/avatar/[uuid]/route.ts b/apps/web/app/api/avatar/[uuid]/route.ts index eb4308079a..348bb567ca 100644 --- a/apps/web/app/api/avatar/[uuid]/route.ts +++ b/apps/web/app/api/avatar/[uuid]/route.ts @@ -6,7 +6,6 @@ import { z } from "zod"; import { AVATAR_FALLBACK, WEBAPP_URL } from "@calcom/lib/constants"; import { convertSvgToPng } from "@calcom/lib/server/imageUtils"; -import { validateBase64Image } from "@calcom/lib/server/imageValidation"; import prisma from "@calcom/prisma"; const querySchema = z.object({ @@ -47,12 +46,6 @@ async function handler(req: NextRequest, { params }: { params: Promise } }, }); - const validation = validateBase64Image(data); - if (!validation.isValid) { - const url = new URL(AVATAR_FALLBACK, WEBAPP_URL).toString(); - return NextResponse.redirect(url, 302); - } - // Convert SVG to PNG if needed and update the database if (data.startsWith("data:image/svg+xml;base64,")) { const pngData = await convertSvgToPng(data); @@ -79,11 +72,6 @@ async function handler(req: NextRequest, { params }: { params: Promise } "Content-Type": "image/png", "Content-Length": imageResp.length.toString(), "Cache-Control": "max-age=86400", - // Security headers to prevent XSS - "X-Content-Type-Options": "nosniff", - "Content-Disposition": "inline", - "X-Frame-Options": "DENY", - "Content-Security-Policy": "default-src 'none'; img-src 'self'", }, status: 200, }); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 0dcec6b8f7..207f1f530e 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -3581,14 +3581,6 @@ "webhook_metadata": "Metadata", "stats": "Stats", "booking_status": "Booking status", - "unsupported_file_type": "{{type}} files cannot be uploaded as images", - "only_image_files_allowed": "Only image files are allowed", - "failed_to_validate_image_file": "Failed to validate image file", - "invalid_image_file_format": "Invalid image file format", - "svg_contains_dangerous_content": "SVG contains potentially dangerous content", - "unrecognized_image_format": "Unrecognized image format or invalid file", - "invalid_base64_format": "Invalid base64 format", - "empty_image_data": "Empty image data", "visit": "Visit", "location_custom_label_input_label": "Custom label on booking page", "meeting_link": "Meeting link", diff --git a/packages/app-store/_utils/oauth/updateProfilePhotoGoogle.ts b/packages/app-store/_utils/oauth/updateProfilePhotoGoogle.ts index 5a9d3d4eae..5526343d87 100644 --- a/packages/app-store/_utils/oauth/updateProfilePhotoGoogle.ts +++ b/packages/app-store/_utils/oauth/updateProfilePhotoGoogle.ts @@ -3,7 +3,6 @@ import type { OAuth2Client } from "googleapis-common"; import logger from "@calcom/lib/logger"; import { uploadAvatar } from "@calcom/lib/server/avatar"; -import { validateBase64Image } from "@calcom/lib/server/imageValidation"; import { UserRepository } from "@calcom/lib/server/repository/user"; import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image"; import prisma from "@calcom/prisma"; @@ -17,17 +16,12 @@ export async function updateProfilePhotoGoogle(oAuth2Client: OAuth2Client, userI return; } + // Handle base64 data if ( avatarUrl.startsWith("data:image/png;base64,") || avatarUrl.startsWith("data:image/jpeg;base64,") || avatarUrl.startsWith("data:image/jpg;base64,") ) { - const validation = validateBase64Image(avatarUrl); - if (!validation.isValid) { - logger.error(`Invalid avatar image from Google OAuth: ${validation.error}`); - return; - } - const resizedAvatarUrl = await uploadAvatar({ avatar: await resizeBase64Image(avatarUrl), userId, diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 42a66ac4b1..a414ace851 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -68,9 +68,6 @@ export const MAX_EVENT_DURATION_MINUTES = 1440; /** Minimum duration allowed for an event in minutes */ export const MIN_EVENT_DURATION_MINUTES = 1; -/** Maximum file size allowed for banner uploads in bytes (5MB) */ -export const MAX_BANNER_SIZE = 5 * 1024 * 1024; - export const HOSTED_CAL_FEATURES = process.env.NEXT_PUBLIC_HOSTED_CAL_FEATURES || !IS_SELF_HOSTED; export const PUBLIC_QUERY_RESERVATION_INTERVAL_SECONDS = diff --git a/packages/lib/imageValidationConstants.ts b/packages/lib/imageValidationConstants.ts deleted file mode 100644 index 2a2c646bac..0000000000 --- a/packages/lib/imageValidationConstants.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Shared image validation constants and utilities - * Used by both server-side and client-side validation modules - */ - -/** - * Magic numbers (file signatures) for different file types - */ -export const FILE_SIGNATURES = { - PNG: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], - JPEG_FF_D8_FF: [0xff, 0xd8, 0xff], - GIF87a: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61], - GIF89a: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61], - WEBP: [0x52, 0x49, 0x46, 0x46], - WEBP_SIGNATURE: [0x57, 0x45, 0x42, 0x50], - BMP: [0x42, 0x4d], - ICO: [0x00, 0x00, 0x01, 0x00], - SVG: [0x3c, 0x3f, 0x78, 0x6d, 0x6c], - SVG_DIRECT: [0x3c, 0x73, 0x76, 0x67], - - PDF: [0x25, 0x50, 0x44, 0x46], - HTML: [0x3c, 0x21, 0x44, 0x4f, 0x43, 0x54, 0x59, 0x50, 0x45], - HTML_TAG: [0x3c, 0x68, 0x74, 0x6d, 0x6c], - SCRIPT_TAG: [0x3c, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74], - ZIP: [0x50, 0x4b, 0x03, 0x04], - EXECUTABLE: [0x4d, 0x5a], -} as const; - -/** - * Check if bytes match a signature - */ -export function matchesSignature(data: Uint8Array, signature: readonly number[]): boolean { - if (data.length < signature.length) return false; - return signature.every((byte, index) => data[index] === byte); -} - -/** - * SVG content validation patterns - */ -export const DANGEROUS_SVG_PATTERNS = [ - " { - const validation = validateBase64Image(data); - if (!validation.isValid) { - throw new Error(`Invalid image data: ${validation.error}`); - } - const objectKey = uuidv4(); const processedData = await convertSvgToPng(data); @@ -46,11 +40,6 @@ export const uploadLogo = async ({ logo: string; isBanner?: boolean; }): Promise => { - const validation = validateBase64Image(data); - if (!validation.isValid) { - throw new Error(`Invalid image data: ${validation.error}`); - } - const objectKey = uuidv4(); const processedData = await convertSvgToPng(data); diff --git a/packages/lib/server/imageValidation.test.ts b/packages/lib/server/imageValidation.test.ts deleted file mode 100644 index 3a7751dbb3..0000000000 --- a/packages/lib/server/imageValidation.test.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { describe, it, expect } from "vitest"; - -import { FILE_SIGNATURES } from "@calcom/lib/imageValidationConstants"; -import { validateBase64Image } from "@calcom/lib/server/imageValidation"; - -describe("validateBase64Image", () => { - const createBase64Data = (bytes: number[], mimeType = "image/png") => { - const buffer = Buffer.from(bytes); - return `data:${mimeType};base64,${buffer.toString("base64")}`; - }; - - describe("Valid image formats", () => { - it("should validate PNG images", () => { - const pngBytes = [...FILE_SIGNATURES.PNG, ...Array(10).fill(0)]; - const base64Data = createBase64Data(pngBytes); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(true); - expect(result.detectedFormat).toBe("PNG"); - expect(result.error).toBeUndefined(); - }); - - it("should validate JPEG images", () => { - const jpegBytes = [...FILE_SIGNATURES.JPEG_FF_D8_FF, 0xe0, ...Array(10).fill(0)]; - const base64Data = createBase64Data(jpegBytes, "image/jpeg"); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(true); - expect(result.detectedFormat).toBe("JPEG"); - }); - - it("should validate GIF87a images", () => { - const gifBytes = [...FILE_SIGNATURES.GIF87a, ...Array(10).fill(0)]; - const base64Data = createBase64Data(gifBytes, "image/gif"); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(true); - expect(result.detectedFormat).toBe("GIF"); - }); - - it("should validate GIF89a images", () => { - const gifBytes = [...FILE_SIGNATURES.GIF89a, ...Array(10).fill(0)]; - const base64Data = createBase64Data(gifBytes, "image/gif"); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(true); - expect(result.detectedFormat).toBe("GIF"); - }); - - it("should validate WEBP images", () => { - const webpBytes = [ - ...FILE_SIGNATURES.WEBP, - 0x00, - 0x00, - 0x00, - 0x00, - ...FILE_SIGNATURES.WEBP_SIGNATURE, - ...Array(10).fill(0), - ]; - const base64Data = createBase64Data(webpBytes, "image/webp"); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(true); - expect(result.detectedFormat).toBe("WEBP"); - }); - - it("should validate BMP images", () => { - const bmpBytes = [...FILE_SIGNATURES.BMP, ...Array(10).fill(0)]; - const base64Data = createBase64Data(bmpBytes, "image/bmp"); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(true); - expect(result.detectedFormat).toBe("BMP"); - }); - - it("should validate ICO images", () => { - const icoBytes = [...FILE_SIGNATURES.ICO, ...Array(10).fill(0)]; - const base64Data = createBase64Data(icoBytes, "image/x-icon"); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(true); - expect(result.detectedFormat).toBe("ICO"); - }); - - it("should validate safe SVG images", () => { - const svgContent = - ''; - const svgBytes = Array.from(Buffer.from(svgContent, "utf8")); - const base64Data = createBase64Data(svgBytes, "image/svg+xml"); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(true); - expect(result.detectedFormat).toBe("SVG"); - }); - - it("should validate SVG with direct tag", () => { - const svgContent = ''; - const svgBytes = Array.from(Buffer.from(svgContent, "utf8")); - const base64Data = createBase64Data(svgBytes, "image/svg+xml"); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(true); - expect(result.detectedFormat).toBe("SVG"); - }); - }); - - describe("Dangerous file types", () => { - it("should reject PDF files", () => { - const pdfBytes = [...FILE_SIGNATURES.PDF, 0x2d, 0x31, 0x2e, 0x34]; - const base64Data = createBase64Data(pdfBytes); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(false); - expect(result.detectedFormat).toBe("PDF"); - expect(result.errorKey).toBe("unsupported_file_type"); - expect(result.errorParams).toEqual({ type: "PDF" }); - }); - - it("should reject HTML files with DOCTYPE", () => { - const htmlBytes = [...FILE_SIGNATURES.HTML]; - const base64Data = createBase64Data(htmlBytes); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(false); - expect(result.detectedFormat).toBe("HTML"); - expect(result.errorKey).toBe("unsupported_file_type"); - expect(result.errorParams).toEqual({ type: "HTML" }); - }); - - it("should reject HTML files with tag", () => { - const htmlBytes = [...FILE_SIGNATURES.HTML_TAG, 0x3e]; - const base64Data = createBase64Data(htmlBytes); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(false); - expect(result.detectedFormat).toBe("HTML"); - expect(result.errorKey).toBe("unsupported_file_type"); - expect(result.errorParams).toEqual({ type: "HTML" }); - }); - - it("should reject script files", () => { - const scriptBytes = [...FILE_SIGNATURES.SCRIPT_TAG]; - const base64Data = createBase64Data(scriptBytes); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(false); - expect(result.detectedFormat).toBe("Script"); - expect(result.errorKey).toBe("unsupported_file_type"); - expect(result.errorParams).toEqual({ type: "Script" }); - }); - - it("should reject ZIP files", () => { - const zipBytes = [...FILE_SIGNATURES.ZIP]; - const base64Data = createBase64Data(zipBytes); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(false); - expect(result.detectedFormat).toBe("ZIP"); - expect(result.errorKey).toBe("unsupported_file_type"); - expect(result.errorParams).toEqual({ type: "ZIP" }); - }); - - it("should reject executable files", () => { - const exeBytes = [...FILE_SIGNATURES.EXECUTABLE]; - const base64Data = createBase64Data(exeBytes); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(false); - expect(result.detectedFormat).toBe("Executable"); - expect(result.errorKey).toBe("unsupported_file_type"); - expect(result.errorParams).toEqual({ type: "Executable" }); - }); - - it("should reject SVG with script content", () => { - const svgContent = ''; - const svgBytes = Array.from(Buffer.from(svgContent, "utf8")); - const base64Data = createBase64Data(svgBytes, "image/svg+xml"); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(false); - expect(result.detectedFormat).toBe("SVG"); - expect(result.error).toBe("svg_contains_dangerous_content"); - }); - - it("should reject SVG with javascript: URLs", () => { - const svgContent = - 'link'; - const svgBytes = Array.from(Buffer.from(svgContent, "utf8")); - const base64Data = createBase64Data(svgBytes, "image/svg+xml"); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(false); - expect(result.detectedFormat).toBe("SVG"); - expect(result.error).toBe("svg_contains_dangerous_content"); - }); - - it("should reject SVG with onload handlers", () => { - const svgContent = ''; - const svgBytes = Array.from(Buffer.from(svgContent, "utf8")); - const base64Data = createBase64Data(svgBytes, "image/svg+xml"); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(false); - expect(result.detectedFormat).toBe("SVG"); - expect(result.error).toBe("svg_contains_dangerous_content"); - }); - }); - - describe("Error format validation", () => { - it("should return errorKey and errorParams for unsupported file types", () => { - const pdfBytes = [...FILE_SIGNATURES.PDF, 0x2d, 0x31, 0x2e, 0x34]; - const base64Data = createBase64Data(pdfBytes); - - const result = validateBase64Image(base64Data); - - expect(result).toHaveProperty("isValid"); - expect(result).toHaveProperty("errorKey"); - expect(result).toHaveProperty("errorParams"); - expect(result).toHaveProperty("detectedFormat"); - expect(result.isValid).toBe(false); - expect(result.errorKey).toBe("unsupported_file_type"); - expect(result.errorParams).toEqual({ type: "PDF" }); - }); - - it("should return error field for non-interpolated errors", () => { - const base64Data = "data:image/png;base64,"; - - const result = validateBase64Image(base64Data); - - expect(result).toHaveProperty("isValid"); - expect(result).toHaveProperty("error"); - expect(result.isValid).toBe(false); - expect(result.error).toBe("empty_image_data"); - expect(result.errorKey).toBeUndefined(); - expect(result.errorParams).toBeUndefined(); - }); - }); - - describe("Edge cases", () => { - it("should handle empty image data", () => { - const base64Data = "data:image/png;base64,"; - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(false); - expect(result.error).toBe("empty_image_data"); - }); - - it("should handle malformed base64", () => { - const base64Data = "data:image/png;base64,invalid-base64!@#$"; - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(false); - expect(result.error).toBe("invalid_base64_format"); - }); - - it("should handle unrecognized file format", () => { - const unknownBytes = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05]; - const base64Data = createBase64Data(unknownBytes); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(false); - expect(result.detectedFormat).toBe("Unknown"); - expect(result.error).toBe("unrecognized_image_format"); - }); - - it("should handle files too short for magic number detection", () => { - const shortBytes = [0x89]; - const base64Data = createBase64Data(shortBytes); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(false); - expect(result.detectedFormat).toBe("Unknown"); - expect(result.error).toBe("unrecognized_image_format"); - }); - - it("should handle WEBP files without proper WEBP signature", () => { - const fakeWebpBytes = [0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x46, 0x41, 0x4b, 0x45]; - const base64Data = createBase64Data(fakeWebpBytes); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(false); - expect(result.detectedFormat).toBe("Unknown"); - expect(result.error).toBe("unrecognized_image_format"); - }); - - it("should handle base64 data without proper data URL prefix", () => { - const pngBytes = [...FILE_SIGNATURES.PNG]; - const buffer = Buffer.from(pngBytes); - const base64Data = buffer.toString("base64"); - - const result = validateBase64Image(base64Data); - - expect(result.isValid).toBe(true); - expect(result.detectedFormat).toBe("PNG"); - }); - }); -}); diff --git a/packages/lib/server/imageValidation.ts b/packages/lib/server/imageValidation.ts deleted file mode 100644 index 2817cca9e5..0000000000 --- a/packages/lib/server/imageValidation.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Server-side image validation utility with magic number checking to prevent XSS attacks - * This prevents malicious files (like PDFs with embedded JavaScript) from being uploaded as images - */ -import { - FILE_SIGNATURES, - matchesSignature, - containsDangerousSVGContent, - isValidBase64, -} from "../imageValidationConstants"; - -export interface ImageValidationResult { - isValid: boolean; - error?: string; - errorKey?: string; - errorParams?: Record; - detectedFormat?: string; -} - -/** - * Validate base64 image data for magic numbers and content - */ -export function validateBase64Image(base64Data: string): ImageValidationResult { - try { - const base64Content = base64Data.replace(/^data:image\/[^;]+;base64,/, ""); - - if (!isValidBase64(base64Content)) { - return { isValid: false, error: "invalid_base64_format" }; - } - - const buffer = Buffer.from(base64Content, "base64"); - const uint8Array = new Uint8Array(buffer); - - if (uint8Array.length === 0) { - return { isValid: false, error: "empty_image_data" }; - } - - if (matchesSignature(uint8Array, FILE_SIGNATURES.PDF)) { - return { - isValid: false, - errorKey: "unsupported_file_type", - errorParams: { type: "PDF" }, - detectedFormat: "PDF", - }; - } - - if ( - matchesSignature(uint8Array, FILE_SIGNATURES.HTML) || - matchesSignature(uint8Array, FILE_SIGNATURES.HTML_TAG) - ) { - return { - isValid: false, - errorKey: "unsupported_file_type", - errorParams: { type: "HTML" }, - detectedFormat: "HTML", - }; - } - - if (matchesSignature(uint8Array, FILE_SIGNATURES.SCRIPT_TAG)) { - return { - isValid: false, - errorKey: "unsupported_file_type", - errorParams: { type: "Script" }, - detectedFormat: "Script", - }; - } - - if (matchesSignature(uint8Array, FILE_SIGNATURES.ZIP)) { - return { - isValid: false, - errorKey: "unsupported_file_type", - errorParams: { type: "ZIP" }, - detectedFormat: "ZIP", - }; - } - - if (matchesSignature(uint8Array, FILE_SIGNATURES.EXECUTABLE)) { - return { - isValid: false, - errorKey: "unsupported_file_type", - errorParams: { type: "Executable" }, - detectedFormat: "Executable", - }; - } - - if (matchesSignature(uint8Array, FILE_SIGNATURES.PNG)) { - return { isValid: true, detectedFormat: "PNG" }; - } - - if (matchesSignature(uint8Array, FILE_SIGNATURES.JPEG_FF_D8_FF)) { - return { isValid: true, detectedFormat: "JPEG" }; - } - - if ( - matchesSignature(uint8Array, FILE_SIGNATURES.GIF87a) || - matchesSignature(uint8Array, FILE_SIGNATURES.GIF89a) - ) { - return { isValid: true, detectedFormat: "GIF" }; - } - - if (matchesSignature(uint8Array, FILE_SIGNATURES.BMP)) { - return { isValid: true, detectedFormat: "BMP" }; - } - - if (matchesSignature(uint8Array, FILE_SIGNATURES.ICO)) { - return { isValid: true, detectedFormat: "ICO" }; - } - - if (matchesSignature(uint8Array, FILE_SIGNATURES.WEBP) && uint8Array.length >= 12) { - if (matchesSignature(uint8Array.slice(8), FILE_SIGNATURES.WEBP_SIGNATURE)) { - return { isValid: true, detectedFormat: "WEBP" }; - } - } - - if ( - matchesSignature(uint8Array, FILE_SIGNATURES.SVG) || - matchesSignature(uint8Array, FILE_SIGNATURES.SVG_DIRECT) - ) { - const textContent = buffer.toString("utf8"); - - if (containsDangerousSVGContent(textContent)) { - return { isValid: false, error: "svg_contains_dangerous_content", detectedFormat: "SVG" }; - } - - return { isValid: true, detectedFormat: "SVG" }; - } - - return { isValid: false, error: "unrecognized_image_format", detectedFormat: "Unknown" }; - } catch (error) { - return { isValid: false, error: "failed_to_validate_image_file" }; - } -} diff --git a/packages/trpc/server/routers/viewer/me/updateProfile.handler.ts b/packages/trpc/server/routers/viewer/me/updateProfile.handler.ts index f97ccf5e75..9936331b20 100644 --- a/packages/trpc/server/routers/viewer/me/updateProfile.handler.ts +++ b/packages/trpc/server/routers/viewer/me/updateProfile.handler.ts @@ -13,7 +13,6 @@ import logger from "@calcom/lib/logger"; import { uploadAvatar } from "@calcom/lib/server/avatar"; import { checkUsername } from "@calcom/lib/server/checkUsername"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { validateBase64Image } from "@calcom/lib/server/imageValidation"; import { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries/teams"; import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image"; import slugify from "@calcom/lib/slugify"; @@ -152,26 +151,17 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) } } - if (input.avatarUrl) { - const validation = validateBase64Image(input.avatarUrl); - if (!validation.isValid) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `Invalid avatar image: ${validation.error}`, - }); - } - - try { - data.avatarUrl = await uploadAvatar({ - avatar: await resizeBase64Image(input.avatarUrl), - userId: user.id, - }); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: error instanceof Error ? error.message : "Failed to upload avatar", - }); - } + // if defined AND a base 64 string, upload and update the avatar URL + if ( + input.avatarUrl && + (input.avatarUrl.startsWith("data:image/png;base64,") || + input.avatarUrl.startsWith("data:image/jpeg;base64,") || + input.avatarUrl.startsWith("data:image/jpg;base64,")) + ) { + data.avatarUrl = await uploadAvatar({ + avatar: await resizeBase64Image(input.avatarUrl), + userId: user.id, + }); } if (input.completedOnboarding) { diff --git a/packages/trpc/server/routers/viewer/organizations/update.handler.ts b/packages/trpc/server/routers/viewer/organizations/update.handler.ts index a672c3a9f8..5957c63b0c 100644 --- a/packages/trpc/server/routers/viewer/organizations/update.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/update.handler.ts @@ -15,8 +15,6 @@ import { TRPCError } from "@trpc/server"; import type { TrpcSessionUser } from "../../../types"; import type { TUpdateInputSchema } from "./update.schema"; -import { validateBase64Image } from "@calcom/lib/server/imageValidation"; -import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; type UpdateOptions = { ctx: { @@ -188,76 +186,32 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { metadata: mergeMetadata({ ...input.metadata }), }; - if (input.banner) { - const validation = validateBase64Image(input.banner); - if (!validation.isValid) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `Invalid banner image: ${validation.error}`, - }); - } - - if ( - input.banner.startsWith("data:image/png;base64,") || + if ( + input.banner && + (input.banner.startsWith("data:image/png;base64,") || input.banner.startsWith("data:image/jpeg;base64,") || - input.banner.startsWith("data:image/jpg;base64,") || - input.banner.startsWith("data:image/svg+xml;base64,") - ) { - try { - const banner = await resizeBase64Image(input.banner, { maxSize: 1500 }); - data.bannerUrl = await uploadLogo({ - logo: banner, - teamId: currentOrgId, - isBanner: true, - }); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: error instanceof Error ? error.message : "Failed to upload banner", - }); - } - } else { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Unsupported banner format. Please use PNG, JPEG, or SVG.", - }); - } + input.banner.startsWith("data:image/jpg;base64,")) + ) { + const banner = await resizeBase64Image(input.banner, { maxSize: 1500 }); + data.bannerUrl = await uploadLogo({ + logo: banner, + teamId: currentOrgId, + isBanner: true, + }); } else { data.bannerUrl = null; } - if (input.logoUrl) { - const validation = validateBase64Image(input.logoUrl); - if (!validation.isValid) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `Invalid logo image: ${validation.error}`, - }); - } - - if ( - input.logoUrl.startsWith("data:image/png;base64,") || + if ( + input.logoUrl && + (input.logoUrl.startsWith("data:image/png;base64,") || input.logoUrl.startsWith("data:image/jpeg;base64,") || - input.logoUrl.startsWith("data:image/jpg;base64,") || - input.logoUrl.startsWith("data:image/svg+xml;base64,") - ) { - try { - data.logoUrl = await uploadLogo({ - logo: await resizeBase64Image(input.logoUrl), - teamId: currentOrgId, - }); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: error instanceof Error ? error.message : "Failed to upload logo", - }); - } - } else { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Unsupported logo format. Please use PNG, JPEG, or SVG.", - }); - } + input.logoUrl.startsWith("data:image/jpg;base64,")) + ) { + data.logoUrl = await uploadLogo({ + logo: await resizeBase64Image(input.logoUrl), + teamId: currentOrgId, + }); } if (input.slug) { @@ -266,10 +220,12 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { /** If the team doesn't have a slug we can assume that it hasn't been published yet. */ !prevOrganisation.slug ) { + // Save it on the metadata so we can use it later data.metadata = mergeMetadata({ requestedSlug: input.slug }); } else { data.slug = input.slug; data.metadata = mergeMetadata({ + // If we save slug, we don't need the requestedSlug anymore requestedSlug: undefined, ...input.metadata, }); diff --git a/packages/trpc/server/routers/viewer/organizations/updateUser.handler.ts b/packages/trpc/server/routers/viewer/organizations/updateUser.handler.ts index 334e7a50da..901abbe1eb 100644 --- a/packages/trpc/server/routers/viewer/organizations/updateUser.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/updateUser.handler.ts @@ -6,7 +6,6 @@ import { RoleManagementError } from "@calcom/features/pbac/domain/errors/role-ma import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory"; import { uploadAvatar } from "@calcom/lib/server/avatar"; import { checkRegularUsername } from "@calcom/lib/server/checkRegularUsername"; -import { validateBase64Image } from "@calcom/lib/server/imageValidation"; import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image"; import { prisma } from "@calcom/prisma"; import type { MembershipRole } from "@calcom/prisma/enums"; @@ -117,41 +116,20 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => { timeZone: input.timeZone, }; - if (input.avatar) { - const validation = validateBase64Image(input.avatar); - if (!validation.isValid) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `Invalid avatar image: ${validation.error}`, - }); - } - - if ( - input.avatar.startsWith("data:image/png;base64,") || + if ( + input.avatar && + (input.avatar.startsWith("data:image/png;base64,") || input.avatar.startsWith("data:image/jpeg;base64,") || - input.avatar.startsWith("data:image/jpg;base64,") || - input.avatar.startsWith("data:image/svg+xml;base64,") - ) { - try { - const avatar = await resizeBase64Image(input.avatar); - data.avatarUrl = await uploadAvatar({ - avatar, - userId: user.id, - }); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: error instanceof Error ? error.message : "Failed to upload avatar", - }); - } - } else if (input.avatar === "") { - data.avatarUrl = null; - } else { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Unsupported image format. Please use PNG, JPEG, or SVG.", - }); - } + input.avatar.startsWith("data:image/jpg;base64,")) + ) { + const avatar = await resizeBase64Image(input.avatar); + data.avatarUrl = await uploadAvatar({ + avatar, + userId: user.id, + }); + } + if (input.avatar === "") { + data.avatarUrl = null; } // Update user diff --git a/packages/ui/components/image-uploader/BannerUploader.tsx b/packages/ui/components/image-uploader/BannerUploader.tsx index dbb6b37a1a..458ae8e94d 100644 --- a/packages/ui/components/image-uploader/BannerUploader.tsx +++ b/packages/ui/components/image-uploader/BannerUploader.tsx @@ -4,7 +4,6 @@ import { useCallback, useState, useEffect } from "react"; import Cropper from "react-easy-crop"; import checkIfItFallbackImage from "@calcom/lib/checkIfItFallbackImage"; -import { MAX_BANNER_SIZE } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { ButtonColor } from "../button"; @@ -13,7 +12,6 @@ import { Dialog, DialogClose, DialogContent, DialogTrigger, DialogFooter } from import { showToast } from "../toast"; import { useFileReader, createImage, Slider } from "./Common"; import type { FileEvent, Area } from "./Common"; -import { validateImageFile } from "./imageValidation"; type BannerUploaderProps = { id: string; @@ -92,21 +90,14 @@ export default function BannerUploader({ return; } + const limit = 5 * 1000000; // max limit 5mb const file = e.target.files[0]; - const validation = await validateImageFile(file, MAX_BANNER_SIZE); - - if (!validation.isValid) { - if (validation.errorKey && validation.errorParams) { - showToast(t(validation.errorKey, validation.errorParams) as string, "error"); - } else if (validation.error) { - showToast(t(validation.error), "error"); - } - e.target.value = ""; - return; + if (file.size > limit) { + showToast(t("image_size_limit_exceed"), "error"); + } else { + setFile(file); } - - setFile(file); }; const showCroppedImage = useCallback( @@ -184,7 +175,7 @@ export default function BannerUploader({ name={id} placeholder={t("upload_image")} className="text-default pointer-events-none absolute mt-4 opacity-0 " - accept="image/jpeg,image/jpg,image/png,image/gif,image/webp,image/bmp,image/x-icon,image/svg+xml" + accept="image/*" /> {t("choose_a_file")} diff --git a/packages/ui/components/image-uploader/ImageUploader.tsx b/packages/ui/components/image-uploader/ImageUploader.tsx index 4567854cf3..12ab7e0c67 100644 --- a/packages/ui/components/image-uploader/ImageUploader.tsx +++ b/packages/ui/components/image-uploader/ImageUploader.tsx @@ -4,7 +4,6 @@ import { useCallback, useState } from "react"; import Cropper from "react-easy-crop"; import checkIfItFallbackImage from "@calcom/lib/checkIfItFallbackImage"; -import { MAX_BANNER_SIZE } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { ButtonColor, ButtonProps } from "../button"; @@ -13,7 +12,6 @@ import { Dialog, DialogClose, DialogContent, DialogTrigger, DialogFooter } from import { showToast } from "../toast"; import { useFileReader, createImage, Slider } from "./Common"; import type { FileEvent, Area } from "./Common"; -import { validateImageFile } from "./imageValidation"; const MAX_IMAGE_SIZE = 512; @@ -90,26 +88,19 @@ export default function ImageUploader({ method: "readAsDataURL", }); - const onInputFile = async (e: FileEvent) => { + const onInputFile = (e: FileEvent) => { if (!e.target.files?.length) { return; } + const limit = 5 * 1000000; // max limit 5mb const file = e.target.files[0]; - const validation = await validateImageFile(file, MAX_BANNER_SIZE); - - if (!validation.isValid) { - if (validation.errorKey && validation.errorParams) { - showToast(t(validation.errorKey, validation.errorParams) as string, "error"); - } else if (validation.error) { - showToast(t(validation.error), "error"); - } - e.target.value = ""; - return; + if (file.size > limit) { + showToast(t("image_size_limit_exceed"), "error"); + } else { + setFile(file); } - - setFile(file); }; const showCroppedImage = useCallback( @@ -172,7 +163,7 @@ export default function ImageUploader({ name={id} placeholder={t("upload_image")} className="text-default pointer-events-none absolute mt-4 opacity-0 " - accept="image/jpeg,image/jpg,image/png,image/gif,image/webp,image/bmp,image/x-icon,image/svg+xml" + accept="image/*" /> {t("choose_a_file")} diff --git a/packages/ui/components/image-uploader/imageValidation.test.ts b/packages/ui/components/image-uploader/imageValidation.test.ts deleted file mode 100644 index 1713e19362..0000000000 --- a/packages/ui/components/image-uploader/imageValidation.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -import { MAX_BANNER_SIZE } from "@calcom/lib/constants"; - -import { validateImageFile, MAX_IMAGE_FILE_SIZE } from "./imageValidation"; - -describe("validateImageFile", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - const createMockFile = (bytes: number[], name = "test.png", type = "image/png", size?: number) => { - const buffer = new ArrayBuffer(bytes.length); - const view = new Uint8Array(buffer); - bytes.forEach((byte, index) => { - view[index] = byte; - }); - - const file = { - name, - type, - size: size !== undefined ? size : bytes.length, - arrayBuffer: vi.fn().mockResolvedValue(buffer), - } as unknown as File; - - return file; - }; - - describe("File size validation", () => { - it("should reject files larger than MAX_IMAGE_FILE_SIZE", async () => { - const largeFile = createMockFile([0x89, 0x50, 0x4e, 0x47], "large.png", "image/png", 6 * 1024 * 1024); - - const result = await validateImageFile(largeFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(false); - expect(result.error).toBe("image_size_limit_exceed"); - }); - - it("should accept files within size limit", async () => { - const validFile = createMockFile( - [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], - "valid.png", - "image/png", - 1024 - ); - - const result = await validateImageFile(validFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(true); - expect(result.error).toBeUndefined(); - }); - - it("should respect custom maxFileSize parameter", async () => { - const customMaxSize = 2 * 1024 * 1024; // 2MB - const largeFile = createMockFile([0x89, 0x50, 0x4e, 0x47], "large.png", "image/png", 3 * 1024 * 1024); - - const result = await validateImageFile(largeFile, customMaxSize); - - expect(result.isValid).toBe(false); - expect(result.error).toBe("image_size_limit_exceed"); - }); - }); - - describe("Magic number validation - dangerous files", () => { - it("should reject PDF files", async () => { - const pdfFile = createMockFile([0x25, 0x50, 0x44, 0x46], "fake.png", "image/png"); - - const result = await validateImageFile(pdfFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(false); - expect(result.errorKey).toBe("unsupported_file_type"); - expect(result.errorParams).toEqual({ type: "PDF" }); - }); - - it("should reject HTML files", async () => { - const htmlFile = createMockFile( - [0x3c, 0x21, 0x44, 0x4f, 0x43, 0x54, 0x59, 0x50, 0x45, 0x20], - "fake.png", - "image/png" - ); - - const result = await validateImageFile(htmlFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(false); - expect(result.errorKey).toBe("unsupported_file_type"); - expect(result.errorParams).toEqual({ type: "HTML" }); - }); - - it("should reject script files", async () => { - const scriptFile = createMockFile([0x3c, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74], "fake.png", "image/png"); - - const result = await validateImageFile(scriptFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(false); - expect(result.errorKey).toBe("unsupported_file_type"); - expect(result.errorParams).toEqual({ type: "Script" }); - }); - - it("should reject ZIP files", async () => { - const zipFile = createMockFile([0x50, 0x4b, 0x03, 0x04], "fake.png", "image/png"); - - const result = await validateImageFile(zipFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(false); - expect(result.errorKey).toBe("unsupported_file_type"); - expect(result.errorParams).toEqual({ type: "ZIP" }); - }); - - it("should reject executable files", async () => { - const exeFile = createMockFile([0x4d, 0x5a], "fake.png", "image/png"); - - const result = await validateImageFile(exeFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(false); - expect(result.errorKey).toBe("unsupported_file_type"); - expect(result.errorParams).toEqual({ type: "Executable" }); - }); - }); - - describe("Magic number validation - valid images", () => { - it("should accept PNG files", async () => { - const pngFile = createMockFile( - [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], - "test.png", - "image/png" - ); - - const result = await validateImageFile(pngFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(true); - expect(result.error).toBeUndefined(); - }); - - it("should accept JPEG files", async () => { - const jpegFile = createMockFile([0xff, 0xd8, 0xff, 0xe0], "test.jpg", "image/jpeg"); - - const result = await validateImageFile(jpegFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(true); - expect(result.error).toBeUndefined(); - }); - - it("should accept GIF files", async () => { - const gifFile = createMockFile([0x47, 0x49, 0x46, 0x38, 0x37, 0x61], "test.gif", "image/gif"); - - const result = await validateImageFile(gifFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(true); - expect(result.error).toBeUndefined(); - }); - - it("should accept WEBP files", async () => { - const webpFile = createMockFile( - [0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50], - "test.webp", - "image/webp" - ); - - const result = await validateImageFile(webpFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(true); - expect(result.error).toBeUndefined(); - }); - - it("should accept BMP files", async () => { - const bmpFile = createMockFile([0x42, 0x4d], "test.bmp", "image/bmp"); - - const result = await validateImageFile(bmpFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(true); - expect(result.error).toBeUndefined(); - }); - - it("should accept ICO files", async () => { - const icoFile = createMockFile([0x00, 0x00, 0x01, 0x00], "test.ico", "image/x-icon"); - - const result = await validateImageFile(icoFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(true); - expect(result.error).toBeUndefined(); - }); - }); - - describe("New error format validation", () => { - it("should return errorKey and errorParams for unsupported file types", async () => { - const pdfFile = createMockFile([0x25, 0x50, 0x44, 0x46], "fake.png", "image/png"); - - const result = await validateImageFile(pdfFile, MAX_IMAGE_FILE_SIZE); - - expect(result).toHaveProperty("isValid"); - expect(result).toHaveProperty("errorKey"); - expect(result).toHaveProperty("errorParams"); - expect(result.isValid).toBe(false); - expect(result.errorKey).toBe("unsupported_file_type"); - expect(result.errorParams).toEqual({ type: "PDF" }); - }); - - it("should return error field for backward compatibility when not using errorKey pattern", async () => { - const invalidFile = createMockFile([0x00, 0x01, 0x02, 0x03], "invalid.png", "image/png"); - - const result = await validateImageFile(invalidFile, MAX_IMAGE_FILE_SIZE); - - expect(result).toHaveProperty("isValid"); - expect(result).toHaveProperty("error"); - expect(result.isValid).toBe(false); - expect(result.error).toBe("invalid_image_file_format"); - expect(result.errorKey).toBeUndefined(); - expect(result.errorParams).toBeUndefined(); - }); - }); - - describe("Constants integration", () => { - it("should use MAX_BANNER_SIZE constant correctly", () => { - expect(MAX_IMAGE_FILE_SIZE).toBe(MAX_BANNER_SIZE); - expect(MAX_IMAGE_FILE_SIZE).toBe(5 * 1024 * 1024); // 5MB - }); - }); - - describe("Edge cases", () => { - it("should reject files with invalid image format", async () => { - const invalidFile = createMockFile([0x00, 0x01, 0x02, 0x03], "invalid.png", "image/png"); - - const result = await validateImageFile(invalidFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(false); - expect(result.error).toBe("invalid_image_file_format"); - }); - - it("should handle file reading errors gracefully", async () => { - const errorFile = createMockFile([0x89, 0x50, 0x4e, 0x47], "error.png", "image/png"); - - const mockArrayBuffer = vi.fn().mockRejectedValue(new Error("File read error")); - Object.defineProperty(errorFile, "arrayBuffer", { value: mockArrayBuffer }); - - const result = await validateImageFile(errorFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(false); - expect(result.error).toBe("failed_to_validate_image_file"); - }); - - it("should handle empty files", async () => { - const emptyFile = createMockFile([], "empty.png", "image/png"); - - const result = await validateImageFile(emptyFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(false); - expect(result.error).toBe("invalid_image_file_format"); - }); - - it("should handle files with insufficient bytes for magic number detection", async () => { - const shortFile = createMockFile([0x89], "short.png", "image/png"); - - const result = await validateImageFile(shortFile, MAX_IMAGE_FILE_SIZE); - - expect(result.isValid).toBe(false); - expect(result.error).toBe("invalid_image_file_format"); - }); - }); -}); diff --git a/packages/ui/components/image-uploader/imageValidation.ts b/packages/ui/components/image-uploader/imageValidation.ts deleted file mode 100644 index 25d1750c97..0000000000 --- a/packages/ui/components/image-uploader/imageValidation.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { MAX_BANNER_SIZE } from "@calcom/lib/constants"; -import { - FILE_SIGNATURES, - matchesSignature, - containsDangerousSVGContent, -} from "@calcom/lib/imageValidationConstants"; - -export const MAX_IMAGE_FILE_SIZE = MAX_BANNER_SIZE; - -const stripBomAndWhitespace = (data: Uint8Array): Uint8Array => { - let start = 0; - - if (data.length >= 3 && data[0] === 0xef && data[1] === 0xbb && data[2] === 0xbf) { - start = 3; - } - - while (start < data.length) { - const byte = data[start]; - if (byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d || byte === 0x0c || byte === 0x0b) { - start++; - } else { - break; - } - } - - return data.slice(start); -}; - -const checkTextBasedDangerousSignature = ( - data: Uint8Array -): { key: string; params: Record } | null => { - const text = new TextDecoder("utf-8", { fatal: false }).decode(data).toLowerCase(); - - if (text.includes(" { - let trimmedText = text.replace(/^[\uFEFF\s]+/, ""); - - if (trimmedText.length > maxLength) { - trimmedText = trimmedText.substring(0, maxLength); - } - - return new TextEncoder().encode(trimmedText); -}; - -export const validateImageFile = async ( - file: File, - maxFileSize: number = MAX_IMAGE_FILE_SIZE -): Promise<{ - isValid: boolean; - error?: string; - errorKey?: string; - errorParams?: Record; -}> => { - if (file.size > maxFileSize) { - return { isValid: false, error: "image_size_limit_exceed" }; - } - - try { - const arrayBuffer = await file.arrayBuffer(); - const uint8Array = new Uint8Array(arrayBuffer); - - if (uint8Array.length === 0) { - return { isValid: false, error: "invalid_image_file_format" }; - } - - const strippedData = stripBomAndWhitespace(uint8Array); - - if (strippedData.length === 0) { - return { isValid: false, error: "invalid_image_file_format" }; - } - - const trimmedHead = strippedData.slice(0, 512); - - const textBasedError = checkTextBasedDangerousSignature(trimmedHead); - if (textBasedError) { - return { isValid: false, errorKey: textBasedError.key, errorParams: textBasedError.params }; - } - - if (matchesSignature(trimmedHead, FILE_SIGNATURES.PDF)) { - return { isValid: false, errorKey: "unsupported_file_type", errorParams: { type: "PDF" } }; - } - - if (matchesSignature(trimmedHead, FILE_SIGNATURES.ZIP)) { - return { isValid: false, errorKey: "unsupported_file_type", errorParams: { type: "ZIP" } }; - } - - if (matchesSignature(trimmedHead, FILE_SIGNATURES.EXECUTABLE)) { - return { isValid: false, errorKey: "unsupported_file_type", errorParams: { type: "Executable" } }; - } - - const isValidImage = - matchesSignature(trimmedHead, FILE_SIGNATURES.PNG) || - matchesSignature(trimmedHead, FILE_SIGNATURES.JPEG_FF_D8_FF) || - matchesSignature(trimmedHead, FILE_SIGNATURES.GIF87a) || - matchesSignature(trimmedHead, FILE_SIGNATURES.GIF89a) || - matchesSignature(trimmedHead, FILE_SIGNATURES.BMP) || - matchesSignature(trimmedHead, FILE_SIGNATURES.ICO) || - (matchesSignature(trimmedHead, FILE_SIGNATURES.WEBP) && - trimmedHead.length >= 12 && - matchesSignature(trimmedHead.slice(8), FILE_SIGNATURES.WEBP_SIGNATURE)) || - (() => { - const fullText = new TextDecoder("utf-8", { fatal: false }).decode(strippedData); - - const svgTrimmedHead = createTrimmedTextHead(fullText); - - return ( - (matchesSignature(svgTrimmedHead, FILE_SIGNATURES.SVG) || - matchesSignature(svgTrimmedHead, FILE_SIGNATURES.SVG_DIRECT)) && - !containsDangerousSVGContent(fullText) - ); - })(); - - if (!isValidImage) { - return { isValid: false, error: "invalid_image_file_format" }; - } - - return { isValid: true }; - } catch (error) { - return { isValid: false, error: "failed_to_validate_image_file" }; - } -};