dms/upload: MIME-Sniff gegen deklarierten Typ statt gegen Positivliste (Office/TIFF-Uploads) #401
No files matched your search
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user