dms/upload: MIME-Sniff gegen deklarierten Typ statt gegen Positivliste (Office/TIFF-Uploads) #401

Merged
patrick merged 1 commits from fix/dms-upload-sniff into main 2026-10-28 11:32:04 +01:00
3 changed files with 90 additions and 2 deletions

No files matched your search

+3 -2
View File
@@ -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)
}
+45
View File
@@ -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
@@ -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)
}
}