From 3b757170fe36ce61bd12df9faca96ea080a4759a Mon Sep 17 00:00:00 2001 From: Laurence Date: Wed, 5 Nov 2025 16:50:10 +0000 Subject: [PATCH 1/3] Add file type restriction to image upload input using accept attribute --- ui/src/application/Applications.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/application/Applications.tsx b/ui/src/application/Applications.tsx index e0d2be72..cb34de3e 100644 --- a/ui/src/application/Applications.tsx +++ b/ui/src/application/Applications.tsx @@ -128,6 +128,7 @@ const Applications = observer(() => { From fdaaa5afbcdaac1ce26694f062b9b8ed0e88c271 Mon Sep 17 00:00:00 2001 From: Laurence Date: Wed, 5 Nov 2025 17:32:36 +0000 Subject: [PATCH 2/3] Add dual validation: check both extension and MIME type - Validate file extension (.gif, .png, .jpg, .jpeg) to match backend - Also validate MIME type for defense in depth - Reuse validExtensions array for accept attribute --- ui/src/application/Applications.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ui/src/application/Applications.tsx b/ui/src/application/Applications.tsx index cb34de3e..11a295c1 100644 --- a/ui/src/application/Applications.tsx +++ b/ui/src/application/Applications.tsx @@ -59,6 +59,9 @@ const Applications = observer(() => { useEffect(() => void appStore.refresh(), []); + const validExtensions = ['.gif', '.png', '.jpg', '.jpeg']; + const validMimeTypes = ['image/gif', 'image/png', 'image/jpeg', 'image/jpg']; + const handleImageUploadClick = (id: number) => { uploadId.current = id; if (fileInputRef.current) { @@ -71,7 +74,13 @@ const Applications = observer(() => { if (!file) { return; } - if (['image/png', 'image/jpeg', 'image/gif'].indexOf(file.type) !== -1) { + const fileName = file.name.toLowerCase(); + const ext = fileName.substring(fileName.lastIndexOf('.')); + + const hasValidExtension = validExtensions.indexOf(ext) !== -1; + const hasValidMimeType = validMimeTypes.indexOf(file.type) !== -1; + + if (hasValidExtension && hasValidMimeType) { appStore.uploadImage(uploadId.current, file); } else { alert('Uploaded file must be of type png, jpeg or gif.'); @@ -128,7 +137,7 @@ const Applications = observer(() => { From ca205418af6c3402c0a5f32dfd76ea7a2e56c5a5 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Wed, 5 Nov 2025 14:26:04 -0600 Subject: [PATCH 3/3] align frontend and backend logic Signed-off-by: eternal-flame-AD --- api/application.go | 3 ++- ui/src/application/Applications.tsx | 13 +------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/api/application.go b/api/application.go index 805ae2b1..6a8ec8e7 100644 --- a/api/application.go +++ b/api/application.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "github.com/gin-gonic/gin" "github.com/gotify/server/v2/auth" @@ -459,7 +460,7 @@ func generateNonExistingImageName(imgDir string, gen func() string) string { } func ValidApplicationImageExt(ext string) bool { - switch ext { + switch strings.ToLower(ext) { case ".gif", ".png", ".jpg", ".jpeg": return true default: diff --git a/ui/src/application/Applications.tsx b/ui/src/application/Applications.tsx index 11a295c1..3ceff7ea 100644 --- a/ui/src/application/Applications.tsx +++ b/ui/src/application/Applications.tsx @@ -60,7 +60,6 @@ const Applications = observer(() => { useEffect(() => void appStore.refresh(), []); const validExtensions = ['.gif', '.png', '.jpg', '.jpeg']; - const validMimeTypes = ['image/gif', 'image/png', 'image/jpeg', 'image/jpg']; const handleImageUploadClick = (id: number) => { uploadId.current = id; @@ -74,17 +73,7 @@ const Applications = observer(() => { if (!file) { return; } - const fileName = file.name.toLowerCase(); - const ext = fileName.substring(fileName.lastIndexOf('.')); - - const hasValidExtension = validExtensions.indexOf(ext) !== -1; - const hasValidMimeType = validMimeTypes.indexOf(file.type) !== -1; - - if (hasValidExtension && hasValidMimeType) { - appStore.uploadImage(uploadId.current, file); - } else { - alert('Uploaded file must be of type png, jpeg or gif.'); - } + appStore.uploadImage(uploadId.current, file); }; return (