Actuele situatie: PHP2Shell. Twee kritieke PHP-kwetsbaarheden (CVSS 9.2) maken schadelijke code zonder inloggen mogelijk. Patches sinds 21-10-2026.Wat u nu moet doen →

Bestandsuploads in PHP veilig opzetten: PHP-uploadbeveiliging

Uploadformulieren zijn een veelgebruikte weg voor webshells. Zo controleert u bestandstypen goed, slaat u bestanden veilig op en voorkomt u dat de server geüploade scripts uitvoert.

Stand 6 min. leestijdGidsen

Kort gezegd: Bij PHP-uploadbeveiliging beslist niet één afzonderlijke controle, maar de combinatie: bestandstype aan serverzijde controleren, bestanden hernoemen, buiten de webroot opslaan en in de uploadmap geen scriptuitvoering toestaan. Een ongecontroleerd uploadformulier is een van de klassieke wegen waarlangs aanvallers een achterdeur op een webserver brengen.

Waarom uploadformulieren een geliefde toegangspoort zijn

Elk formulier dat bestanden aanneemt, schrijft vreemde gegevens naar uw server. Daartoe horen sollicitatieformulieren, klantportalen, beeldgalerijen, supportgebieden en veel extensies van CMS'en en webshops. Lukt het een aanvaller daar een bestand neer te zetten dat de webserver vervolgens als PHP uitvoert, dan heeft hij een permanente achterdeur.

Voor de directie betekent dit: een onschuldig ogende upload voor cv's kan in het ergste geval leiden tot overname van de hele webserver. Bovendien staan in uploadmappen vaak persoonsgegevens, die bij een verkeerde configuratie openbaar op te vragen zijn.

Voor beheerders en ontwikkelaars betekent dit: toepassingscode en serverconfiguratie moeten bij elkaar passen. Een zwakte op het ene niveau wordt vaak door het andere opgevangen als beide netjes zijn uitgevoerd. Hoe u reeds binnengebrachte bestanden vindt, beschrijft de gids Webshells herkennen en verwijderen.

De basisregels voor PHP-uploadbeveiliging

De volgende regels komen overeen met de aanbevelingen van de OWASP Cheat Sheets en hebben zich in de praktijk bewezen:

  • Toegestane lijst in plaats van blokkadelijst: Alleen uitdrukkelijk toegestane bestandstypen aannemen, bijvoorbeeld PDF, JPG en PNG. Blokkadelijsten voor afzonderlijke extensies zijn via varianten en dubbele extensies te omzeilen.
  • Inhoud controleren, niet alleen de extensie: Het werkelijke type aan serverzijde bepalen met finfo. Het MIME-type in $_FILES is een opgave van de afzender en niet te vertrouwen.
  • Nieuwe bestandsnaam: De originele naam nooit als bestandsnaam gebruiken. Een willekeurige naam genereren en de extensie uit de toegestane lijst toevoegen.
  • Opslag buiten de webroot: Bestanden zo opslaan dat ze niet rechtstreeks via een URL bereikbaar zijn. Uitgeleverd wordt via een script met bevoegdheidscontrole.
  • Grootte beperken: In de toepassing, in PHP en in de webserver.
  • Geen uitvoering: Moeten uploads toch in de webroot staan, schakel dan de scriptuitvoering in die map uit.
  • Controle alleen op de server: Controles in JavaScript in de browser verbeteren het gebruiksgemak, maar beschermen niet.

Een veilig voorbeeld met finfo en willekeurige naam

Het volgende voorbeeld toont de kern van een veilige verwerking. Het is bewust beknopt en moet in uw toepassing worden ingebed, dus met aanmelding, bevoegdheidscontrole, CSRF-bescherming en logging.

<?php
declare(strict_types=1);

const MAX_BYTES  = 5 * 1024 * 1024; // 5 MB
const ERLAUBT    = [
    'application/pdf' => 'pdf',
    'image/jpeg'      => 'jpg',
    'image/png'       => 'png',
];
const ZIELORDNER = '/var/www/daten/uploads'; // buiten de webroot

$datei = $_FILES['dokument'] ?? null;

if (!is_array($datei) || $datei['error'] !== UPLOAD_ERR_OK) {
    http_response_code(400);
    exit('Upload fehlgeschlagen.');
}

if ($datei['size'] > MAX_BYTES || !is_uploaded_file($datei['tmp_name'])) {
    http_response_code(400);
    exit('Datei ungültig.');
}

$finfo = new finfo(FILEINFO_MIME_TYPE);
$typ   = $finfo->file($datei['tmp_name']);

if (!isset(ERLAUBT[$typ])) {
    http_response_code(415);
    exit('Dateityp nicht erlaubt.');
}

$name = bin2hex(random_bytes(16)) . '.' . ERLAUBT[$typ];
$ziel = ZIELORDNER . '/' . $name;

if (!move_uploaded_file($datei['tmp_name'], $ziel)) {
    http_response_code(500);
    exit('Speichern fehlgeschlagen.');
}

chmod($ziel, 0640);
// De originele naam alleen als tekst in de database opslaan
// en bij elke uitvoer met htmlspecialchars() escapen.

Bij afbeeldingen loont een extra stap: de afbeelding met GD of Imagick opnieuw coderen en daarbij de metadata verwerpen. Dat vervangt de andere controles niet, maar verlaagt het restrisico door ingebedde vreemde gegevens.

Voor documenten uit onbekende bronnen, zoals sollicitaties of klachtenfoto's, is bovendien een virusscan vóór doorgifte zinvol. Die beschermt minder de webserver dan de medewerkers die de bestanden later op hun werkcomputers openen. Log bovendien elke upload met tijd, account, bestandsgrootte en herkend type, zodat ongebruikelijke patronen later na te gaan zijn.

