Strona główna / Artykuły

Od przesyłania do adresu URL: Bezpieczne przechowywanie i serwowanie plików użytkowników w Express

Dowiedz się, gdzie aplikacje Express powinny przechowywać przesłane pliki, jak express.static mapuje folder na adresy URL oraz jakie zabezpieczenia zapobiegają temu, by przesłane przez użytkowników pliki stały się luką bezpieczeństwa.

Wydawca: REACTAPP.TOP7 min czytania1589 słów

Źródła: Storing Uploaded Files and Serving Them in Express

Użytkownik wybiera zdjęcie profilowe, klikuje „przesłać”, a sekundę później pojawia się ono na jego stronie. Za tą prostą interakcją kryją się trzy istotne decyzje projektowe: gdzie fizycznie znajduje się plik, jak serwer może go ponownie odnaleźć oraz w jaki sposób przeglądarka go wyświetla. Ten przewodnik omawia wszystkie te aspekty w kontekście aplikacji Express – od wyboru miejsca przechowywania po udostępnianie plików za pomocą adresu URL – oraz kończy się wskazówkami dotyczącymi środków ostrożności, które zapobiegają przekształceniu się funkcji przesyłania plików w pole ataku.

Miejsce przechowywania przesyłanych plików

Zanim plik będzie mógł zostać ponownie udostępniony, musi zostać gdzieś zapisany. W najprostszym ustawieniu jest to folder w własnym systemie plików serwera, zwykle o nazwie uploads, umieszczony w korzeniu projektu obok kodu źródłowego, a nie w jego wnętrzu:

project-root/
  uploads/
    avatars/
      user-42.png
      user-91.png
    documents/
      invoice-108.pdf
  src/
    app.js

Grupowanie plików według przeznaczenia, takiego jak avatars/ i documents/, szybko przynosi korzyści. Ułatwia zarządzanie listami katalogów w miarę wzrostu ich liczby, ułatwia stosowanie zasad czyszczenia i tworzenia kopii zapasowych dla poszczególnych kategorii, a także – jak wkrótce zobaczysz – generuje URL-y, które są łatwe do przewidzenia. Jedynie pojedynczy folder zawierający wszystkie rodzaje plików staje się trudny do zarządzania na długo przed tym, zanim stanowi problem wydajnościowy.

W większości aplikacji baza danych przechowuje jedynie odnośnik do pliku, np. ścieżkę względną avatars/user-42.png, a nie sam plik ani pełny URL. Przechowywanie ścieżki względnej oznacza, że później można zmienić domenę lub przenieść się na inny system przechowywania danych bez konieczności przepisywania każdego wiersza.

Twardy dysk lokalny czy zewnętrzne przechowywanie obiektów

Zachowywanie plików na serwerze aplikacji

Zapisywanie plików bezpośrednio na dysku serwera, jak pokazano powyżej, nie wymaga dodatkowej infrastruktury. Jest to odpowiednie rozwiązanie do rozwoju lokalnego, prototypów, narzędzi wewnętrznych oraz aplikacji, których objętość przesyłanych danych jest mała i przewidywalna.

Przenoszenie plików do usługi przechowywania

Systemy produkcyjne częściej przekazują przesyłane dane do dedykowanego magazynu obiektów, na przykład Amazon S3, Google Cloud Storage lub podobnej usługi, unikając w ten sposób angażowania dysku serwera aplikacji.

