# Django/Security afbeelding upload, zomaar accepteren kan server laten crashen: een perfecte gids voor beveiliging en efficiëntie In webdiensten is afbeelding uploaden een "altijd aanwezige functie" en lijkt het makkelijk te implementeren. Maar het uploadend punt is **de meest directe doorgang voor externe data naar de server** en voor aanvallers de goedkoopste manier om maximale schade toe te brengen. (webshell upload, afbeelding parser kwetsbaarheden, DoS, etc.) Deze tekst legt uit hoe je het ogenschijnlijk tegenstrijdige doel van "beveiliging obsessief, middelen economisch" **beide** behaalt. De kern is simpel. * **In de verificatie stap lees je zo min mogelijk** (geen decodering, alleen header/metadata) * **In de opslag stap maak je opnieuw** (Transcoding voor Sanitizing) --- ## De essentie van upload beveiliging: "Vertrouw niet, wacht tot het nodig is" {#sec-98341e6a33e1} Bij uploads kunnen we bijna niets vertrouwen. * Bestandnaam: kan door de gebruiker gewijzigd worden * Extensie: kan vrij gewijzigd worden * Content-Type: wordt door de client meegegeven * Bestandinhoud: wordt door de aanvaller gemaakt Daarom convergeren de strategieën naar twee: 1. **Snel en goedkoop filteren** (cheap checks) 2. **De definitieve versie wordt altijd door de server gemaakt** (server-generated artifact) --- ## Veelvoorkomende misvatting: "Om alles te controleren moet je alles lezen?" {#sec-f9b6b6b99eb2} Een veelgemaakte fout van ontwikkelaars die beveiliging serieus nemen is het toevoegen van code zoals hieronder in de verificatie stap. ```python img = Image.open(file) img.verify() # of img.load() ``` Het probleem is dat dit **de server middelen vooraf aan de aanvaller levert**. ### Waarom gevaarlijk? {#sec-7ed56ee48a1d} * **Compressie bom (Decompression Bomb)** Op het eerste gezicht een paar MB, maar na decodering kan het tientallen GB worden. `load()` decodeert daadwerkelijk de pixels en kan geheugen/CPU in een oogwenk opruimen, wat leidt tot DoS. * **Onnodige I/O** `verify()` leest het bestand tot het einde, wat I/O kost en vaak een `seek(0)` of heropenen vereist. **Conclusie:** In de upload "verificatie" stap decodeer je geen pixels. **Header + metadata + resolutie beperking** is voldoende voor een eerste verdedigingslaag. --- ## Bescherming is geen enkele lijn: 3‑stappen Defense in Depth {#sec-5756153c302b} Afbeelding uploaden kan niet met een simpele "if" line worden opgelost. Een realistische balans vereist lagen. ### 1. Extensies zijn leugens — MIME bepalen via Magic Number {#sec-bbad131e1332} Een bestand met de naam `profile.png` zegt niets. Je moet het **signatuur (Magic Number)** lezen om het echte type te bevestigen. * Lees het hele bestand niet. **Eerste 1–2KB volstaat**. * Voorbeeldbibliotheek: `python-magic` (libmagic gebaseerd) ### 2. Pillow is "alleen openen" nog veilig — Lazy Loading voor resolutie beperking {#sec-f0a8fb4bda8a} `Image.open()` van Pillow laadt meestal **niet meteen pixels** maar alleen de header. Deze eigenschap kun je gebruiken om, voordat een decodering/ geheugen bom ontstaat, te blokkeren op **resolutie (pixel aantal)**. * Controleer: `width * height <= MAX_PIXELS` * Punt: gebruik `size` zonder `load()`/`verify()` ### 3. De beste sanering is "opnieuw tekenen" — Transcoding voor Sanitizing {#sec-398d6ae888a9} Dit is het belangrijkste principe. > Sla het origineel niet op. Afbeeldingen kunnen metadata (EXIF), profiel, Slack ruimte, parser trucs, etc. bevatten in **niet‑pixel gebieden**. Door alleen pixel data te extraheren en de server een nieuwe afbeelding te laten maken, worden veel van deze verborgen zaken automatisch verwijderd. * Aanbevolen: **WebP (of AVIF/JPEG)** om de server opnieuw te coderen en op te slaan * Effect: **Sanitizing + grootte optimalisatie + consistente formaatbeleid** --- ## Praktische implementatie: DRF Serializer (beveiliging + geheugen efficiëntie) {#sec-7b99e0ec685d} De onderstaande code bevat de filosofie "in de verificatie stap lees zo min mogelijk" en "in de opslag stap maak opnieuw". * `f.size` is metadata die Django al kent, dus benut het * Magic Number leest **alleen het begin** * Pillow gebruikt `open()` voor **resolutie alleen** * Definitieve opslag is **Transcoding (WebP)** * Elke stap herstelt de file pointer met `seek(0)` ```python from io import BytesIO import magic from PIL import Image, ImageOps, UnidentifiedImageError from rest_framework import serializers # Beleid (pas aan op basis van service) MAX_SIZE = 5 * 1024 * 1024 # 5MB MAX_PIXELS = 4_194_304 # 2048 * 2048 ≈ 4MP ALLOWED_MIME = {"image/png", "image/jpeg", "image/webp"} class SecureImageUploadSerializer(serializers.Serializer): file = serializers.ImageField() def validate_file(self, f): # [1] Grootte beperking: goedkoopste en snelste filter if f.size > MAX_SIZE: raise serializers.ValidationError("Bestand is te groot.") # [2] Magic Number voor MIME: vertrouw niet op extensie/Content-Type f.seek(0) head = f.read(2048) # alleen het begin f.seek(0) mime = magic.from_buffer(head, mime=True) if mime not in ALLOWED_MIME: raise serializers.ValidationError("Ondersteunde bestandsindeling niet gevonden.") # [3] Resolutie beperking: zonder load/verify alleen header op basis van size try: with Image.open(f) as img: w, h = img.size if (w * h) > MAX_PIXELS: raise serializers.ValidationError("Afbeelding resolutie is te groot.") except UnidentifiedImageError: raise serializers.ValidationError("Ongeldige afbeelding.") except Exception: raise serializers.ValidationError("Fout tijdens afbeelding verificatie.") finally: f.seek(0) return f def create(self, validated_data): f = validated_data["file"] # Definitieve versie wordt altijd door de server gemaakt (Sanitizing) try: with Image.open(f) as img: # EXIF rotatie correctie (belangrijk bij mobiele uploads) img = ImageOps.exif_transpose(img) # Veilig en consistent kleur ruimte normaliseren if img.mode not in ("RGB", "RGBA"): img = img.convert("RGB") out = BytesIO() img.save(out, format="WEBP", quality=85, method=6) out.seek(0) safe_bytes = out.getvalue() # Hier sla je safe_bytes op in je opslag # - Naam randomiseren (UUID) # - Directory sharding (bijv. ab/cd/uuid.webp) # - In DB sla je alleen de server gegenereerde sleutel op return safe_bytes except Exception: raise serializers.ValidationError("Fout tijdens afbeelding verwerking.") ``` --- ## "Obsessieve beveiliging" vs "middelen economisch" in balans brengen {#sec-7ec5087de2e9} De cruciale balans hier is: ### In de verificatie stap niet te veel eisen {#sec-5c32bd66af4d} De verificatie stap is het meest beladen met verkeer en het gebied waar aanvallers zonder kosten herhaaldelijk kunnen aanroepen. Als je hier een "duurzame operatie" zoals `load()` uitvoert, kunnen aanvallers de server kosten laten oplopen. * ✅ Grootte beperking / header MIME / resolutie beperking * ❌ Pixel decodering forceren / volledige bestand lezen / meerdere heropenen ### De echte veiligheid ontstaat in de opslag stap {#sec-a49dab56fd86} Verificatie is een "filteren" stap, opslag is een "standariseren" stap. Als de server een **nieuwe byte stream** genereert, wordt beveiliging en beheer eenvoudiger. * Formaat uniform → vereenvoudigt cache strategie / thumbnail pipeline * Metadata opruimen → helpt bij het verwijderen van persoonlijke gegevens (EXIF GPS) * Vermindert kans op kwaadaardige payloads --- ## Als je deze punten volgt, is het nog beter! {#sec-733fb04d797a} ![Afbeelding die kwaadaardige upload beschermt](/media/editor_temp/6/60e76b29-7657-4042-8e5b-3b7b2670a4c7.png) * **Bestandsnaam vertrouw je nooit, maak een server‑gegenereerde naam** (UUID aanbevolen) * **Overweeg een architectuur waarbij de app server de upload niet direct ontvangt** (bij grote services direct upload naar object storage via presigned URL + asynchrone verificatie/transformatie) * **Verwerk tijdslimiet/worker isolatie** Afbeelding transformatie gebruikt CPU. Houd de web request‑response route kort en verplaats naar een worker/queue. * **Log/metrics** Verwijderingsreden (MIME mismatch, resolutie overschrijding, grootte overschrijding) tellen geeft snel inzicht in aanvalspatronen. --- ## Samenvattende checklist {#sec-ab5708035e92} 1. **Laad het volledige bestand niet in geheugen.** `read()` alleen het begin, de rest streaming/ bestand object. 2. **Vertrouw niet op extensie/Content-Type, gebruik Magic Number voor MIME** 3. **In de verificatie stap decodeer je geen pixels, controleer alleen resolutie** 4. **Sla het origineel niet op, maak een nieuwe afbeelding via Transcoding** 5. **Herstel de file pointer met `seek(0)` na elke stap** Upload beveiliging begint met een sceptische houding tegenover gebruikers, maar upload prestaties worden bereikt door een goed begrip van hoe het systeem werkt. Beide aspecten moeten samenkomen om een veilige upload poort te creëren. --- **Bekijk ook gerelateerde posts** - [[Web uploaden van bestanden vereenvoudigen] Dropzone.js volledig beheers gids](/ko/whitedec/2025/12/24/dropzone-js-complete-guide/)