Bij CMS'en en webshops brengen de ingebouwde mediabibliotheken meestal eigen controles mee. Riskanter zijn formulierextensies, importfuncties en zelf geprogrammeerde extra pagina's die bestanden langs de bibliotheek heen opslaan. Juist deze plekken moet u bij een controle als eerste bekijken.

Tip

Lever bestanden uit de beschermde map uit met Content-Disposition: attachment en X-Content-Type-Options: nosniff. Dan toont de browser problematische inhoud niet rechtstreeks, maar biedt hij die als download aan.

PHP-uitvoering in de uploadmap uitschakelen

Zelfs als een bestand met verkeerde inhoud erdoorheen glipt, mag de webserver het niet als script uitvoeren. Deze tweede beschermlaag hoort op elke server die uploads in de webroot opslaat.

Voor Nginx met PHP-FPM:

location ^~ /uploads/ {
    location ~* \.(php|phtml|phar)$ {
        deny all;
    }
}

Voor Apache, het best direct in de VirtualHost in plaats van in een .htaccess, zodat de regel niet door de toepassing kan worden overschreven:

<Directory "/var/www/html/uploads">
    AllowOverride None
    <FilesMatch "\.(php|phtml|phar)$">
        Require all denied
    </FilesMatch>
</Directory>

Draait PHP als Apache-module (mod_php), dan kunt u in hetzelfde blok aanvullend php_admin_flag engine off instellen. Controleer na de wijziging met een onschuldig testbestand dat alleen een tekst uitvoert, dat de server een verzoek daarop met 403 beantwoordt.

Groottegrenzen en het verband met PHP2Shell

Uploads hangen nauw samen met de PHP-instellingen post_max_size en upload_max_filesize. In het kader van PHP2Shell adviseert php.net als tussenmaatregel post_max_size op 8M te zetten als er geen uploads nodig zijn, of op 32M met uploads, en upload_max_filesize daaronder te houden. Dat is geen volledige bescherming en vervangt niet de update naar een verholpen PHP-versie.

; php.ini of FPM-pool (php_admin_value), herstart daarna alle workers
file_uploads        = On
post_max_size       = 32M
upload_max_filesize = 20M
max_file_uploads    = 5

Heeft een website helemaal geen uploads nodig, dan is file_uploads = Off de schoonste oplossing. Welke instellingen tot aan de patch zinvol zijn, staat onder Spoedmaatregelen zonder patch. Passende rechten voor uploadmappen beschrijft Bestandsrechten op de webserver.

Typische fouten in uploadfuncties

Bij het doorlopen van bestaande toepassingen duiken steeds dezelfde patronen op. Controleer uw formulieren gericht op deze punten:

  • De extensie wordt maar gedeeltelijk gecontroleerd, zodat bestandsnamen met meerdere extensies erdoorheen glippen.
  • De originele naam uit het formulier belandt in het opslagpad. Dat opent wegen voor padmanipulatie en het overschrijven van bestaande bestanden.
  • Uploads staan openbaar met voorspelbare namen. Bij sollicitaties of contracten is dat ook een privacyprobleem.
  • De uploadmap is voor de webserver beschrijfbaar en tegelijk uitvoerbaar.
  • Oude uploadextensies of formulier-plug-ins worden niet bijgewerkt, hoewel de fabrikant beveiligingsupdates heeft gepubliceerd.
  • Fouten worden niet gelogd. Geweigerde uploads zijn echter een vroeg signaal van gerichte pogingen.

Hoe helpt PHP SOS concreet?

  • We controleren uw uploadfuncties in de code en in de serverconfiguratie en benoemen concrete zwakke plekken met prioriteit.
  • We stellen de uitvoeringsblokkade in de uploadmap, passende rechten en groottegrenzen in binnen de serververharding.
  • Bij verdenking doorzoeken we uploadmappen op binnengebrachte scripts en ruimen die op via onze malwareverwijdering.
  • Op verzoek laten we de uitvoering onafhankelijk natrekken door een penetratietest.

Veelgestelde vragen

Volstaat het om de bestandsextensie bij de upload te controleren?

Nee. De extensie en het door de browser gemelde MIME-type komen van de afzender en zijn vrij te kiezen. Controleer de inhoud aan serverzijde, geef een nieuwe bestandsnaam en blokkeer scriptuitvoering in de uploadmap.

Waar moeten geüploade bestanden worden opgeslagen?

Het best buiten de openbare webmap. De aflevering neemt dan een script over dat vooraf controleert of de gebruiker het bestand mag zien. Zo zijn bestanden noch rechtstreeks op te vragen noch uit te voeren.

Zijn SVG-bestanden als afbeelding onschuldig?

Niet automatisch. SVG is XML en kan actieve inhoud bevatten. Sta SVG alleen toe als u de bestanden opschoont of uitsluitend als download met een passende Content-Disposition-header uitlevert.

Heeft de uploadgrootte iets met PHP2Shell te maken?

Ja, indirect. php.net adviseert als tussenmaatregel lagere waarden voor post_max_size en upload_max_filesize. Dat verlaagt het risico, maar vervangt niet de update naar een verholpen PHP-versie en de herstart van alle workers.

Bronnen

Direct contact

Elk uur telt. Schrijf ons nu.

Beschrijf kort het systeem, de PHP-versie en wat u opmerkt. Wij nemen contact op, brengen de situatie in kaart en werken op afstand tot het incident is afgerond en gedocumenteerd.

Directe hulp