|
|
@@ -2,9 +2,12 @@
|
|
|
package handlers
|
|
|
|
|
|
import (
|
|
|
+ "bytes"
|
|
|
"context"
|
|
|
"encoding/json"
|
|
|
+ "errors"
|
|
|
"fmt"
|
|
|
+ "io"
|
|
|
"net/http"
|
|
|
"net/url"
|
|
|
"strings"
|
|
|
@@ -75,6 +78,14 @@ func (h *UploadHandler) UploadFile(w http.ResponseWriter, r *http.Request) {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+ // Заголовок Content-Type подделывается клиентом — проверяем реальную
|
|
|
+ // сигнатуру файла по первым байтам, чтобы нельзя было залить произвольный
|
|
|
+ // контент под видом изображения.
|
|
|
+ if err := verifyImageSignature(file, contentType); err != nil {
|
|
|
+ writeError(w, http.StatusBadRequest, "file content does not match declared image type", err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
ext := ".jpg"
|
|
|
switch contentType {
|
|
|
case "image/png":
|
|
|
@@ -161,6 +172,49 @@ func (h *UploadHandler) PresignedURL(w http.ResponseWriter, r *http.Request) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+// verifyImageSignature читает первые байты файла и проверяет, что реальная
|
|
|
+// сигнатура соответствует заявленному content-type. После проверки возвращает
|
|
|
+// курсор файла в начало, чтобы последующая загрузка читала его целиком.
|
|
|
+func verifyImageSignature(file io.ReadSeeker, declaredType string) error {
|
|
|
+ head := make([]byte, 512)
|
|
|
+ n, err := io.ReadFull(file, head)
|
|
|
+ if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF {
|
|
|
+ return fmt.Errorf("read file header: %w", err)
|
|
|
+ }
|
|
|
+ head = head[:n]
|
|
|
+ if _, err := file.Seek(0, io.SeekStart); err != nil {
|
|
|
+ return fmt.Errorf("seek file start: %w", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ if !matchesImageType(head, declaredType) {
|
|
|
+ return errors.New("file signature mismatch")
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// matchesImageType сверяет байтовую сигнатуру с заявленным типом.
|
|
|
+// net/http.DetectContentType умеет распознавать jpeg/png/webp, но не heic,
|
|
|
+// поэтому для heic проверяем ISO-BMFF бокс ftyp вручную.
|
|
|
+func matchesImageType(head []byte, declaredType string) bool {
|
|
|
+ if declaredType == "image/heic" {
|
|
|
+ return isHEIC(head)
|
|
|
+ }
|
|
|
+ return http.DetectContentType(head) == declaredType
|
|
|
+}
|
|
|
+
|
|
|
+// isHEIC проверяет, что файл — ISO Base Media File Format с HEIF-брендом.
|
|
|
+func isHEIC(head []byte) bool {
|
|
|
+ if len(head) < 12 || !bytes.Equal(head[4:8], []byte("ftyp")) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ brand := string(head[8:12])
|
|
|
+ switch brand {
|
|
|
+ case "heic", "heix", "hevc", "heim", "heis", "mif1", "msf1":
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ return false
|
|
|
+}
|
|
|
+
|
|
|
func (h *UploadHandler) DeleteObjects(ctx context.Context, urls []string) error {
|
|
|
prefix := h.publicEndpoint + "/" + h.bucket + "/"
|
|
|
for _, fileURL := range urls {
|