Bezpieczeństwo aplikacji webowych: hash, walidacja i uwierzytelnianie
Bezpieczeństwo web nie jest opcjonalne. Wraz z rosnącą liczbą cyberzagrożeń programiści muszą wbudowywać zabezpieczenia w każdą warstwę swoich aplikacji. Niniejszy przewodnik omawia kluczowe praktyki, które warto wdrożyć już dziś.
Bezpieczeństwo haseł
Nigdy nie przechowuj haseł w postaci jawnej
Zawsze należy obliczać hash haseł przy użyciu nowoczesnych algorytmów, takich jak bcrypt, Argon2 czy scrypt. Algorytmy te zostały celowo zaprojektowane jako wolne, dzięki czemu ataki brute-force stają się nieopłacalne.
// Dobrze: użycie bcrypt
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash(password, 12);
Porównanie algorytmów hashujących
Nie wszystkie algorytmy hashujące są sobie równe. Wybór odpowiedniego zależy od modelu zagrożeń i przypadku użycia:
| Algorytm | Rozmiar wyjścia | Szybkość | Zastosowanie | Status bezpieczeństwa |
|---|---|---|---|---|
| MD5 | 128 bitów | Bardzo szybki | Sumy kontrolne, hashe niezwiązane z bezpieczeństwem | Kryptograficznie złamany |
| SHA-256 | 256 bitów | Szybki | Integralność danych, podpisy cyfrowe | Bezpieczny |
| bcrypt | 184 bity | Wolny (regulowany) | Przechowywanie haseł | Bezpieczny |
| Argon2 | Konfigurowalny | Wolny (regulowany) | Przechowywanie haseł (nowoczesne) | Zalecany dla nowych projektów |
bcrypt i Argon2 są celowo wolne — to cecha, a nie wada. Każda operacja obliczania hashu trwa od dziesiątek do setek milisekund, co sprawia, że ataki brute-force na dużą skalę stają się ekonomicznie nieopłacalne.
Zrozumienie entropii hasła
Siłę hasła można mierzyć matematycznie za pomocą entropii: entropy = log2(charset_size^length). Hasło składające się z 8 małych liter (26 znaków) ma około 37,6 bita entropii. Hasło o długości 16 znaków, mieszające wielkie i małe litery, cyfry oraz symbole (95 znaków), osiąga około 105 bitów — wykładniczo trudniejsze do złamania. Dlatego dla większości użytkowników długość liczy się bardziej niż złożoność. Aby zgłębić matematykę stojącą za siłą hasła, warto zajrzeć do naszego przewodnika entropia hasła wyjaśniona.
Korzystaj z menedżera haseł
Warto zachęcać użytkowników do korzystania z menedżera haseł. Hasła wybierane przez ludzi mają tendencję do podążania przewidywalnymi wzorcami, które atakujący wykorzystują w atakach słownikowych. Menedżery haseł generują naprawdę losowe ciągi i eliminują powtarzanie haseł między usługami — jeden z najczęstszych wektorów ataków credential stuffing.
Stosuj wystarczającą liczbę rund soli
Liczba rund soli określa koszt obliczeniowy. Wyższa wartość oznacza większe bezpieczeństwo, ale wolniejsze działanie. 10–12 rund stanowi dobry kompromis dla większości aplikacji.
Walidacja danych wejściowych
Waliduj zarówno po stronie klienta, jak i serwera
Walidacja po stronie klienta poprawia UX, lecz walidacja po stronie serwera jest niezbędna dla bezpieczeństwa. Nigdy nie należy ufać danym pochodzącym od klienta.
Sanityzuj wszystkie dane wprowadzane przez użytkowników
Ataki typu injection można zablokować, sanityzując dane wejściowe:
- Stosuj zapytania parametryzowane dla SQL
- Escapuj wyjście HTML, aby zapobiec XSS
- Rygorystycznie waliduj przesyłane pliki
Konkretne przykłady ataków
Zrozumienie rzeczywistych ataków pomaga się przed nimi bronić. Wyobraźmy sobie formularz komentarzy, który renderuje dane użytkownika bezpośrednio do HTML. Atakujący wysyła:
<script>alert('xss')</script>
Jeśli aplikacja wyrenderuje to bez escapowania, skrypt wykona się w przeglądarce każdego odwiedzającego — kradnąc ciasteczka, przekierowując użytkowników lub wstrzykując keyloggery. Rozwiązanie: zawsze koduj wyjście kontekstowo. Do sanityzacji HTML warto użyć bibliotek takich jak DOMPurify.
SQL injection jest równie groźny. W formularzu logowania atakujący wpisuje jako nazwę użytkownika:
' OR 1=1 --
Jeśli zapytanie zostanie zbudowane przez konkatenację stringów ("SELECT * FROM users WHERE username='" + input + "'"), całkowicie omija to uwierzytelnianie. Sekwencja -- zamienia resztę zapytania w komentarz. Rozwiązanie: zawsze stosuj zapytania parametryzowane (zwane też prepared statements). Wszystkie liczące się biblioteki bazodanowe je wspierają:
// ŹLE: konkatenacja stringów
db.query(`SELECT * FROM users WHERE username='${input}'`);
// DOBRZE: zapytanie parametryzowane
db.query('SELECT * FROM users WHERE username = $1', [input]);
Content Security Policy (CSP)
Jako warstwa obrony w głąb (defense-in-depth) warto wdrożyć nagłówki Content Security Policy. CSP informuje przeglądarkę, które źródła treści są zaufane, skutecznie blokując inline scripts i nieautoryzowane ładowanie zasobów. Nawet jeśli w kodzie istnieje podatność XSS, rygorystyczna polityka CSP może uniemożliwić wykonanie wstrzykniętego skryptu. Warto zacząć od Content-Security-Policy: default-src 'self' i stopniowo dodawać wyjątki w miarę potrzeb.
Funkcje hashujące
Wybór odpowiedniego hashu
Różne zastosowania wymagają różnych funkcji hashujących:
| Zastosowanie | Zalecane |
|---|---|
| Hasła | bcrypt, Argon2 |
| Integralność | SHA-256 |
| Sumy kontrolne | SHA-256, MD5 (poza kontekstem bezpieczeństwa) |
| Szybki hash | BLAKE3 |
Zrozumienie wyjścia hashu i kolizji
MD5 zwraca hash o długości 128 bitów (32 znaki szesnastkowe), natomiast SHA-256 generuje hash 256-bitowy (64 znaki szesnastkowe). Ta różnica ma znaczenie: większa przestrzeń wyjściowa oznacza wykładniczo więcej możliwych wartości hashu, co czyni kolizje znacznie mniej prawdopodobnymi. Kolizja zachodzi, gdy dwa różne wejścia generują ten sam hash — atakujący zdolny do wytworzenia kolizji może fałszować podpisy cyfrowe lub manipulować zweryfikowanymi danymi.
Kolizje MD5 można generować w ciągu sekund na współczesnym sprzęcie. SHA-256 pozostaje odporny na kolizje i nie są znane praktyczne ataki. Dlatego dobór właściwego algorytmu do właściwego kontekstu jest kluczowy:
- Sumy kontrolne i deduplikacja: MD5 jest akceptowalny, gdy bezpieczeństwo nie ma znaczenia
- Integralność danych i podpisy: SHA-256 zapewnia silną odporność na kolizje
- Przechowywanie haseł: bcrypt lub Argon2, które stosują dodawanie soli i celową powolność
HMAC do uwierzytelniania wiadomości
Gdy konieczna jest weryfikacja zarówno integralności, jak i autentyczności wiadomości, należy użyć HMAC (Hash-based Message Authentication Code). HMAC łączy funkcję hashującą z tajnym kluczem, dzięki czemu tylko strony znające klucz mogą wygenerować lub zweryfikować tag. Jest to niezbędne do uwierzytelniania API, weryfikacji webhook oraz bezpiecznego generowania token.
Nigdy nie używaj MD5 ani SHA-1 do celów bezpieczeństwa
MD5 i SHA-1 są kryptograficznie złamane do zastosowań związanych z bezpieczeństwem. Do hashowania kryptograficznego należy używać SHA-256 lub SHA-3.
HTTPS wszędzie
Co naprawdę robi TLS
TLS (Transport Layer Security) zapewnia trzy kluczowe ochrony: szyfrowanie podczas przesyłania (uniemożliwia podsłuch), uwierzytelnianie serwera (potwierdza, że rozmawiamy z prawdziwym serwerem, a nie z podszywającym się) oraz integralność danych (wykrywa wszelkie modyfikacje w trakcie transmisji). Bez TLS wszystkie dane przesyłane między użytkownikami a serwerem — hasła, token, dane osobowe — wędrują w postaci jawnej.
Zawsze używaj TLS
- Pozyskuj certyfikaty od zaufanych CA (Let’s Encrypt jest darmowy i w pełni zautomatyzowany)
- Przekierowuj HTTP na HTTPS
- Stosuj nagłówki HSTS
- Aktualizuj wersje TLS
HSTS i mixed content
Nagłówki HTTP Strict Transport Security (HSTS) instruują przeglądarki, aby łączyły się wyłącznie przez HTTPS, nawet gdy użytkownik wpisze http://. Ustawienie Strict-Transport-Security: max-age=31536000; includeSubDomains wymusza tę zasadę przez cały rok dla wszystkich subdomen. Zapobiega to atakom SSL stripping, w których atakujący degraduje połączenie do HTTP.
Warto uważać na ostrzeżenia o mixed content: jeśli strona HTTPS ładuje obrazy, skrypty lub arkusze stylów przez HTTP, przeglądarka je zablokuje lub wyświetli ostrzeżenie. Należy przejrzeć strony pod kątem zakodowanych na sztywno adresów http:// i stosować ścieżki zależne od protokołu lub wymuszać HTTPS dla wszystkich zasobów.
Uwierzytelnianie
Wdróż rate limiting
Atakom brute-force można zapobiec dzięki rate limiting:
- Ograniczaj liczbę prób logowania na adres IP
- Dodawaj opóźnienia po nieudanych próbach
- Stosuj CAPTCHA przy podejrzanej aktywności
Podstawy uwierzytelniania JWT
JSON Web Tokens (JWT) udostępniają bezstanowy mechanizm uwierzytelniania o strukturze header.payload.signature. Serwer podpisuje token tajnym kluczem, a klienci dołączają go do kolejnych żądań. Ponieważ token zawiera claim użytkownika, serwer nie musi przy każdym żądaniu sprawdzać stanu sesji — co sprawia, że JWT dobrze sprawdzają się w systemach rozproszonych i mikroserwisach.
Należy zawsze ustawiać krótkie czasy wygaśnięcia dla access token (np. 15 minut) i używać refresh token do uzyskiwania nowych access token. Refresh token trzeba przechowywać bezpiecznie (cookies httpOnly, nie localStorage) oraz wdrożyć rotację token, aby każdy refresh token mógł zostać użyty tylko raz.
Uwierzytelnianie wieloskładnikowe (MFA)
MFA nie jest już opcjonalne dla żadnej poważnej aplikacji. Wymaganie drugiego składnika — kodów TOTP (Google Authenticator), kluczy sprzętowych (YubiKey) lub powiadomień push — drastycznie zmniejsza skutki naruszenia haseł. Nawet jeśli atakujący zdobędzie poprawne dane uwierzytelniające, bez drugiego składnika nie zdoła się uwierzytelnić.
Zapobieganie session fixation
Ataki typu session fixation polegają na tym, że atakujący ustawia znany identyfikator sesji, zanim użytkownik się uwierzytelni. Po zalogowaniu atakujący wykorzystuje ten sam identyfikator sesji do przejęcia uwierzytelnionej sesji. Można temu zapobiec, zawsze regenerując identyfikator sesji po pomyślnym uwierzytelnieniu i unieważniając stary.
Stosuj bezpieczne zarządzanie sesjami
- Generuj kryptograficznie losowe identyfikatory sesji
- Ustawiaj flagi secure i httpOnly na ciasteczkach
- Wdrażaj limity czasu sesji
- Unieważniaj sesje przy wylogowaniu
Lista kontrolna nagłówków bezpieczeństwa
Wdrażanie odpowiednich nagłówków odpowiedzi HTTP to jeden z najskuteczniejszych i najmniej kosztownych sposobów na utwardzenie aplikacji. Poniżej skrócona tabela kluczowych nagłówków bezpieczeństwa:
| Nagłówek | Cel | Przykładowa wartość |
|---|---|---|
| Content-Security-Policy | Zapobiega XSS i wstrzykiwaniu danych | default-src 'self' |
| Strict-Transport-Security | Wymusza połączenia HTTPS | max-age=31536000; includeSubDomains |
| X-Content-Type-Options | Zapobiega MIME type sniffing | nosniff |
| X-Frame-Options | Zapobiega clickjacking | DENY |
| Referrer-Policy | Kontroluje informacje o Referer | strict-origin-when-cross-origin |
Nagłówki te można ustawić na poziomie serwera webowego (Nginx, Apache), na poziomie CDN/edge (Cloudflare, Vercel) lub w ramach framework aplikacyjnego. Warto przetestować nagłówki za pomocą narzędzi takich jak securityheaders.com. Cel to ocena A+ — większość tych nagłówków to pojedyncza linia konfiguracji i ich wdrożenie nic nie kosztuje.
Korzystanie z naszych narzędzi bezpieczeństwa
Warto zapoznać się z naszymi narzędziami bezpieczeństwa wspierającymi pracę programisty:
- Generator MD5 hash — do sum kontrolnych i systemów legacy
- Generator UUID — do bezpiecznych losowych identyfikatorów
- Generator losowych haseł — do generowania silnych haseł
Aby uzyskać szerszy obraz tego, jak narzędzia do kodowania, hashowania i konwersji wpisują się w przepływ pracy programistycznej, warto sięgnąć po nasz przewodnik po niezbędnych narzędziach dla deweloperów.
Najczęściej zadawane pytania
Jaka jest najczęstsza podatność bezpieczeństwa w aplikacjach webowych?
Cross-Site Scripting (XSS) pozostaje najpowszechniejszą podatnością web według OWASP. Występuje, gdy aplikacje umieszczają niezaufane dane na stronach bez właściwej walidacji. XSS można zapobiec, sanityzując wszystkie dane wprowadzane przez użytkowników, stosując nagłówki Content Security Policy oraz kodując wyjście zależnie od kontekstu (HTML, JavaScript, URL lub CSS).
Czy MD5 jest nadal bezpieczny do hashowania haseł?
Nie — MD5 nigdy nie powinien być używany do hashowania haseł. Jest obliczeniowo szybki, przez co jest podatny na ataki brute-force i ataki z użyciem tablic tęczowych. Współczesne karty graficzne potrafią obliczać miliardy hashy MD5 na sekundę. Zamiast tego należy używać bcrypt, scrypt lub Argon2, które są celowo wolne i mają wbudowane dodawanie soli, aby przeciwdziałać atakom.
Jak długie powinno być bezpieczne hasło w 2026 roku?
Zaleca się minimum 12 znaków, ale 16+ znaków zapewnia znacznie silniejszą ochronę. Długość liczy się bardziej niż złożoność — 20-znakowy passphrase typu „correct-horse-battery-staple” jest silniejszy od krótkiego, złożonego hasła w rodzaju „P@ss1!”. Niezależnie od długości hasła, w przypadku kont krytycznych warto włączyć uwierzytelnianie wieloskładnikowe (MFA).
Jaka jest różnica między szyfrowaniem a hashowaniem?
Szyfrowanie jest odwracalne — dane można odszyfrować z powrotem do pierwotnej postaci za pomocą klucza. Hashowanie jest jednokierunkowe — z hashu nie da się odzyskać danych pierwotnych. Szyfrowania należy używać do danych, które trzeba odczytywać (jak przechowywane dane użytkowników), a hashowania — do danych, które wystarczy weryfikować (jak hasła i sumy kontrolne).
Czy warto budować własny system uwierzytelniania?
Nie — budowanie uwierzytelniania od zera jest ryzykowne i podatne na błędy. Lepiej korzystać ze sprawdzonych framework i usług, takich jak Auth0, Firebase Auth czy Supabase Auth. Obsługują one przechowywanie haseł, zarządzanie sesjami, rotację token, MFA oraz ochronę przed brute-force. Czas programistów warto przeznaczyć na unikalne funkcje aplikacji.
Podsumowanie
Bezpieczeństwo to proces ciągły, a nie jednorazowe zadanie. Należy śledzić nowe podatności, regularnie audytować kod i przestrzegać zasady najmniejszych uprawnień. Użytkownicy powierzają nam swoje dane — warto uszanować to zaufanie solidnymi praktykami bezpieczeństwa.