From c9b64d9c1a8744c9ee5e068366a500ae0dab36bc Mon Sep 17 00:00:00 2001
From: Paul Nothaft <53005142+the-luap@users.noreply.github.com>
Date: Fri, 17 Jul 2026 22:06:40 +0200
Subject: [PATCH] fix(uploads): register HEIC/HEIF with the file validator +
fix admin format hint (codex review of #832)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two findings from the Codex review:
- validateFileType requires an ALLOWED_MEDIA_TYPES entry, which had neither
image/heic nor image/heif — so HEIC was rejected before sharp ever saw it,
despite the EXTENSION_TO_MIME additions. Added both with a single 'ftyp'
(offset 4) magic number (the check is .every, so alternatives can't be
separate entries).
- Changing the shared upload.fileRequirements string to interpolate {{formats}}
left the admin PhotoUpload caller passing only { limit }, rendering the
placeholder literally (it was also already dropping {{sizeLimit}} from #823).
The admin caller now passes formats + sizeLimit + limit, from the admin
settings it already loads.
---
backend/src/utils/fileSecurityUtils.js | 16 ++++++++++++++++
frontend/src/components/admin/PhotoUpload.tsx | 13 +++++++++++--
2 files changed, 27 insertions(+), 2 deletions(-)
diff --git a/backend/src/utils/fileSecurityUtils.js b/backend/src/utils/fileSecurityUtils.js
index 39e59b70..28155b33 100644
--- a/backend/src/utils/fileSecurityUtils.js
+++ b/backend/src/utils/fileSecurityUtils.js
@@ -79,6 +79,22 @@ const ALLOWED_IMAGE_TYPES = {
extensions: ['.svg'],
// SVG files are XML-based text files, so we skip magic number validation
magicNumbers: null
+ },
+ // HEIC/HEIF (iPhone). ISO-BMFF container: bytes 4-7 are the "ftyp" box marker,
+ // present in every HEIF/HEIC file (single entry — the magic check is `.every`,
+ // so alternatives can't be listed as separate entries). Sharp's libvips
+ // decodes these; extension + MIME are already gated by validateFileType.
+ 'image/heic': {
+ extensions: ['.heic'],
+ magicNumbers: [
+ { offset: 4, bytes: [0x66, 0x74, 0x79, 0x70] } // "ftyp"
+ ]
+ },
+ 'image/heif': {
+ extensions: ['.heif'],
+ magicNumbers: [
+ { offset: 4, bytes: [0x66, 0x74, 0x79, 0x70] } // "ftyp"
+ ]
}
};
diff --git a/frontend/src/components/admin/PhotoUpload.tsx b/frontend/src/components/admin/PhotoUpload.tsx
index 7d3f0ea1..b3a7d0a1 100644
--- a/frontend/src/components/admin/PhotoUpload.tsx
+++ b/frontend/src/components/admin/PhotoUpload.tsx
@@ -8,7 +8,7 @@ import { useQuery } from '@tanstack/react-query';
import { categoriesService } from '../../services/categories.service';
import { settingsService } from '../../services/settings.service';
import { useTranslation } from 'react-i18next';
-import { extensionsToMimeTypes, extensionsToAcceptString } from '../../utils/fileTypes';
+import { extensionsToMimeTypes, extensionsToAcceptString, extensionsToLabel } from '../../utils/fileTypes';
import { useUploadProgress } from '../../hooks/useUploadProgress';
interface PhotoUploadProps {
@@ -118,6 +118,15 @@ export const PhotoUpload: React.FC
- {t('upload.fileRequirements', { limit: maxFilesPerUpload })} + {t('upload.fileRequirements', { formats: formatsLabel, limit: maxFilesPerUpload, sizeLimit: maxFileSizeMb })}