Dlaczego wybór ma znaczenie

  • Dyski mogą być małe i krótkotrwałe. Wiele platform hostingowych odbudowuje lub zastępuje instancje przy każdej aktualizacji, więc plik istniejący tylko na dysku lokalnym może zniknąć przy następnej publikacji.
  • Wielokrotne instancje nie dzielą się dyskiem. Po skalowaniu poziomym plik zapisany na jednej instancji jest niewidoczny dla pozostałych. Centralne magazynowanie zapewnia wszystkim instancjom takie samo widok.
  • Magazyny obiektów są stworzone do tego celu. Ich głównym zadaniem jest trwałość danych, replikacja, tworzenie kopii zapasowych oraz efektywna dostawa na dużą skalę, podczas gdy serwer aplikacji uniwersalny jest zoptymalizowany do uruchamiania twojego kodu.
  • To wszystko nie czyni lokalnego magazynowania błędnym. Dla projektów edukacyjnych oraz naprawdę małych aplikacji jest to rozsądny pierwszy krok, a poniższe zasady (uporządkowane ścieżki, przewidywalne adresy URL, dokładna weryfikacja) pozostają bez zmian, jeśli później przeniesiesz dane do zewnętrznego magazynu. Umieszczenie kodu obsługującego przechowywanie w małym module z funkcjami takimi jak save i getUrl znacznie ułatwia tę migrację.

    Użycie express.static do obsługi folderu

    Po zapisaniu pliku przeglądarka nadal potrzebuje sposobu na jego pobraanie. Express zawiera middleware do obsługi plików statycznych właśnie w tym celu:

    const express = require("express");
    const app = express();
    
    app.use("/uploads", express.static("uploads"));
    

    Ta linia montuje katalog uploads pod prefiksem URL /uploads. Dla każdego żądania, którego ścieżka zaczyna się od tego prefiksu, Express szuka pliku w odpowiadającym miejscu w folderze i, jeśli taki istnieje, przesyła go z odpowiednim typem treści oraz nagłówkami cache’owania. Nie trzeba pisać osobnego obsługującego kodu dla każdego pliku.

    To inny model myślenia niż w przypadku zwykłej trasy API. Zamiast uruchamiać logikę, która ładuje dane i tworzy odpowiedź, publikuje się cały katalog, który jest identyfikowany wyłącznie na podstawie ścieżki. Z tego wynikają dwie praktyczne uwagi:

    • Argument folderu jest rozstrzygany względem katalogu, z którego uruchomiono proces Node, a nie pliku zawierającego kod. Użycie ścieżki bezwzględnej, na przykład takiej utworzonej za pomocą path.join(__dirname, „..“, „uploads“), zapobiega niespodziankom, gdy aplikacja jest uruchamiana z innego katalogu roboczego.
    • Wszystko, co znajduje się w tym folderze, staje się dostępne publicznie. Montuj tylko ten katalog, który ma być udostępniony.

    Jak zapisany plik staje się adresem URL

    Gdy już skonfigurowano serwowanie statycznego, ścieżka na dysku i ścieżka w adreście URL odzwierciedlają się wzajemnie:

    File on disk:     uploads/avatars/user-42.png
    Accessible at:    https://yourapp.com/uploads/avatars/user-42.png
    

    Dzięki temu jedno-do-jednego powiązaniu frontend może bezpośrednio odwoływać się do pliku, na przykład w tagu obrazka, bez konieczności korzystania z dedykowanego punktu końcowego API do pobierania danych:

    <img src="https://yourapp.com/uploads/avatars/user-42.png" alt="User avatar" />
    

    Tutaj struktura folderów z pierwszej sekcji sprawdza się w praktyce. Takie ułożenie plików na dysku, jakie wybierzesz, jest dokładnie tym samym, które widzą klienci w adresach URL, więc uporządkowana i spójna struktura zapewnia równie uporządkowane i spójne linki. Oznacza to również, że adres URL można utworzyć na podstawie względnej ścieżki przechowywanej w bazie danych, dzięki czemu domena i prefiks znajdują się w jednej wartości konfiguracyjnej.

    Środki bezpieczeństwa dla przesyłanych przez użytkowników plików

    Przesyłanie plików jest popularnym celem nadużyć, więc stwierdzenie „przesyłanie plików działa” to dopiero punkt wyjścia. Każda z poniższych działań eliminuje konkretne zagrożenie.

    Rozszerzenie nazwy pliku to po prostu tekst wybrany przez klienta i nic nie dowodzi. Sprawdź rzeczywisty typ zawartości, najlepiej poprzez odczyt pierwszych bajtów pliku, zamiast ufać deklarowanemu typowi MIME, i przyjmij tylko to, czego wymaga dana funkcja. Na przykład endpoint do awatarów powinien przyjmować tylko obrazy, a nic więcej. Formaty takie jak SVG i HTML wymagają szczególnej ostrożności, ponieważ przeglądarki mogą wykonywać w nich zamontowane skrypty.

    Ogranicz rozmiar

    Ustal maksymalny rozmiar dla każdego przesyłanego pliku. Bez ograniczenia jeden zbyt duży żądanie, czy to przypadkowe, czy celowe, może wypełnić dysk lub przeciążyć przepustowość. Wprowadź to ograniczenie podczas analizy żądania, aby serwer przerwał odczyt wcześniej, zamiast najpierw przyjąć całą treść.

    Generuj własne nazwy plików

    Nigdy nie zapisuj pliku pod nazwą podaną przez klienta. Nazwa ta może zawierać nietypowe znaki lub sekwencje umożliwiające przechodzenie po ścieżkach, takie jak ../../config.js, które mają na celu umknięcie kontroli w katalogu przesyłania plików. Stwórz na serwerze nową nazwę, np. losowy identyfikator w połączeniu z sprawdzoną rozszerzaniem. Jako efekt uboczny zapobiega to również temu, że dwóch użytkowników przesyłających plik photo.jpg nadpisze się nawzajem.

    Zachowaj izolację katalogu przesyłania plików

    Zachowuj przesłane pliki w katalogu wyraźnie oddzielonym od kodu źródłowego i konfiguracji, upewniając się, że middleware statyczny udostępnia wyłącznie ten katalog. Błędne podłączenie korzenia projektu mogłoby umożliwić dostęp do plików środowiskowych i kodu źródłowego każdemu, kto odgadnie odpowiednią ścieżkę.

    Pomyśl o tym, który źródło dostarcza pliki

    Używanie treści dostarczonych przez użytkownika z tego samego domeny co główna aplikacja może prowadzić do problemów takich jak cross-site scripting, ponieważ złośliwy plik będzie uruchamiany pod adresem pochodzenia twojej strony. Z tego powodu wiele systemów produkcyjnych przesyła pliki z zewnętrznego magazynowania lub oddzielnego poddomeny. Wysyłanie nagłówka X-Content-Type-Options: nosniff oraz, w przypadku plików do pobrania, które nie powinny być wyświetlane w formie inline, nagłówka Content-Disposition: attachment, dodatkowo zmniejsza to ryzyko.

    Częste pytania

    Czy każdy projekt powinien używać S3 lub podobnej usługi?

    Niekoniecznie. W przypadku małych projektów lub tych służących do nauki lokalny dysk jest prostszy i w pełni wystarczający. Zewnętrzne magazynowanie staje się istotne, gdy uruchamiasz aplikację na platformie z tymczasowym systemem plików lub gdy musisz obsługiwać kilka instancji, które powinny mieć dostęp do tych samych plików.

    Czy express.static wymaga dodatkowego pakietu?

    Nie, jest to wbudowane w Express. Analiza przesyłanych plików z formularzy typu multipart zazwyczaj wymaga biblioteki takiej jak Multer, ale serwowanie już zapisanych plików nie wymaga tego.

    Czy dopuszczalne jest zachowanie oryginalnej nazwy pliku?

    Lepiej tego nie robić. Nazwa wygenerowana przez serwer unika dziwnych znaków, kolizji identycznych nazw oraz podstępów polegających na specjalnie skonstruowanych nazwach plików. Jeśli użytkownicy muszą zobaczyć oryginalną nazwę, należy ją przechowywać jako metadane w bazie danych.

    Czy dostęp do przesłanych plików można ograniczyć?

    Tak, ale nie tylko za pomocą express.static, który udostępnia plik każdemu, kto zna jego URL. Pliki prywatne wymagają dedykowanej ścieżki, która sprawdza autoryzację i uprawnienia przed przesyłaniem pliku, lub w przypadku zewnętrznego przechowywania – krótkotrwałych linków generowanych wyłącznie dla uprawnionych użytkowników.

    Podsumowanie

    Skuteczna funkcja przesyłania plików odpowiada kolejno na cztery pytania. Gdzie znajduje się plik: na dysku serwera ze względu na prostotę, czy w magazynie obiektów, ponieważ przetrwa aktualizacje i umożliwia skalowanie? Jak aplikacja znajdzie go później: za pomocą uporządkowanej struktury folderów oraz ścieżki względnej przechowywanej w bazie danych? Jak przeglądarka pobra go: za pośrednictwem express.static powiązanego z jasnym prefiksem URL, czy też za pomocą ochronionej trasy przeznaczonej dla plików prywatnych? A co zapewnia jego bezpieczeństwo: weryfikowane typy plików, limity wielkości, nazwy generowane przez serwer oraz ściśle określone, najlepiej oddzielne miejsce przechowywania. Gdy te kwestie zostaną rozwiązane, reszta funkcjonalności to już głównie kwestie techniczne.

    Literatura pokrewna

  • Dlaczego zadania @Cron w NestJS wykonywają się jednokrotnie na każdą replikę i jak to naprawić — Dowiedz się, dlaczego harmonogramy @Cron działające wewnątrz procesu mnożą się, gdy usługa NestJS jest skalowana, oraz jak zewnętrzny wyzwalacz, operacje idempotentne i zabezpieczone punkty końcowe mogą to rozwiązać.