Bezpieczny upload plików: od walidacji do izolacji
Dlaczego walidacja rozszerzenia niczego nie załatwia: nazwy, typy, serwowanie, parsery, archiwa i limity. Docelowy wzorzec uploadu oraz metodyka testu.
- AUTOR
- Karol Rapacz / CEO Breachroad · OSCP · PNPT
- PUBLIKACJA
- 25 lipca 2026
- CZAS CZYTANIA
- 18 min czytania
- TEMAT
- Pentest i AppSec
Upload plików jest jedną z niewielu funkcji, w których użytkownik wprowadza do systemu treść interpretowaną później przez inne komponenty: serwer WWW, bibliotekę graficzną, konwerter dokumentów, przeglądarkę innego użytkownika, indekser wyszukiwania, silnik podglądu. Każdy z tych komponentów jest osobnym interpreterem z własnymi błędami, więc pytanie „czy to jest prawdziwy obrazek” jest tylko pierwszym z kilku, i wcale nie najważniejszym.
Bezpieczny upload projektuje się wokół trzech pytań: gdzie plik ląduje, kto go potem czyta, i jak jest serwowany. Walidacja rozszerzenia odpowiada na żadne z nich.
Nazwa pliku jest danymi, nie identyfikatorem
Nazwa nadana przez użytkownika nie powinna nigdy trafić do systemu plików w postaci oryginalnej. Powodów jest kilka i każdy z osobna wystarcza.
Nazwa może zawierać sekwencje przejścia po katalogach i zapisać plik poza katalogiem docelowym. Może zawierać separatory ścieżek w kodowaniu, którego walidacja nie przewidziała, albo znaki Unicode normalizowane do postaci zmieniającej wynik już po sprawdzeniu. Może być nazwą zarezerwowaną w systemie Windows albo zaczynać się od kropki i stać się plikiem konfiguracyjnym serwera. Może mieć wiele rozszerzeń i trafić na serwer, który interpretuje ostatnie znane sobie, a nie ostatnie w kolejności. Może być na tyle długa, żeby zostać obciętą, przez co rozszerzenie zniknie razem z walidacją.
Wzorzec docelowy jest krótki: aplikacja generuje własną nazwę, na przykład identyfikator losowy, i zapisuje ją w bazie razem z nazwą oryginalną jako zwykłym polem tekstowym. Nazwa oryginalna służy tylko do wyświetlenia i do zaproponowania nazwy przy pobieraniu — a w obu tych miejscach jest kodowana odpowiednio do kontekstu.
Typ pliku: deklaracja kontra fakt
Nagłówek typu treści w żądaniu jest deklaracją klienta, więc nie jest dowodem. Rozszerzenie również nie. Sprawdzenie sygnatury pliku jest lepsze, ale nadal niewystarczające, ponieważ istnieją pliki poprawne w więcej niż jednym formacie jednocześnie — dokument, który jest jednocześnie obrazem i skryptem, przechodzi kontrolę sygnatury i zachowuje się różnie w zależności od tego, kto go otworzy.
Praktyczne podejście jest dwuwarstwowe. Po pierwsze, lista dozwolonych typów wynikająca z funkcji biznesowej, a nie lista zakazanych. Po drugie, normalizacja przez ponowne zakodowanie: obraz jest dekodowany i zapisywany na nowo w kontrolowanym formacie, dokument jest konwertowany, a plik wynikowy to artefakt wygenerowany przez aplikację, nie plik przesłany przez użytkownika. Ponowne kodowanie usuwa metadane, ładunki doklejone za końcem struktury i większość konstrukcji wieloformatowych.
Jeżeli funkcja wymaga zachowania oryginału — bo tak bywa w obiegu dokumentów — oryginał przechowujemy w magazynie prywatnym, a użytkownikom pokazujemy wersję znormalizowaną.
Serwowanie: tu powstaje trwały XSS
Najczęstsza poważna konsekwencja złego uploadu nie polega na wykonaniu kodu na serwerze, tylko na tym, że plik jest serwowany z tego samego pochodzenia co aplikacja. Wtedy dokument HTML albo grafika wektorowa wgrana jako awatar staje się skryptem działającym w kontekście sesji innych użytkowników.
Reguły, które to zamykają, są proste. Pliki użytkowników serwuj z osobnej domeny — nie tylko z osobnej ścieżki — żeby ciasteczka sesyjne i magazyn lokalny aplikacji były poza zasięgiem. Ustawiaj nagłówek zapobiegający zgadywaniu typu treści i wysyłaj typ z listy dozwolonych, a nie odgadnięty. Dla wszystkiego, co nie musi być wyświetlane w przeglądarce, ustawiaj dyspozycję treści wymuszającą pobranie. Grafikę wektorową albo konwertuj do rastra, albo serwuj wyłącznie jako plik do pobrania, albo czyść z aktywnych elementów dedykowanym narzędziem. Zablokuj wykonywanie kodu w katalogu składowania na poziomie serwera i, jeśli to możliwe, w ogóle nie trzymaj plików w katalogu obsługiwanym przez serwer WWW.
Autoryzacja na ścieżce pobierania
Plik jest zasobem takim samym jak rekord w bazie i podlega tym samym regułom autoryzacji obiektowej. Bardzo częsty błąd polega na tym, że warstwa danych jest chroniona, a warstwa plików nie: dokument leży pod przewidywalnym adresem albo pod podpisanym adresem o zbyt długiej ważności i zbyt szerokim zakresie.
Bezpieczny wariant to pobieranie przez kontroler aplikacji, który sprawdza uprawnienia do obiektu, a dopiero potem strumieniuje treść — albo krótkotrwały adres podpisany, generowany po sprawdzeniu uprawnień, ważny minuty i ograniczony do jednego obiektu.
Przetwarzanie: parsery są największym ryzykiem
Po zapisaniu pliku zaczyna się część, o której łatwo zapomnieć, bo dzieje się w tle. Miniatura, podgląd, konwersja formatu, ekstrakcja tekstu do wyszukiwarki, odczyt metadanych, skanowanie antywirusowe — każda z tych operacji to uruchomienie parsera napisanego zwykle w języku bez ochrony pamięci, na danych pochodzących od użytkownika.
Biblioteki graficzne, konwertery dokumentów i narzędzia do obróbki obrazów mają długą historię podatności, a część z nich obsługuje wewnętrzne mechanizmy pobierania zasobów zewnętrznych, co zamienia przetwarzanie pliku w żądanie sieciowe z serwera. Formaty oparte na XML — grafika wektorowa i dokumenty biurowe — dokładają do tego całą rodzinę problemów z encjami zewnętrznymi.
Wniosek architektoniczny: przetwarzanie plików użytkowników należy traktować jak uruchamianie obcego kodu. Osobny proces lub kontener, bez poświadczeń w zmiennych środowiskowych, bez dostępu do usługi metadanych chmury, z zablokowanym ruchem wychodzącym, z systemem plików tylko do odczytu poza katalogiem roboczym, z limitem czasu i pamięci. Przy takiej izolacji nawet skuteczne wykorzystanie błędu w parserze daje atakującemu proces, w którym nie ma nic wartościowego i skąd nie da się nic wysłać.
Archiwa i dekompresja
Archiwa zasługują na osobny akapit, bo łączą dwa problemy naraz. Pierwszy to ścieżki wewnątrz archiwum: wpis zawierający sekwencje przejścia po katalogach albo dowiązanie symboliczne potrafi zapisać plik poza katalogiem docelowym podczas rozpakowywania. Drugi to współczynnik kompresji: kilkadziesiąt kilobajtów potrafi rozwinąć się do gigabajtów i wyczerpać dysk oraz pamięć.
Bezpieczne rozpakowywanie oznacza: odrzucanie wpisów ze ścieżkami absolutnymi i sekwencjami nadrzędnymi, ignorowanie dowiązań, limit liczby wpisów, limit rozmiaru po dekompresji, limit zagnieżdżenia archiwów oraz zapis do katalogu tymczasowego z późniejszym przeniesieniem.
Limity i odmowa usługi
Limity są częścią bezpieczeństwa. Potrzebne są cztery: maksymalny rozmiar żądania, maksymalny rozmiar pojedynczego pliku, maksymalna liczba plików w żądaniu oraz limit tempa uploadów na konto i na adres. Do tego dochodzą limity specyficzne dla formatu — przede wszystkim maksymalne wymiary obrazu, ponieważ plik o kilkuset kilobajtach potrafi zadeklarować rozdzielczość wymagającą wielu gigabajtów pamięci przy dekodowaniu.
Warto też ustawić limit czasu przetwarzania i kolejkować operacje kosztowne, zamiast wykonywać je w cyklu żądania.
Skanowanie antywirusowe
Skanowanie ma sens, gdy pliki są następnie pobierane przez ludzi i otwierane na stacjach roboczych — wtedy jest warstwą redukującą ryzyko po stronie odbiorcy. Nie ma sensu jako zabezpieczenie samej aplikacji, ponieważ nie wykrywa plików skrojonych pod konkretny parser ani konstrukcji wieloformatowych.
Jeśli skanowanie jest wdrożone, powinno działać na etapie kwarantanny, przed udostępnieniem pliku innym użytkownikom, i nie powinno blokować odpowiedzi dla użytkownika przesyłającego.
Docelowy przepływ
Zbierając to w całość, dojrzały upload wygląda tak.
Plik trafia najpierw do kwarantanny — magazynu prywatnego, spoza katalogu serwera WWW, bez możliwości wykonania i bez publicznego odczytu. Aplikacja generuje własny identyfikator i zapisuje metadane w bazie, łącznie z nazwą oryginalną jako polem tekstowym.
Następnie walidacja: rozmiar, typ ustalony po zawartości i porównany z listą dozwolonych, wymiary obrazu, liczba stron dokumentu, struktura archiwum.
Potem normalizacja w izolowanym procesie: ponowne zakodowanie obrazu, konwersja dokumentu, usunięcie metadanych, wygenerowanie miniatury. Wynik jest artefaktem aplikacji.
Dalej publikacja: plik znormalizowany trafia do magazynu docelowego, a rekord w bazie wiąże go z właścicielem i organizacją.
Na końcu serwowanie: pobranie przechodzi przez autoryzację obiektową, plik jest wydawany z osobnej domeny, z jawnie ustawionym typem, z blokadą zgadywania typu i z wymuszonym pobieraniem tam, gdzie to możliwe.
Metodyka testu
Test zaczyna się od ustalenia, czy istnieje ścieżka, w której nazwa użytkownika staje się nazwą na dysku. Jeżeli aplikacja generuje własne nazwy, cała rodzina problemów z nazwami znika i można przejść dalej.
Drugi krok to sprawdzenie weryfikacji typu: czy deklaracja z żądania ma wpływ na decyzję, czy plik z niepasującą zawartością jest odrzucany, oraz czy po stronie serwera zachodzi ponowne kodowanie. Najprostszym testem jest porównanie pliku wynikowego z przesłanym — jeżeli są identyczne bajt w bajt, normalizacji nie ma.
Trzeci krok dotyczy serwowania: z jakiej domeny wydawane są pliki, jakie nagłówki im towarzyszą, co się dzieje przy otwarciu bezpośrednio w przeglądarce oraz czy grafika wektorowa jest traktowana inaczej niż rastrowa.
Czwarty krok to autoryzacja: czy plik należący do innego konta jest osiągalny po znajomości adresu, jaka jest ważność adresów podpisanych i czy obejmują jeden obiekt.
Piąty krok to odporność: zachowanie przy pliku o deklarowanych ogromnych wymiarach, przy archiwum zagnieżdżonym, przy dokumencie z odwołaniem zewnętrznym oraz przy serii uploadów pod limit rozmiaru.
Szósty krok, często pomijany, to cykl życia: czy pliki tymczasowe są usuwane, czy usunięcie obiektu w aplikacji usuwa plik z magazynu i czy kopie zapasowe magazynu podlegają tym samym regułom dostępu.
Wszystkie próby wykonuje się w zakresie objętym zgodą i na koncie testowym, a jako dowód wystarcza wykazanie zachowania — nie trzeba umieszczać w systemie klienta plików aktywnych.
Detekcja
Warto logować typ ustalony po zawartości obok typu zadeklarowanego. Rozbieżność między nimi jest sygnałem o niskim poziomie szumu i wysokiej wartości.
Wyjątki parserów w procesie przetwarzania oznaczają albo uszkodzony plik, albo próbę wykorzystania błędu; ich seria z jednego konta jest alertem.
Ruch wychodzący z komponentu przetwarzającego pliki, w architekturze, która go nie przewiduje, jest jednym z najlepszych sygnałów, jakie da się mieć.
Nagły wzrost uploadów, plików odrzuconych albo zużycia pamięci przez proces konwertujący wskazuje na próbę odmowy usługi.
Układ magazynu i wielodostępność
Magazyn obiektowy jest dziś domyślnym miejscem składowania plików i wnosi własny zestaw decyzji, które przesądzają o bezpieczeństwie niezależnie od kodu aplikacji.
Dostęp publiczny wyłączony na poziomie zasobnika, nie tylko na poziomie pojedynczych obiektów. Ustawienie na obiekcie da się nadpisać przez pomyłkę w kodzie lub przez migrację; blokada na poziomie zasobnika jest odporna na obie sytuacje.
Klucze obiektów nie są tajemnicą i nie powinny nią być. Struktura klucza powinna zawierać identyfikator organizacji i identyfikator obiektu, ponieważ ułatwia to egzekwowanie polityk i rozliczanie kosztów, ale bezpieczeństwo ma wynikać z kontroli dostępu, a nie z nieodgadywalności ścieżki.
Osobne prefiksy dla kwarantanny, treści znormalizowanych i miniatur pozwalają nadać różne polityki: kwarantanna bez odczytu przez usługi frontowe, miniatury z krótkim czasem życia w pamięci podręcznej, oryginały wyłącznie przez kontroler aplikacji.
Wersjonowanie i usuwanie wymagają decyzji świadomej. Włączone wersjonowanie sprawia, że „usunięty” plik nadal istnieje — co bywa pożądane przy odzyskiwaniu po incydencie i niepożądane przy realizacji żądania usunięcia danych osobowych. Reguły cyklu życia powinny odzwierciedlać politykę retencji, a nie domyślne ustawienia dostawcy.
Poświadczenia komponentów muszą być rozdzielone: usługa przyjmująca pliki potrzebuje prawa zapisu do kwarantanny, usługa przetwarzająca — odczytu z kwarantanny i zapisu do magazynu docelowego, usługa serwująca — wyłącznie odczytu. Jedno konto z pełnym dostępem do zasobnika zamienia dowolny błąd aplikacyjny w dostęp do wszystkich plików wszystkich klientów.
Checklista
Nazwa pliku jest generowana przez aplikację. Typ jest ustalany po zawartości i porównywany z listą dozwolonych. Obrazy są ponownie kodowane, dokumenty konwertowane. Pliki są serwowane z osobnej domeny, z jawnym typem i blokadą zgadywania. Grafika wektorowa nie jest serwowana jako aktywna treść. Pobieranie przechodzi przez autoryzację obiektową. Adresy podpisane są krótkotrwałe i wąskie. Przetwarzanie działa w izolacji, bez sekretów i bez sieci. Archiwa mają limity wpisów, rozmiaru po dekompresji i zagnieżdżenia. Obowiązują limity rozmiaru, liczby, tempa i wymiarów obrazu. Katalog składowania nie wykonuje kodu. Usunięcie obiektu usuwa plik.
Wniosek
Upload plików jest funkcją, w której spotyka się najwięcej warstw systemu naraz: nazwa trafia do systemu plików, zawartość do parsera, wynik do przeglądarki innego użytkownika, a metadane do wyszukiwarki. Dlatego jego bezpieczeństwo nie sprowadza się do jednej kontroli, tylko do przepływu: kwarantanna, walidacja, normalizacja, publikacja, kontrolowane serwowanie.
Jeżeli miałbym wskazać dwie decyzje o największej wartości względem kosztu, byłyby to ponowne kodowanie treści zamiast przyjmowania pliku użytkownika oraz serwowanie plików z osobnej domeny. Pierwsza usuwa większość ładunków, druga sprawia, że nawet skuteczny ładunek nie ma dostępu do sesji.
Źródła pierwotne: OWASP — File Upload Cheat Sheet, CWE-434: Unrestricted Upload of File with Dangerous Type, CWE-22: Path Traversal.


