Path traversal (directory traversal) to jeden z prostszych ataków webowych, a mimo to nadal trafia do top 10 podatności. Pozwala atakującemu czytać dowolne pliki z serwera — konfigurację, klucze SSH, hasła, kod aplikacji. W niektórych przypadkach umożliwia też zapis.

Jak to działa?

Wyobraź sobie aplikację, która wyświetla obrazki:

https://example.com/loadImage?filename=photo.jpg

Serwer bierze parametr filename i dokłada go do ścieżki bazowej:

/var/www/images/ + photo.jpg → /var/www/images/photo.jpg

Co jeśli zamiast photo.jpg podamy ../../../etc/passwd?

/var/www/images/../../../etc/passwd → /etc/passwd

Sekwencja ../ cofa się o katalog wyżej. Trzy takie sekwencje z /var/www/images/ docierają do roota filesystem. Serwer zwraca zawartość /etc/passwd.

Co atakujący chce przeczytać?

Na Linuxie typowe cele:

/etc/passwd              # lista użytkowników
/etc/shadow              # hashe haseł (jeśli serwer działa jako root)
/etc/hosts               # konfiguracja DNS
/proc/self/environ       # zmienne środowiskowe (mogą zawierać klucze API)
/home/user/.ssh/id_rsa   # klucz prywatny SSH
/var/log/auth.log        # logi logowania
~/.bash_history          # historia komend

Na aplikacji webowej:

/var/www/html/.env             # hasła do bazy, klucze API
/var/www/html/config/database.yml  # konfiguracja Rails
/etc/nginx/nginx.conf          # konfiguracja serwera

Techniki obchodzenia zabezpieczeń

Naiwna obrona to filtrowanie ../ ze stringa. Atakujący mają na to kilka sposobów.

Podwójne sekwencje

Jeśli serwer usuwa ../ raz, wystarczy zagnieżdżenie:

....//....//....//etc/passwd

Po usunięciu wewnętrznego ../ zostaje ../../../etc/passwd.

URL encoding

%2e%2e%2f                    →  ../
%2e%2e/                      →  ../
..%2f                        →  ../

Podwójne kodowanie

Jeśli serwer dekoduje URL dwa razy:

%252e%252e%252f  →  %2e%2e%2f  →  ../

Kodowanie niestandardowe

..%c0%af         →  ../  (UTF-8 overlong encoding)
..%ef%bc%8f      →  ../  (fullwidth slash)

Null byte

Jeśli serwer wymusza rozszerzenie pliku (np. .jpg):

../../../etc/passwd%00.jpg

Null byte (%00) przycina string — serwer otwiera /etc/passwd, ignorując .jpg. Ten trik działa głównie na starszych wersjach PHP i C.

Ścieżka bezwzględna

Czasem nie trzeba ../ — wystarczy podać pełną ścieżkę:

filename=/etc/passwd

Rozpoczęcie od wymaganego katalogu

Jeśli serwer sprawdza, czy ścieżka zaczyna się od /var/www/images:

filename=/var/www/images/../../../etc/passwd

Walidacja przechodzi, bo ścieżka zaczyna się od wymaganego katalogu.

Jak się bronić?

Dwie warstwy obrony — obie potrzebne jednocześnie.

1. Walidacja inputu

Najlepiej: whitelist dozwolonych nazw plików. Jeśli to niemożliwe — sprawdź, czy input zawiera tylko alfanumeryczne znaki:

import re

def is_safe_filename(filename):
    return bool(re.match(r'^[a-zA-Z0-9._-]+$', filename))

To blokuje ../, %2e, null bytes i wszystko inne.

2. Kanonizacja ścieżki

Nawet z walidacją inputu — sprawdź rozwiązaną ścieżkę:

import os

BASE_DIR = '/var/www/images'

def safe_path(filename):
    full_path = os.path.realpath(os.path.join(BASE_DIR, filename))
    if not full_path.startswith(BASE_DIR):
        raise ValueError('Path traversal detected')
    return full_path

os.path.realpath() rozwiązuje symlinki i ../, więc nawet jeśli atakujący obejdzie walidację inputu, kanonizacja to złapie.

3. Minimalne uprawnienia

Serwer webowy nie powinien działać jako root. Jeśli działa jako www-data, nawet udany path traversal nie przeczyta /etc/shadow.

# Sprawdź jako kto działa serwer
ps aux | grep nginx
ps aux | grep apache

4. chroot / kontenery

W środowisku produkcyjnym — chroot, Docker, lub namespace’y. Atakujący może podać ../../../../, ale jeśli root filesystem jest izolowany, nie ucieknie dalej niż kontener.

Testowanie

Szybki test na swojej aplikacji:

# Podstawowy test
curl "https://twoja-strona.com/api/file?name=../../../etc/passwd"

# Z encodingiem
curl "https://twoja-strona.com/api/file?name=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd"

# Null byte
curl "https://twoja-strona.com/api/file?name=../../../etc/passwd%00.jpg"

Jeśli którykolwiek zwraca treść /etc/passwd — masz problem.