snapshot: preserve upload hardening phase
This commit is contained in:
147
backend/workflows/upload_validation.py
Normal file
147
backend/workflows/upload_validation.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
def _header_matches(extension: str, header: bytes) -> bool:
|
||||
extension = extension.lower().lstrip(".")
|
||||
if extension == "png":
|
||||
return header.startswith(b"\x89PNG\r\n\x1a\n")
|
||||
if extension in {"jpg", "jpeg"}:
|
||||
return header.startswith(b"\xff\xd8\xff")
|
||||
if extension == "webp":
|
||||
return header.startswith(b"RIFF") and header[8:12] == b"WEBP"
|
||||
if extension == "pdf":
|
||||
return header.startswith(b"%PDF")
|
||||
if extension == "ico":
|
||||
return header.startswith(b"\x00\x00\x01\x00")
|
||||
if extension == "svg":
|
||||
text = header.decode("utf-8", errors="ignore").lower()
|
||||
return "<svg" in text
|
||||
return False
|
||||
|
||||
|
||||
def validate_uploaded_file(
|
||||
uploaded_file,
|
||||
*,
|
||||
allowed_extensions: set[str],
|
||||
max_size_bytes: int,
|
||||
allowed_content_types: set[str] | None = None,
|
||||
invalid_type_message: str,
|
||||
size_message: str,
|
||||
unreadable_message: str,
|
||||
) -> None:
|
||||
if not uploaded_file:
|
||||
return
|
||||
|
||||
if getattr(uploaded_file, "size", 0) > max_size_bytes:
|
||||
raise forms.ValidationError(size_message)
|
||||
|
||||
extension = Path(getattr(uploaded_file, "name", "")).suffix.lower().lstrip(".")
|
||||
if extension not in allowed_extensions:
|
||||
raise forms.ValidationError(invalid_type_message)
|
||||
|
||||
content_type = (getattr(uploaded_file, "content_type", "") or "").lower().strip()
|
||||
if allowed_content_types and content_type and content_type not in allowed_content_types:
|
||||
raise forms.ValidationError(invalid_type_message)
|
||||
|
||||
try:
|
||||
header = uploaded_file.read(512)
|
||||
uploaded_file.seek(0)
|
||||
except Exception as exc:
|
||||
raise forms.ValidationError(unreadable_message) from exc
|
||||
|
||||
if not _header_matches(extension, header):
|
||||
raise forms.ValidationError(invalid_type_message)
|
||||
|
||||
|
||||
def validate_avatar_upload(uploaded_file) -> None:
|
||||
validate_uploaded_file(
|
||||
uploaded_file,
|
||||
allowed_extensions={"png", "jpg", "jpeg", "webp", "svg"},
|
||||
max_size_bytes=5 * 1024 * 1024,
|
||||
allowed_content_types={
|
||||
"image/png",
|
||||
"image/x-png",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/pjpeg",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
},
|
||||
invalid_type_message=_("Bitte ein PNG-, JPG-, WEBP- oder SVG-Bild hochladen."),
|
||||
size_message=_("Das Profilbild darf maximal 5 MB groß sein."),
|
||||
unreadable_message=_("Die Bilddatei konnte nicht gelesen werden."),
|
||||
)
|
||||
|
||||
|
||||
def validate_logo_upload(uploaded_file) -> None:
|
||||
validate_uploaded_file(
|
||||
uploaded_file,
|
||||
allowed_extensions={"svg", "png", "jpg", "jpeg", "webp"},
|
||||
max_size_bytes=5 * 1024 * 1024,
|
||||
allowed_content_types={
|
||||
"image/png",
|
||||
"image/x-png",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/pjpeg",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
},
|
||||
invalid_type_message=_("Bitte ein SVG-, PNG-, JPG- oder WEBP-Bild hochladen."),
|
||||
size_message=_("Das Logo darf maximal 5 MB groß sein."),
|
||||
unreadable_message=_("Die Logo-Datei konnte nicht gelesen werden."),
|
||||
)
|
||||
|
||||
|
||||
def validate_favicon_upload(uploaded_file) -> None:
|
||||
validate_uploaded_file(
|
||||
uploaded_file,
|
||||
allowed_extensions={"ico", "png", "svg", "webp"},
|
||||
max_size_bytes=2 * 1024 * 1024,
|
||||
allowed_content_types={
|
||||
"image/x-icon",
|
||||
"image/vnd.microsoft.icon",
|
||||
"image/png",
|
||||
"image/x-png",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
},
|
||||
invalid_type_message=_("Bitte eine ICO-, PNG-, SVG- oder WEBP-Datei hochladen."),
|
||||
size_message=_("Das Favicon darf maximal 2 MB groß sein."),
|
||||
unreadable_message=_("Die Favicon-Datei konnte nicht gelesen werden."),
|
||||
)
|
||||
|
||||
|
||||
def validate_pdf_upload(uploaded_file) -> None:
|
||||
validate_uploaded_file(
|
||||
uploaded_file,
|
||||
allowed_extensions={"pdf"},
|
||||
max_size_bytes=10 * 1024 * 1024,
|
||||
allowed_content_types={"application/pdf"},
|
||||
invalid_type_message=_("Bitte eine gültige PDF-Datei hochladen."),
|
||||
size_message=_("Der PDF-Briefkopf darf maximal 10 MB groß sein."),
|
||||
unreadable_message=_("Die PDF-Datei konnte nicht gelesen werden."),
|
||||
)
|
||||
|
||||
|
||||
def validate_signature_upload(uploaded_file) -> None:
|
||||
validate_uploaded_file(
|
||||
uploaded_file,
|
||||
allowed_extensions={"png", "jpg", "jpeg"},
|
||||
max_size_bytes=4 * 1024 * 1024,
|
||||
allowed_content_types={
|
||||
"image/png",
|
||||
"image/x-png",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/pjpeg",
|
||||
},
|
||||
invalid_type_message=_("Bitte eine PNG- oder JPG-Datei hochladen."),
|
||||
size_message=_("Die Signatur-Datei ist zu groß (max. 4 MB)."),
|
||||
unreadable_message=_("Die Signatur-Datei konnte nicht gelesen werden."),
|
||||
)
|
||||
Reference in New Issue
Block a user