{"record":{"id":"5d49575346d07ca1","repo":"gotify/server","slug":"file-must-be-an-image","errorCode":null,"errorMessage":"file must be an image","messagePattern":"file must be an image","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"api/application.go","lineNumber":427,"sourceCode":"\twithID(ctx, \"id\", func(id uint) {\n\t\tapp, err := a.DB.GetApplicationByID(id)\n\t\tif success := successOrAbort(ctx, 500, err); !success {\n\t\t\treturn\n\t\t}\n\t\tif app != nil && app.UserID == auth.GetUserID(ctx) {\n\t\t\tfile, err := ctx.FormFile(\"file\")\n\t\t\tif err == http.ErrMissingFile {\n\t\t\t\tctx.AbortWithError(400, errors.New(\"file with key 'file' must be present\"))\n\t\t\t\treturn\n\t\t\t} else if err != nil {\n\t\t\t\tctx.AbortWithError(500, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\thead := make([]byte, 261)\n\t\t\topen, _ := file.Open()\n\t\t\topen.Read(head)\n\t\t\tif !filetype.IsImage(head) {\n\t\t\t\tctx.AbortWithError(400, errors.New(\"file must be an image\"))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\text := filepath.Ext(file.Filename)\n\t\t\tif !ValidApplicationImageExt(ext) {\n\t\t\t\tctx.AbortWithError(400, errors.New(\"invalid file extension\"))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tname := generateNonExistingImageName(a.ImageDir, func() string {\n\t\t\t\treturn generateImageName() + ext\n\t\t\t})\n\n\t\t\terr = ctx.SaveUploadedFile(file, a.ImageDir+name)\n\t\t\tif err != nil {\n\t\t\t\tctx.AbortWithError(500, err)\n\t\t\t\treturn\n\t\t\t}","sourceCodeStart":409,"sourceCodeEnd":445,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/api/application.go#L409-L445","documentation":"After reading the first 261 bytes of the uploaded file, the handler runs filetype.IsImage(head) to sniff the magic bytes. If the content does not match a known image format, it rejects the upload with this 400 error — content sniffing takes precedence over the client-declared filename or MIME type.","triggerScenarios":"Uploading a file whose actual bytes are not an image (PDF, text, zip, executable) to the image-upload endpoint; also occurs when the file is smaller than the sniffed header or corrupted so magic bytes are unreadable.","commonSituations":"Renaming evil.txt to evil.png and uploading; uploading SVG/HEIC/other formats not recognized by the filetype library; empty or truncated files; build artifacts accidentally attached.","solutions":["Upload an actual image file in a supported format (png, jpg, gif, webp, etc.)","Do not rely on renaming the extension — the content must genuinely be image bytes","Re-export/convert the file to PNG or JPEG if using an exotic format the sniffer rejects","If it is a valid image that fails, check the file is not truncated/empty and that the client is not sending only a partial body"],"exampleFix":"// before\nfd.append('file', new Blob(['not an image'], {type:'image/png'}));\n// after\nfd.append('file', file, 'photo.png'); // real image bytes from input[type=file]","handlingStrategy":"validation","validationCode":"const isImage = (f) => f.type.startsWith('image/') && f.size > 0 && !f.name.endsWith('.svg');\nif (!isImage(file)) throw new Error('upload must be a real image file');","typeGuard":"function isUploadableImage(f) {\n  return f instanceof File && f.size > 0 && ['image/png','image/jpeg','image/gif','image/webp'].includes(f.type);\n}","tryCatchPattern":null,"preventionTips":["Validate MIME type and magic bytes client-side before uploading","Never rename non-image files to image extensions expecting the server to accept them","Check the server's supported formats (filetype lib) — avoid svg/heic unless confirmed","Reject empty (0-byte) files before upload"],"tags":["http-400","file-upload","content-sniffing","validation","go"],"backgroundTag":"file-type-validation-failed","analyzedSha":"14bfc256276775c425f988d621dccfe705de18ac","analyzedAt":"2026-09-05T12:52:36.781Z","contentChangedAt":"2026-09-05T12:52:36.781Z","schemaVersion":2},"datasetVersion":"2026-09-12T17:17:11.597Z"}