Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!
⛔ Potrzebujesz wsparcia? Oceny CV? A może Code Review? ✅ Dołącz do naszej społeczności na Discordzie!
Jako front end developer też możesz przyczynić się do tworzenia bezpiecznych aplikacji internetowych. Walidacja danych wprowadzanych przez użytkownika obejmuje nie tylko dane z formularzy, ale też wgrywane pliki. W tym artykule znajdziesz kilka kroków, które pomogą Ci zabezpieczyć aplikację przed potencjalnymi zagrożeniami.
Odczytywanie zawartości wybranych przez użytkownika plików umożliwia FileReader. Jest to rozwiązanie wbudowane w JavaScript i działa po stronie klienta (w przeglądarce).
Jednak dane pliku takie jak nazwa czy wielkość są dostępne wcześniej – w obiekcie przekazywanym do naszej funkcji (callbacku) po uruchomieniu zdarzenia change (czyli wgraniu pliku). Właśnie te informacje pozwolą nam wstępnie zwalidować plik po stronie front endu zanim jeszcze zrobimy coś z jego zawartością za pomocą FileReadera lub prześlemy na serwer.
Poniżej widzisz przykład prostego interfejsu, gdzie umożliwiamy użytkownikowi wgranie pliku CSV.
<form class="uploader"> <label class="uploader__label"> Wybierz plik CSV: <input class="uploader__input" type="file" accept="text/csv, .csv" /> </label> </form>
Zwróć uwagę, że w inpucie zastosowaliśmy atrybut accept, który determinuje typ ( MIME type) wgrywanego pliku oraz jego rozszerzenie. Dlaczego nie jest to wystarczające zabezpieczenie – o tym już za chwilę.
document.querySelector('.uploader__input').addEventListener('change', validateFiles); function validateFiles(e) { const [file] = e.target.files; console.log(file) }
W pliku JavaScript zapisujemy informacje o przesłanym pliku w zmiennej file (używamy destrukturyzacji, stąd nawiasy kwadratowe wokół nazwy) i wyświetlamy je w konsoli – będzie to obiekt m.in. z takimi właściwościami jak: name, size czy type. Nie tworzymy na razie instancji FileReadera – nie jest nam ona potrzebna do walidacji przedstawionej poniżej.
Podstawowym zabezpieczeniem jest sprawdzenie zarówno rozszerzenia pliku, jak i jego typu zawartości ( MIME type). Pamiętaj, że sama nazwa pliku może zostać sfałszowana, więc sprawdzenie typu jest bezpieczniejsze.
Dlaczego atrybut accept w HTML-u jest niewystarczający?
<input class="uploader__input" type="file" accept="text/csv, .csv" />
Na poniższej grafice widzisz, że użycie tego atrybutu nie zablokowało możliwości wgrywania innych plików. Eksplorator jedynie automatycznie wyświetlił pliki o odpowiednim rozszerzeniu, nadal jednak możemy wgrać inny plik albo przez wprowadzenie jego nazwy, albo zmianę filtrowania na wszystkie typy plików.
Dodatkowo w DevTools w przeglądarce możemy wyszukać atrybut accept w DOM-ie, usunąć go lub nadpisać i w ten sposób utorować sobie drogę do wgrania innych plików.
Sprawdzajmy więc nazwę i typ również w JavaScripcie. Możemy to osiągnąć, korzystając z informacji dostępnych po wybraniu pliku przez użytkownika:
function validateFiles(e) { const [file] = e.target.files; if (file && file.name.includes('.csv') && file.type === "text/csv") { // możemy zająć się obsługą pliku const reader = new FileReader(); reader.onload = function(event) { console.log(event.target.result) } reader.readAsText(file) } else { console.log("Nieprawidłowy plik, wgraj plik CSV.") } }
Miej na uwadze, że typy MIME mogą się różnić np. w zależności od wersji programu, z którego pochodzą pliki ( przykładem jest Excel i rozszerzenia .xls oraz .xlsx), a czasem nawet systemu operacyjnego. Chodzi o to, by przypadkiem nie zablokować użytkownikowi możliwości wgrywania plików tylko dlatego, że nie obsłużyliśmy odpowiedniego typu MIME.
Ogranicz liczbę i wielkość przesyłanych plików, aby zmniejszyć ryzyko m.in. ataku poprzez zapełnienie serwera ( denial of service, DoS).
Przykładowo jeśli tworzysz narzędzie do przetwarzania ikon SVG, to użytkownicy nie będą wgrywać plików wielkości kilkunastu megabajtów i więcej. Możesz więc ustawić limit, by nie dopuścić do przesyłania dużych, potencjalnie szkodliwych plików.
W pliku HTML dodajemy atrybut multiple, który umożliwi przesłanie większej liczby plików.
<input class="uploader__input" type="file" accept="text/csv, .csv" multiple />
function validateFiles(e) { const [file] = e.target.files; const filesCount = e.target.files.length if (filesCount > 5) { console.log("Przekroczono limit plików. Wgraj maksymalnie 5 plików.") return } //... }
Choć e.target.files nie przechowuje tablicy (to tzw. obiekt FileList), to możemy sprawdzić jego długość (length), czyli liczbę zwartych w nim elementów. W ten sposób, jeśli liczba plików zostanie przekroczona, zakończymy działanie funkcji alertem dla użytkownika i wczesnym zwrotem (ang. early return).
Wielkość pliku przechowywana jest we właściwości size i podana w bajtach. W przypadku ograniczenia liczby wgrywanych plików do jednego wystarczy prosty warunek:
const [file] = e.target.files; //... if (file.size > 100000) { console.log("Przekroczono limit wielkości pliku.") return }
Natomiast przy większej liczbie plików najpierw musimy zsumować ich rozmiar. Możemy to zrobić za pomocą metody reduce, lecz najpierw trzeba zamienić wartość przechowywaną przez e.target.files na tablicę (poniżej zamiana następuje przez spread):
function validateFiles(e) { const [file] = e.target.files; const filesCount = e.target.files.length const filesSize = [...e.target.files].reduce((totalSize, file) => { return totalSize + file.size }, 0) //... if (filesSize > 100000) { console.log("Przekroczono łączny limit wielkości plików.") return } //... }
Bardzo długa nazwa pliku potencjalnie prowadzi do tzw. buffer overflow, co może zostać wykorzystane do zaatakowania programu. Warto więc walidować liczbę znaków nazwy pliku pod kątem rozsądnej długości.
const [file] = e.target.files; //... if (file.name.length > 45) { console.log("Plik ma za długą nazwę. Dozwolona liczba znaków: 45.") return }
Jak wykonać sprawdzenie wielu plików w pętli, przedstawiam na przykładzie poniżej.
Prócz długości nazwy, należałoby ograniczyć dozwolone znaki w nazwie pliku do liter, liczb i paru znaków specjalnych (jak np. minus, podkreślnik, spacja czy kropka). Wykluczone powinny zostać znaki umożliwiające wstrzyknięcie kodu (code injection) lub modyfikację ścieżki do pliku, czyli np.:
Do tego zabiegu posłużą nam wyrażenia regularne. Do sprawdzenia nazw kilku wgranych plików wykorzystamy pętlę. Nazwy nieprawidłowych plików zapiszemy w tablicy wrongFileNames, aby móc wyświetlić je użytkownikowi:
function validateFiles(e) { //... const permittedCharsRegex = /^[a-zA-Z0-9-._s]+$/; const wrongFileNames = []; for (const file of e.target.files) { if (!permittedCharsRegex.test(file.name)) { wrongFileNames.push(file.name) } } if (wrongFileNames.length > 0) { console.log(`Następujące pliki zawierają niedozwolone znaki: ${wrongFileNames.join(', ')}. Dozwolone: litery, liczby, kropka, minus, podkreślnik i spacja.`) return } //... }
Jeśli zapisujesz też pliki na serwerze, możesz zmienić ich nazwy na unikalne i niedające się przewidzieć, aby uniemożliwić atakującym wykonywanie skryptów poprzez dotarcie do nazw plików oraz manipulowanie nimi. Jeżeli jednak pracujesz tylko po stronie front endu, nie będziesz się tym zajmować.
Możesz dokonać własnej refaktoryzacji, na przykład gromadząc ograniczenia oraz komunikaty dla użytkownika w tablicy obiektów. Wyrażenie regularne sprawdzające dozwolone znaki możesz też rozszerzyć np. o alfabet łaciński.
Walidacja, której dokonaliśmy, to ułamek działań, jakie trzeba podjąć, by zabezpieczyć program czy serwer. Mimo to warto podejmować działania nie tylko po stronie back endu. Gdy zastosujesz nawet prostą walidację w swoich projektach do portfolio, pokażesz, że jesteś programistą świadomym zagrożeń i podejmujesz kroki, by im przeciwdziałać.
Udostępnij ten artykuł:
Potrzebujesz cotygodniowej dawki motywacji?
Zapisz się i zgarnij za darmo e-book o wartości 39 zł!
PS. Zazwyczaj rozsyłam 1-2 wiadomości na tydzień. Nikomu nie będę udostępniał Twojego adresu email.
Mam coś dla Ciebie!
W każdy piątek rozsyłam motywujący do nauki programowania newsletter!
Dodatkowo od razu otrzymasz ode mnie e-book o wartości 39 zł. To ponad 40 stron konkretów o nauce programowania i pracy w IT.
PS Zazwyczaj wysyłam 1-2 wiadomości na tydzień. Nikomu nie będę udostępniał Twojego adresu e-mail.