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
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
117 lines
4.4 KiB
Go
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
|
|
}
|