diff --git a/dms/internal/upload/service.go b/dms/internal/upload/service.go index 89a4fa0e..2486a3e7 100644 --- a/dms/internal/upload/service.go +++ b/dms/internal/upload/service.go @@ -250,9 +250,10 @@ func (s *Service) Complete(ctx context.Context, sessionID string, confirmDuplica sniffPrefix = sniffPrefix[:n] // http.DetectContentType haengt bei Text immer "; charset=..." an - fuer // den Abgleich gegen die Positivliste (die reine Basistypen wie - // "text/plain" enthaelt) wird nur der Basistyp verglichen. + // "text/plain" enthaelt) wird nur der Basistyp verglichen; ob er zum + // deklarierten Typ passt, entscheidet Validator.ValidateSniffed. sniffedType, _, _ := strings.Cut(http.DetectContentType(sniffPrefix), ";") - if sniffErr := s.validator.ValidateMimeType(strings.TrimSpace(sniffedType)); sniffErr != nil { + if sniffErr := s.validator.ValidateSniffed(strings.TrimSpace(sniffedType), sess.MimeType); sniffErr != nil { return "", "", fmt.Errorf("upload: tatsaechlicher dateiinhalt entspricht nicht dem erlaubten typ: %w", sniffErr) } diff --git a/dms/internal/upload/validate.go b/dms/internal/upload/validate.go index 39786190..f27b9ba0 100644 --- a/dms/internal/upload/validate.go +++ b/dms/internal/upload/validate.go @@ -7,6 +7,7 @@ package upload import ( "errors" "fmt" + "strings" ) // ErrFileTooLarge wird geliefert, wenn die angekündigte oder tatsächliche @@ -57,6 +58,50 @@ func (v *Validator) ValidateMimeType(mimeType string) error { return nil } +// sniffAccepts beschreibt, welche deklarierten Typen zu einem von +// http.DetectContentType gelieferten Basistyp passen. Die Funktion kennt nur +// ein festes Signaturset: OOXML/ODF melden sich als application/zip, TIFF und +// alte Office-Formate als application/octet-stream, SVG/XML als text/xml. +var sniffAccepts = map[string]func(declared string) bool{ + "application/zip": func(d string) bool { + return d == "application/zip" || + strings.HasPrefix(d, "application/vnd.openxmlformats-officedocument.") || + strings.HasPrefix(d, "application/vnd.oasis.opendocument.") + }, + "application/octet-stream": func(d string) bool { + switch d { + case "application/octet-stream", "image/tiff", "application/msword", + "application/vnd.ms-excel", "application/vnd.ms-powerpoint": + return true + } + return false + }, + // Reiner Text ohne Signatur ist unter jedem deklarierten Typ plausibel (CSV, JSON, Quelltext). + "text/plain": func(string) bool { return true }, + "text/xml": func(d string) bool { + switch d { + case "text/xml", "application/xml", "image/svg+xml", "text/plain": + return true + } + return false + }, + "text/html": func(d string) bool { return d == "text/html" || d == "text/plain" }, +} + +// ValidateSniffed prueft den aus den tatsaechlichen Bytes erkannten Typ gegen +// den beim Start deklarierten (bereits gegen die Positivliste geprueften) Typ: +// gleicher Typ oder eine bekannte, vertraegliche Zuordnung. Ohne Positivliste +// wird nicht gefiltert. +func (v *Validator) ValidateSniffed(sniffed, declared string) error { + if len(v.allowedMimeTypes) == 0 || sniffed == declared { + return nil + } + if accepts, ok := sniffAccepts[sniffed]; ok && accepts(declared) { + return nil + } + return fmt.Errorf("%w: inhalt wirkt wie %q, deklariert ist %q", ErrDisallowedMimeType, sniffed, declared) +} + func (v *Validator) ValidateSize(sizeBytes int64) error { if sizeBytes <= 0 { return ErrInvalidSize diff --git a/dms/internal/upload/validate_sniff_test.go b/dms/internal/upload/validate_sniff_test.go new file mode 100644 index 00000000..14b42f0d --- /dev/null +++ b/dms/internal/upload/validate_sniff_test.go @@ -0,0 +1,42 @@ +package upload + +import ( + "errors" + "testing" +) + +func TestValidateSniffed(t *testing.T) { + v := NewValidator(1<<20, []string{"application/pdf", "text/plain"}) + docx := "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + cases := []struct { + sniffed, declared string + ok bool + }{ + {"application/pdf", "application/pdf", true}, + {"application/zip", docx, true}, + {"application/zip", "application/pdf", false}, + {"application/octet-stream", "image/tiff", true}, + {"application/octet-stream", "application/pdf", false}, + {"text/plain", "text/csv", true}, + {"text/plain", "application/pdf", true}, + {"text/html", "text/plain", true}, + {"text/html", "application/pdf", false}, + {"text/xml", "image/svg+xml", true}, + {"image/png", "application/pdf", false}, + } + for _, c := range cases { + err := v.ValidateSniffed(c.sniffed, c.declared) + if c.ok && err != nil { + t.Errorf("%s/%s: unerwartet abgelehnt: %v", c.sniffed, c.declared, err) + } + if !c.ok && !errors.Is(err, ErrDisallowedMimeType) { + t.Errorf("%s/%s: sollte mit ErrDisallowedMimeType abgelehnt werden, bekam %v", c.sniffed, c.declared, err) + } + } +} + +func TestValidateSniffed_OhnePositivlisteKeinFilter(t *testing.T) { + if err := NewValidator(1<<20, nil).ValidateSniffed("application/zip", "application/pdf"); err != nil { + t.Fatalf("ohne Positivliste darf nicht gefiltert werden: %v", err) + } +}