Krótko: Hasła w PHP przechowujesz za pomocą password_hash() i sprawdzasz je za pomocą password_verify(). Wartość domyślna PASSWORD_DEFAULT używa obecnie bcrypt, od PHP 8.4 ze współczynnikiem kosztu 12; tam, gdzie PHP jest skompilowane z Argon2, dobrą alternatywą jest PASSWORD_ARGON2ID. Stare skróty MD5 lub SHA-1 zastępujesz, osadzając je natychmiast w password_hash, a przy następnym logowaniu zamieniając na skrót bezpośredni.
Co wyciek bazy danych ma wspólnego z Państwa skrótami
Po włamaniu na serwer WWW baza danych jest jednym z pierwszych celów. Kto, jak to możliwe przy PHP2Shell, może wykonać kod na serwerze, odczytuje też dane dostępowe aplikacji, a tym samym tabelę z kontami użytkowników. To, czy zawarte w niej hasła staną się czytelne po godzinach czy po latach, rozstrzyga się nie w dniu ataku, lecz podczas programowania.
Zaskakująco wiele aplikacji nadal przechowuje hasła metodami, które nigdy nie były do tego przeznaczone: MD5, SHA-1, zwykły SHA-256, a nawet zaszyfrowane w sposób odwracalny. Te metody są szybkie i na tym polega problem. Atakujący mogą przy ich użyciu sprawdzać bardzo wiele kandydatów na sekundę.
PHP od dawna oferuje rozwiązanie, które liczy powoli, automatycznie generuje salt dla każdego hasła i zapisuje metodę oraz parametry w samym skrócie. Wystarczy konsekwentnie z niego korzystać.
password_hash w PHP: bcrypt czy Argon2id?
password_hash() zna kilka metod. Wybór według podręcznika PHP:
| Stała | Metoda | Wskazówka |
|---|---|---|
PASSWORD_DEFAULT |
obecnie bcrypt | Wartość domyślna, w przyszłych wersjach może się zmienić |
PASSWORD_BCRYPT |
bcrypt | Współczynnik kosztu od PHP 8.4 domyślnie 12, wcześniej 10; liczą się tylko pierwsze 72 bajty |
PASSWORD_ARGON2I |
Argon2i | od PHP 7.2, tylko jeśli PHP zbudowano z obsługą Argon2 |
PASSWORD_ARGON2ID |
Argon2id | od PHP 7.3, ten sam warunek |
OWASP zaleca Argon2id jako pierwszy wybór, z co najmniej 19 MiB pamięci, dwoma przebiegami i jednym wątkiem, a bcrypt przede wszystkim dla istniejących systemów. Dla większości aplikacji PHP różnica w praktyce jest mniejsza niż różnica między którąkolwiek z tych metod a MD5. Ważniejsze jest, aby w ogóle używać jednej z nich i regularnie dostosowywać parametry.
To, czy Argon2id jest dostępny, pokazuje krótki test:
<?php
var_dump(defined('PASSWORD_ARGON2ID'));
var_dump(password_algos());
Zapis i sprawdzanie: kompletny wzorzec
Poniższy przykład pokazuje rejestrację i logowanie z automatyczną aktualizacją przestarzałych skrótów. Kontrola długości chroni przed limitem 72 bajtów w bcrypt:
<?php
declare(strict_types=1);
const PASSWORT_ALGO = PASSWORD_DEFAULT;
function passwortSpeichern(string $passwort): string
{
if (strlen($passwort) > 72) {
throw new InvalidArgumentException('Passwort zu lang für das gewählte Verfahren.');
}
return password_hash($passwort, PASSWORT_ALGO);
}
function passwortPruefen(string $eingabe, string $gespeicherterHash, callable $hashAktualisieren): bool
{
if (!password_verify($eingabe, $gespeicherterHash)) {
return false;
}
if (password_needs_rehash($gespeicherterHash, PASSWORT_ALGO)) {
$hashAktualisieren(password_hash($eingabe, PASSWORT_ALGO));
}
return true;
}
strlen() liczy bajty, a nie znaki. Jest to tu zamierzone, ponieważ limit bcrypt odnosi się do bajtów. Kolumna w bazie danych powinna według podręcznika mieścić więcej niż 60 znaków, 255 to dobry wybór. Nigdy nie ustawiaj własnego saltu: ta opcja jest przestarzała i od PHP 8.0 jest ignorowana.
Wraz z przejściem na PHP 8.4 domyślny współczynnik kosztu bcrypt rośnie z 10 do 12. Istniejące skróty są dzięki password_needs_rehash() automatycznie podnoszone przy następnym logowaniu. W silnie obciążonych systemach przed zmianą warto krótko przetestować obciążenie logowania.
Zmierz współczynnik kosztu na swoim serwerze produkcyjnym. Pojedyncze sprawdzenie powinno wyraźnie kosztować czas obliczeniowy, ale pozostawać wyraźnie poniżej sekundy, aby logowania pod obciążeniem nie stały się wąskim gardłem.
Zastępowanie starych skrótów MD5 i SHA-1
Właściwym wyzwaniem jest zwykle zasób istniejący. Samych haseł nie znasz, więc nie możesz ich po prostu zhaszować od nowa. Czekanie, aż każdy użytkownik zaloguje się ponownie, pozostawia nieaktywne konta bez ochrony. OWASP opisuje dlatego postępowanie dwuetapowe:
- Osadzić natychmiast: każdą istniejącą wartość MD5 lub SHA-1 owinąć skryptem w
password_hash()i zapisać z oznaczeniem, na przykład w kolumniehash_typ. - Sprawdzać przy logowaniu: dla oznaczonych kont przeliczyć wprowadzoną wartość najpierw starą metodą, a następnie sprawdzić ją za pomocą
password_verify()względem owiniętej wartości. - Zastąpić bezpośrednio: po udanym logowaniu wygenerować normalny skrót z jawnego hasła i usunąć oznaczenie.
- Uporządkować pozostałość: konta, które po ustalonym terminie nadal mają owinięte skróty, ustawić na reset hasła.
<?php
// Etap 1 (jednorazowo skryptem): $neu = password_hash($alterMd5Wert, PASSWORD_DEFAULT);
// Etapy 2 i 3 przy logowaniu:
if ($konto['hash_typ'] === 'md5-umhuellt') {
$ok = password_verify(md5($eingabe), $konto['passwort_hash']);
if ($ok) {
$speichere(password_hash($eingabe, PASSWORD_DEFAULT), 'direkt');
}
}
Owinięte skróty są lepsze niż gołe wartości MD5, ale słabsze niż skróty bezpośrednie. To rozwiązanie przejściowe, a nie cel. Po incydencie, w którym mogła zostać dotknięta baza danych, ta zmiana nie zastępuje wezwania użytkowników do zmiany haseł. Jakie ślady wskazują na taki incydent, opisuje artykuł Czy już zostałem skompromitowany?.
Pepper, szyfrowanie i inne błędne drogi
Niektóre pomysły brzmią jak więcej bezpieczeństwa, ale dają niewiele lub szkodzą:
- Własne konstrukcje skrótów, takie jak wielokrotny SHA-256 z samodzielnie wygenerowanym saltem, są szybsze niż bcrypt, a przez to słabsze.
- Zaszyfrowane hasła są odwracalne. Jeśli klucz leży na tym samym serwerze, szyfrowanie po włamaniu prawie nie chroni.
- Pepper, czyli tajna wartość dodatkowa dla wszystkich haseł, może być dodatkową warstwą, jeśli jest przechowywany oddzielnie od bazy danych. Nie zastępuje jednak dobrej metody haszowania i utrudnia późniejsze zmiany.
- Wstępne haszowanie przed bcrypt, aby obejść limit 72 bajtów, niesie według OWASP własne ryzyka. Wyraźny limit długości lub Argon2id jest prostszy.
Także przy zasadach dla nowych haseł warto zerknąć na stan techniki: długość chroni bardziej niż skomplikowane wymogi dotyczące znaków specjalnych, a hasła bardzo częste lub takie, które już pojawiły się w wyciekach danych, powinny być odrzucane. Wymuszone zmiany w stałych odstępach bez powodu prowadzą, jak pokazuje doświadczenie, raczej do przewidywalnych wzorców niż do silniejszych haseł.
W systemach logowania hasła nigdy nie występują same: uwierzytelnianie dwuskładnikowe dla administratorów i hamulec wobec masowych prób logowania uzupełniają ich przechowywanie. Oba tematy omawia poradnik Ochrona logowania administratora przed atakami brute force. Dla linków resetujących i kodów jednorazowych obowiązują inne zasady, zob. Bezpieczne liczby losowe i tokeny w PHP. Przegląd dalszych funkcji daje artykuł Wbudowane funkcje bezpieczeństwa PHP.
Jak konkretnie pomaga PHP SOS?
- Sprawdzamy, jak Państwa aplikacja i wtyczki przechowują hasła, i znajdujemy przestarzałe metody także w funkcjach pobocznych.
- Planujemy zmianę dla zasobu istniejącego z owinięciem, migracją przy logowaniu i terminem dla kont pozostałych.
- Dostosowujemy współczynnik kosztu lub parametry Argon2 do Państwa serwerów.
- Po incydencie wspieramy w decyzji o resetach haseł i ich technicznym wdrożeniu.
Kontrolę własnego oprogramowania PHP oferujemy jako audyt bezpieczeństwa i test penetracyjny.
Częste pytania
Jakiego algorytmu używa password_hash z PASSWORD_DEFAULT?
Obecnie bcrypt. Podręcznik PHP zaznacza, że wartość domyślna może się zmienić, gdy pojawią się silniejsze metody. Dlatego kolumna w bazie danych powinna mieścić więcej niż 60 znaków; podręcznik wskazuje 255 jako dobry wybór.
Czy hasła należy szyfrować zamiast haszować?
Nie. Zaszyfrowane hasła można odczytać z powrotem za pomocą klucza, a ten klucz zwykle leży na tym samym serwerze. Skrót hasła to ulica jednokierunkowa: aplikacja może tylko sprawdzić, czy wprowadzona wartość pasuje.
Jaki powinien być współczynnik kosztu w bcrypt?
Od PHP 8.4 wartość domyślna to 12, wcześniej 10. OWASP podaje 10 jako dolną granicę i zaleca, aby iść tak wysoko, jak pozwalają serwery. Zmierz na własnym sprzęcie, jak długo trwa jedno sprawdzenie.
Co oznacza limit 72 bajtów w bcrypt?
bcrypt uwzględnia tylko pierwsze 72 bajty hasła, wszystko dalej jest obcinane. W przypadku znaków diakrytycznych jeden znak często zajmuje dwa bajty. Dlatego jawnie ogranicz długość lub użyj Argon2id, jeśli Państwa PHP jest z nim skompilowane.
Czy użytkownicy muszą zmienić hasła po włamaniu?
Jeśli nie można wykluczyć, że baza danych wyciekła, powinni Państwo to zarządzić. Dobre skróty znacznie spowalniają łamanie, ale słabe hasła pozostają podatne. To, czy dodatkowo istnieje obowiązek zgłoszenia, należy ustalić z inspektorem ochrony danych.