Files
patrickandClaude Sonnet 5.5 4e16d8b155
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
DB-Integrationstests / db-tests (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
dms/upload: Sniff-Ergebnis gegen deklarierten Typ pruefen statt gegen die Positivliste
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 10:26:14 +01:00

117 lines
4.4 KiB
Go

// Package upload implementiert DOC-01: fortsetzbare Server-seitige
// Dateiaufnahme mit Größen-/MIME-Validierung und transaktionaler
// Dokument+Revision-Anlage. Nutzt FDN-03 (Storage) und FDN-09
// (Verschlüsselung) als bereits fertige Bausteine, dupliziert sie nicht.
package upload
import (
"errors"
"fmt"
"strings"
)
// ErrFileTooLarge wird geliefert, wenn die angekündigte oder tatsächliche
// Größe das konfigurierte Limit überschreitet (Schutz vor
// Speicherbomben — siehe "Bekannte Fehler vermeiden" im Ticket).
var ErrFileTooLarge = errors.New("upload: datei ueberschreitet das erlaubte groessenlimit")
// ErrInvalidSize wird geliefert, wenn die angekuendigte Groesse nicht positiv ist.
var ErrInvalidSize = errors.New("upload: ungueltige groesse")
// ErrIncompleteUpload wird geliefert, wenn Complete vor dem vollstaendigen Empfang aufgerufen wird.
var ErrIncompleteUpload = errors.New("upload: sitzung unvollstaendig")
// ErrInvalidOffset wird geliefert, wenn ein Chunk ausserhalb der angekuendigten Groesse beginnt.
var ErrInvalidOffset = errors.New("upload: ungueltiger offset")
// ErrDisallowedMimeType wird geliefert, wenn der MIME-Typ nicht auf der
// Positivliste steht (Akzeptanzkriterium 2).
var ErrDisallowedMimeType = errors.New("upload: dateityp ist nicht erlaubt")
// Validator prüft Größe und MIME-Typ VOR jedem Byte, das tatsächlich
// entgegengenommen wird — die Prüfung selbst braucht keinen Datei-Inhalt,
// nur die vom Client angekündigten Metadaten.
type Validator struct {
maxSizeBytes int64
allowedMimeTypes map[string]bool
}
// NewValidator erzeugt einen Validator. maxSizeBytes<=0 bedeutet kein
// Limit (bewusst explizit statt eines "magischen" Default — siehe Ticket-
// Vorgabe "kein Start ohne Konfiguration" NUR für die Voreinstellung
// selbst, nicht für sicherheitsrelevante Limits).
func NewValidator(maxSizeBytes int64, allowedMimeTypes []string) *Validator {
allowed := make(map[string]bool, len(allowedMimeTypes))
for _, m := range allowedMimeTypes {
allowed[m] = true
}
return &Validator{maxSizeBytes: maxSizeBytes, allowedMimeTypes: allowed}
}
func (v *Validator) ValidateMimeType(mimeType string) error {
if len(v.allowedMimeTypes) == 0 {
return nil
}
if !v.allowedMimeTypes[mimeType] {
return fmt.Errorf("%w: %q", ErrDisallowedMimeType, mimeType)
}
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
}
if v.maxSizeBytes <= 0 {
return nil
}
if sizeBytes > v.maxSizeBytes {
return fmt.Errorf("%w: %d bytes > limit %d bytes", ErrFileTooLarge, sizeBytes, v.maxSizeBytes)
}
return nil
}