Bezpieczne przesyłanie plików w aplikacjach webowych wymaga rygorystycznych uprawnień, limitów rozmiaru, podpisanych URL-i i prostych wzorców skanowania, aby uniknąć incydentów.

Przesyłanie plików wydaje się nieszkodliwe: zdjęcie profilowe, PDF, arkusz. Ale często to pierwszy incydent bezpieczeństwa, bo pozwala obcym włożyć w twój system tajemnicze pudełko. Jeśli je przyjmiesz, zapiszesz i pokażesz innym, stwarzasz nowy wektor ataku.
Ryzyko to nie tylko „ktoś wysłał wirusa”. Zły upload może ujawnić prywatne pliki, wywindować koszty przechowywania lub oszukać użytkowników, by oddali dostęp. Plik nazwany „invoice.pdf” może nie być wcale PDF-em. Nawet prawdziwe PDF-y i obrazy mogą sprawiać problemy, jeśli aplikacja ufa metadanym, generuje podglądy automatycznie lub serwuje je z nieodpowiednimi regułami.
Prawdziwe awarie często wyglądają tak:
Jedna rzecz powoduje wiele incydentów: przechowywanie plików to nie to samo co ich serwowanie. Przechowywanie to miejsce, gdzie trzymasz bajty. Serwowanie to sposób, w jaki te bajty są dostarczane do przeglądarek i aplikacji. Problemy pojawiają się, gdy aplikacja serwuje uploady z takim samym poziomem zaufania i zasadami jak główna strona, dlatego przeglądarka traktuje upload jako „zaufany”.
„Wystarczająco bezpieczne” dla małej lub rosnącej aplikacji zwykle oznacza, że możesz bez lania wody odpowiedzieć na cztery pytania: kto może wgrywać, co akceptujesz, jak duże i jak często, oraz kto może to potem czytać. Nawet jeśli budujesz szybko (z generowanym kodem lub platformą sterowaną czatem), te zabezpieczenia wciąż się liczą.
Traktuj każdy upload jak niezaufane wejście. Praktyczny sposób na ochronę to wyobrazić sobie, kto może nadużyć uploadów i co „sukces” dla niego oznacza.
Większość atakujących to boty skanujące słabe formularze uploadu albo prawdziwi użytkownicy testujący granice, by zdobyć darmowe miejsce, zeskrobać dane lub trollować serwis. Czasem to konkurent szukający wycieków lub przyczyn awarii.
Czego chcą? Zazwyczaj jedno z tych rezultatów:
Następnie zmapuj słabe punkty. Endpoint uploadu to drzwi wejściowe (za duże pliki, dziwne formaty, wysoki ruch). Przechowywanie to tylne pomieszczenie (publiczne buckety, złe uprawnienia, współdzielone foldery). URL-e do pobrania to wyjście (przewidywalne, długotrwałe lub niepowiązane z użytkownikiem).
Przykład: funkcja „upload CV”. Bot wgrywa tysiące dużych PDF-ów, żeby nabić koszty, a nadużywający wgrywa HTML i udostępnia go jako „dokument”, by oszukać innych.
Zanim dodasz kontrolki, zdecyduj co jest najważniejsze dla twojej aplikacji: prywatność (kto może czytać), dostępność (czy możesz dalej serwować), koszty (przechowywanie i transfer) i zgodność (gdzie przechowywane są dane i jak długo). Ta lista priorytetów ułatwia spójne decyzje.
Większość incydentów z uploadami to nie wyszukane ataki — to proste błędy typu „mogę zobaczyć czyjś plik”. Traktuj uprawnienia jako część uploadu, nie dodatek na później.
Zacznij od jednej zasady: domyślnie odmów. Zakładaj, że każdy przesłany obiekt jest prywatny, dopóki nie pozwolisz inaczej. „Domyślnie prywatne” to mocna baza dla faktur, dokumentów medycznych, dokumentów konta i wszystkiego związanego z użytkownikiem. Udostępniaj pliki publicznie tylko wtedy, gdy użytkownik wyraźnie tego oczekuje (np. publiczny avatar), a i tak rozważ dostęp czasowy.
Utrzymuj role proste i rozdzielne. Typowy podział:
Nie polegaj na regułach folderowych typu „wszystko w /user-uploads/ jest OK”. Sprawdzaj własność lub dostęp tenancy przy odczycie, dla każdego pliku. To chroni, gdy ktoś zmienia zespół, odchodzi z organizacji albo plik jest przypisywany na nowo.
Dobry wzorzec dla supportu to wąskie i tymczasowe uprawnienie: przyznaj dostęp do jednego pliku, zaloguj to i ustaw automatyczne wygaśnięcie.
Większość ataków zaczyna się prostym trikiem: plik wygląda bezpiecznie po nazwie lub nagłówku przeglądarki, ale w rzeczywistości jest czymś innym. Traktuj wszystko, co klient wysyła, jako niezaufane.
Zacznij od allowlisty: zdecyduj dokładne formaty, które akceptujesz (np. .jpg, .png, .pdf) i odrzucaj wszystko inne. Unikaj „dowolny obraz” albo „dowolny dokument”, chyba że naprawdę tego potrzebujesz.
Nie ufaj rozszerzeniu nazwy ani nagłówkowi Content-Type od klienta. Oba są łatwe do podrobienia. Plik invoice.pdf może być wykonywalny, a Content-Type: image/png może być kłamstwem.
Silniejsze podejście to sprawdzenie pierwszych bajtów pliku, zwanych „magic bytes” lub sygnaturą pliku. Wiele formatów ma stałe nagłówki (jak PNG czy JPEG). Jeśli nagłówek nie pasuje do dozwolonego formatu, odrzuć plik.
Praktyczna konfiguracja walidacji:
Zmiana nazwy ma większe znaczenie niż się wydaje. Jeśli zapisujesz nazwy dostarczone przez użytkownika bezpośrednio, narażasz się na sztuczki ścieżkowe, dziwne znaki i przypadkowe nadpisania. Używaj wygenerowanego ID do przechowywania i zachowaj oryginalną nazwę tylko do wyświetlenia.
Dla zdjęć profilowych akceptuj tylko JPEG i PNG, weryfikuj nagłówki i usuń metadane jeśli możesz. Dla dokumentów rozważ ograniczenie do PDF i odrzucanie wszystkiego z aktywną zawartością. Jeśli później zdecydujesz, że potrzebujesz SVG lub HTML, traktuj je jak potencjalnie wykonywalne i izoluj.
Większość awarii uploadu to nie „wyszukane haki”. To ogromne pliki, za dużo żądań albo powolne połączenia blokujące serwery, aż aplikacja przestaje odpowiadać. Traktuj każdy bajt jako koszt.
Wybierz maksymalny rozmiar per funkcję, nie jeden globalny. Avatar nie potrzebuje tego samego limitu co dokument podatkowy czy krótki film. Ustal najmniejszy limit, który wciąż jest sensowny, a dla rzeczy większych stwórz osobną ścieżkę (np. direct-to-object-storage ze signed URL).
Wymuszaj limity w więcej niż jednym miejscu, bo klienci kłamią: w logice aplikacji, na serwerze WWW lub reverse proxy, przez timeouty uploadu i przez wczesne odrzucenie, gdy zadeklarowany rozmiar jest za duży (zanim przeczytasz całe ciało).
Konkretny przykład: awatary do 2 MB, PDF-y do 20 MB, a wszystko większe wymaga innego flow (np. podpisany URL do storage).
Nawet małe pliki mogą być DoS-em, jeśli ktoś wgrywa je w pętli. Dodaj rate limits na endpointy uploadu na użytkownika i na IP. Rozważ ostrzejsze limity dla ruchu anonimowego niż dla zalogowanych.
Resumable uploads pomagają prawdziwym użytkownikom na złych sieciach, ale token sesji musi być ścisły: krótka ważność, powiązany z użytkownikiem i przypięty do konkretnego rozmiaru i miejsca docelowego. W przeciwnym razie endpoint „resume” stanie się darmową rurą do twojego storage.
Gdy blokujesz upload, zwracaj jasne błędy dla użytkownika (plik za duży, za dużo żądań), ale nie wyciekaj szczegółów wewnętrznych (stack trace, nazwy bucketów, dane vendorów).
Bezpieczne uploady to nie tylko to, co akceptujesz. To też gdzie plik trafia i jak go potem oddajesz.
Trzymaj bajty uploadu poza główną bazą danych. Większość aplikacji potrzebuje w DB tylko metadanych (ID właściciela, oryginalna nazwa, wykryty typ, rozmiar, checksum, klucz przechowywania, czas utworzenia). Bajty przechowuj w object storage lub serwisie plikowym zaprojektowanym pod duże bloby.
Oddziel publiczne i prywatne pliki na poziomie storage. Używaj różnych bucketów/kontenerów z odmiennymi regułami. Pliki publiczne (np. publiczne avatary) mogą być czytelne bez logowania. Pliki prywatne (umowy, faktury, dokumenty medyczne) nigdy nie powinny być publicznie czytelne, nawet jeśli ktoś odgadnie URL.
Unikaj serwowania plików użytkowników z tej samej domeny co twoja aplikacja, gdy możesz. Jeśli ryzykowny plik się przedostanie (HTML, SVG z skryptami lub problemy z MIME sniffingiem), hostowanie go na głównej domenie może doprowadzić do przejęcia konta. Dedykowana domena do pobierania (lub domena storage) ogranicza zasięg szkód.
Przy pobieraniu narzucaj bezpieczne nagłówki. Ustaw przewidywalny Content-Type oparty na tym, co akceptujesz, a nie na tym, co twierdzi użytkownik. Dla wszystkiego, co może być interpretowane przez przeglądarkę, preferuj wysłanie jako pobranie.
Kilka domyślnych ustawień zapobiegających niespodziankom:
Content-Disposition: attachment dla dokumentów.Content-Type (lub application/octet-stream).Retencja to też bezpieczeństwo. Usuwaj porzucone uploady, usuwaj stare wersje po zastąpieniu i ustawiaj limity czasowe dla tymczasowych plików. Mniej przechowywanych danych to mniej do wycieku.
Signed URLs (pre-signed URLs) to powszechny sposób, by pozwolić na upload lub pobranie bez udostępniania bucketa i bez przesyłania każdego bajtu przez API. URL niesie tymczasowe uprawnienie, potem wygasa.
Dwa typowe flow:
Direct-to-storage zmniejsza obciążenie API, ale wymusza ścisłe reguły storage i ograniczenia URL-i.
Traktuj signed URL jak jednorazowy klucz. Spraw, by był specyficzny i krótkotrwały.
Content-Type, maksymalny rozmiar, checksum.Praktyczny wzorzec: najpierw utwórz rekord uploadu (status: pending), potem wydaj signed URL. Po uploadzie potwierdź, że obiekt istnieje i pasuje rozmiarem i typem, zanim oznaczysz go jako gotowy.
Bezpieczny flow to głównie jasne reguły i jasny stan. Traktuj każdy upload jako niezaufany, dopóki kontrole nie zostaną wykonane.
Zapisz, co każda funkcja pozwala. Zdjęcie profilowe i dokument podatkowy nie powinny dzielić tych samych typów plików, limitów rozmiaru ani widoczności.
Zdefiniuj dozwolone typy i limit rozmiaru per funkcję (np. zdjęcia do 5 MB; PDF-y do 20 MB). Wymuszaj te same zasady w backendzie.
Utwórz „rekord uploadu” zanim pojawią się bajty. Zapisz: właściciela (user lub org), cel (avatar, invoice, attachment), oryginalną nazwę pliku, oczekiwany maksymalny rozmiar i status jak pending.
Uploaduj do prywatnej lokalizacji. Nie pozwól klientowi wybierać finalnej ścieżki.
Waliduj ponownie po stronie serwera: rozmiar, magic bytes/typ, allowlista. Jeśli przejdzie, ustaw status na uploaded.
Skanuj pod kątem malware i zaktualizuj status na clean lub quarantined. Jeśli skan jest asynchroniczny, trzymaj dostęp zablokowany do czasu wyniku.
Pozwalaj na pobranie, podgląd lub przetwarzanie tylko, gdy status = clean.
Mały przykład: dla zdjęcia profilowego utwórz rekord powiązany z użytkownikiem i celem avatar, przechowuj prywatnie, potwierdź, że to naprawdę JPEG/PNG (nie tylko nazwa), przeskanuj, a potem wygeneruj URL podglądu.
Skanowanie to siatka bezpieczeństwa, nie obietnica. Wyłapie znane złe pliki i oczywiste sztuczki, ale nie wszystko. Cel jest prosty: zmniejszyć ryzyko i domyślnie uczynić nieznane pliki nieszkodliwymi.
Niezawodny wzorzec to najpierw kwarantanna. Zapisz każdy nowy upload w prywatnym, niepublicznym miejscu i oznacz go jako pending. Dopiero po przejściu kontroli przenieś go do „clean” (lub oznacz jako dostępny).
Skanowanie synchroniczne działa tylko dla małych plików i niskiego ruchu, bo użytkownik czeka. Większość aplikacji skanuje asynchronicznie: przyjmij upload, zwróć stan „przetwarzanie”, skanuj w tle.
Podstawowe skanowanie to zazwyczaj silnik antywirusowy (lub usługa) plus kilka zabezpieczeń: skan AV, sprawdzenie typu pliku (magic bytes), limity dla archiwów (zip bomb, zagnieżdżone zipy, ogromny rozmiar po rozpakowaniu) i blokowanie formatów, których nie potrzebujesz.
Jeśli skaner się nie powiedzie, przekroczy czas lub zwróci „nieznany”, traktuj plik jako podejrzany. Trzymaj go w kwarantannie i nie udostępniaj linku. Tu zespoły często się potykają: „skan nie powiódł się” nie powinno oznaczać „i tak publikujemy”.
Gdy blokujesz plik, komunikuj neutralnie: „Nie mogliśmy przyjąć tego pliku. Spróbuj innego pliku lub skontaktuj się z supportem.” Nie twierdz, że wykryto malware, chyba że masz pewność.
Weź dwie funkcje: zdjęcie profilowe (pokazywane publicznie) i paragon PDF (prywatny, używany do bilingów lub supportu). Obie to problemy uploadu, ale nie powinny dzielić tych samych reguł.
Dla zdjęcia profilowego utrzymaj rygor: tylko JPEG/PNG, limit rozmiaru (np. 2–5 MB) i re-enkoduj po stronie serwera, żeby nie serwować oryginalnych bajtów użytkownika. Przechowuj publicznie dopiero po kontrolach.
Dla paragonu PDF akceptuj większy rozmiar (np. do 20 MB), trzymaj prywatnie domyślnie i unikaj renderowania inline z głównej domeny aplikacji.
Prosty model statusów informuje użytkownika, nie ujawniając wnętrza systemu:
Signed URLs pasują tu dobrze: używaj krótkotrwałego signed URL do uploadu (write-only, jeden object key). Wystawiaj osobny, krótkotrwały signed URL do odczytu dopiero, gdy status = clean.
Loguj to, co potrzebne do śledztwa, nie samych treści: ID użytkownika, ID pliku, typ-guess, rozmiar, klucz przechowywania, timestampy, wynik skanu, request IDs. Unikaj logowania surowej zawartości lub wrażliwych danych znalezionych w dokumentach.
Większość bugów pojawia się, bo mały „tymczasowy” skrót zostaje na stałe. Zakładaj, że każdy plik jest niezaufany, każdy URL będzie udostępniony, i każde „naprawimy to później” ustawienie zostanie zapomniane.
Pułapki, które się powtarzają:
Content-Type, pozwalając przeglądarce interpretować ryzykowną zawartość.Monitoring to element, który zespoły omijają, dopóki rachunek za storage nie wystrzeli. Śledź wolumen uploadów, średni rozmiar, największych uploaderów i stawki błędów. Jedno skompromitowane konto może potajemnie wgrać tysiące dużych plików w nocy.
Przykład: zespół trzyma avatary pod nazwami podanymi przez użytkownika jak „avatar.png” w współdzielonym folderze. Jeden użytkownik nadpisuje obrazy innych. Poprawka jest nudna, ale skuteczna: generuj klucze obiektów po stronie serwera, trzymaj uploady domyślnie prywatne i udostępniaj zmieniony rozmiar obrazu przez kontrolowaną odpowiedź.
Użyj tego jako ostatniego przeglądu przed wdrożeniem. Traktuj każdy punkt jako blocker wydania, bo większość incydentów wynika z jednego brakującego zabezpieczenia.
Content-Type, bezpieczne nazwy plików i attachment dla dokumentów.Spisz swoje reguły prostym językiem: dozwolone typy, maksymalne rozmiary, kto ma dostęp do czego, jak długo żyją signed URL i co znaczy „skan przeszedł”. To staje się wspólnym kontraktem między produktem, inżynierią i supportem.
Dodaj kilka testów, które łapią typowe błędy: za duże pliki, pliki zmienione z rozszerzeniem wykonywalnym, nieautoryzowane odczyty, wygasłe signed URL i pobrania, gdy status jest „scan pending”. Te testy są tanie w porównaniu do incydentu.
Jeśli budujesz i iterujesz szybko, pomaga workflow z możliwością planowania zmian i bezpiecznego przywracania. Zespoły korzystające z Koder.ai (koder.ai) często polegają na trybie planowania i snapshotach/rollback, dopracowując reguły uploadu w czasie, ale podstawowy wymóg się nie zmienia: politykę egzekwuje backend, nie UI.
Zacznij od zasady domyślnie prywatne i traktuj każde przesłanie jako niezaufane wejście. Wymuś cztery podstawy po stronie serwera:
Jeśli możesz na to wszystko jasno odpowiedzieć, jesteś już przed większością incydentów.
Bo użytkownicy mogą przesłać „tajemnicze pudełko”, które aplikacja zapisze, a potem może je komuś pokazać. To może prowadzić do:
Rzadko chodzi tylko o „ktoś przesłał wirusa”.
Przechowywanie to miejsce, gdzie trzymasz bajty. Serwowanie to sposób, w jaki te bajty są dostarczane do przeglądarek i aplikacji.
Problem pojawia się, gdy aplikacja serwuje pliki użytkowników z tym samym poziomem zaufania i zasadami co główna strona. Jeśli ryzykowny plik zostanie potraktowany jak zwykła strona, przeglądarka może go wykonać (albo użytkownicy zaufają mu zbyt mocno).
Bezpieczniejszy domyślny model: przechowuj prywatnie, a następnie serwuj przez kontrolowane odpowiedzi z bezpiecznymi nagłówkami.
Użyj zasady domyślnego odmówienia i sprawdzaj uprawnienia za każdym razem, gdy plik jest pobierany lub podglądany.
Praktyczne reguły:
Nie ufaj rozszerzeniu pliku ani nagłówkowi Content-Type z przeglądarki. Waliduj po stronie serwera:
Awarie często wynikają z nudnego nadużycia: zbyt wiele uploadów, wielkie pliki albo powolne połączenia blokujące zasoby.
Domyślne dobre praktyki:
Traktuj każdy bajt jak koszt, a każde żądanie jak potencjalne nadużycie.
Tak, ale ostrożnie. Signed URLs pozwalają przeglądarce wysyłać bezpośrednio do storage bez publicznego bucketa.
Dobre domyślny:
Direct-to-storage zmniejsza obciążenie API, ale wymaga ścisłego zakresu i wygaśnięcia.
Najbezpieczniejszy wzorzec to:
Skanowanie pomaga, ale nie daje gwarancji. Traktuj je jako siatkę bezpieczeństwa.
Praktyczne podejście:
Klucz: „niezeskanowane” nie powinno oznaczać „dostępne”.
Serwuj pliki tak, by przeglądarki nie interpretowały ich jako strony webowej.
Dobre domyślne:
Content-Disposition: attachment dla dokumentówWiększość błędów to proste „widzę plik innego użytkownika”.
Jeśli bajty nie pasują do dozwolonego formatu, odrzuć upload.
pending)clean lub quarantined; jeśli skan async — trzymaj blokadęcleanTo zapobiega przypadkom „skan nieudany = udostępnij”.
Content-Type (lub application/octet-stream)To zmniejsza ryzyko, że upload stanie się stroną phishingową lub skryptem.