Kilka tygodni temu trafiÅ‚ do mnie klient z pytaniem, które sÅ‚yszÄ™ regularnie od lat: czy mogÄ™ mu zresetować hasÅ‚o bezpoÅ›rednio w bazie, bo e-mail z resetem nie dochodzi. Standardowa procedura – wchodzisz do phpMyAdmin, UPDATE wp_users SET user_pass = MD5('nowehaslo') WHERE ID = 1, gotowe. Klient siÄ™ loguje, WordPress po cichu rehashuje hasÅ‚o silniejszym algorytmem, wszyscy sÄ… szczęśliwi.

Przy okazji zajrzaÅ‚em do kolumny user_pass w jego bazie. Mieszanina $P$B... i kilku czystych MD5. Klasyczny widok na instalacji, która chodziÅ‚a przez kilka lat bez wiÄ™kszej opieki. I pomyÅ›laÅ‚em sobie – za kilka tygodni to bÄ™dzie wyglÄ…dać inaczej. Bo WordPress 6.8, wydany w kwietniu 2025, zmieniÅ‚ coÅ›, na co część z nas czekaÅ‚a od dawna.

Przez 20 lat WordPress hashował hasła algorytmem z 2004 roku

Å»eby zrozumieć, co zmieniÅ‚o siÄ™ w 6.8, warto wiedzieć, co byÅ‚o wczeÅ›niej – i dlaczego to byÅ‚ problem.

WordPress korzystaÅ‚ z biblioteki phpass, napisanej przez Solara Designera w 2004 roku. Jak na tamte czasy – solidne rozwiÄ…zanie. phpass używaÅ‚ algorytmu portable hashing opartego na MD5, który w bazie danych rozpoznasz po prefixie $P$ (lub $H$ w starszych instalacjach). WyglÄ…da to mniej wiÄ™cej tak:

$P$BIRXBdpFU30FYPOaFGEFPo8EW8Y7HN0

Sam mechanizm dziaÅ‚aÅ‚ nastÄ™pujÄ…co: phpass braÅ‚ hasÅ‚o, generowaÅ‚ losowÄ… sól, a nastÄ™pnie wykonywaÅ‚ wielokrotne iteracje MD5 – domyÅ›lnie 2^8, czyli 256 razy. Ta wartość, zwana cost factor, byÅ‚a zakodowana na staÅ‚e i nie zmieniaÅ‚a siÄ™ w czasie.

I tu leży sedno problemu. Cost factor zakodowany na staÅ‚e to przepis na katastrofÄ™ w perspektywie 20 lat. W 2004 roku 256 iteracji MD5 byÅ‚o rozsÄ…dnym kompromisem miÄ™dzy bezpieczeÅ„stwem a wydajnoÅ›ciÄ…. W 2024 roku GPU potrafi wykonywać miliardy operacji MD5 na sekundÄ™. Hashcat ma dedykowany tryb -m 400 wÅ‚aÅ›nie dla WordPress/phpass i na przeciÄ™tnej maszynie z jednÄ… kartÄ… graficznÄ… robi kilkadziesiÄ…t milionów prób na sekundÄ™. Dedykowany rig z kilkoma GPU – setki milionów.

PrzekÅ‚adajÄ…c to na praktykÄ™: jeÅ›li atakujÄ…cy wejdzie w posiadanie wycieku bazy danych WordPressa, sÅ‚abe i Å›rednie hasÅ‚a zÅ‚amie w minuty. Nie godziny – minuty.

phpass nie był złym kodem. Był dobrym kodem na rok 2004, który WordPress niósł na barkach przez dwie dekady, bo projekt tej skali zmienia fundamentalne mechanizmy bezpieczeństwa bardzo ostrożnie. Ticket #21022 na Trac z propozycją przejścia na bcrypt został otwarty w 2012 roku. Czekał 13 lat.

Co dokładnie zmieniło się w WordPress 6.8?

Dwie funkcje, które obsÅ‚ugujÄ… hasÅ‚a od zawsze – wp_hash_password() i wp_check_password() – zostaÅ‚y przepisane. Zamiast oddelegowywać robotÄ™ do phpass, korzystajÄ… teraz z natywnych funkcji PHP: password_hash() i password_verify() z algorytmem bcrypt.

Nowy hash w bazie wyglÄ…da tak:

$wp$2y$10$abcdefghijklmnopqrstuuVwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12

Prefix $wp nie jest przypadkowy. bcrypt ma ograniczenie – przetwarza maksymalnie 72 bajty hasÅ‚a, reszta jest ignorowana. WordPress obchodzi to przez SHA-384 pre-hashing: zanim hasÅ‚o trafi do bcrypt, jest najpierw hashowane SHA-384 i enkodowane base64. Wynik mieÅ›ci siÄ™ w 72 bajtach, wiÄ™c nawet bardzo dÅ‚ugie hasÅ‚o jest w peÅ‚ni uwzglÄ™dniane. Prefix $wp informuje wp_check_password(), że ma do czynienia wÅ‚aÅ›nie z takim pre-hashowanym bcryptem – żeby nie pomylić go z vanilla bcrypt, który mógÅ‚ wstawić plugin taki jak roots/wp-password-bcrypt.

Obok tego wprowadzono wp_password_needs_rehash() – wrapper na PHP-owÄ… password_needs_rehash(). Przydatne jeÅ›li budujesz wÅ‚asny flow migracji i chcesz programowo sprawdzić, czy dany hash wymaga aktualizacji.

Co z kluczami aplikacji i tokenami?

Application passwords, klucze resetowania hasÅ‚a, klucze żądaÅ„ danych osobowych i klucz recovery mode przeszÅ‚y na BLAKE2b via Sodium – dostÄ™pny przez nowe funkcje wp_fast_hash() i wp_verify_fast_hash(). BLAKE2b jest kryptograficznie bezpieczny, ale celowo szybki – w odróżnieniu od bcrypt, który jest celowo wolny. To wÅ‚aÅ›ciwy dobór algorytmu do celu: tokeny sÄ… losowo generowane z wysokÄ… entropiÄ…, wiÄ™c nie potrzebujÄ… kosztownego hashowania odpornego na brute force. HasÅ‚a użytkowników – owszem.

Post passwords (hasła chroniące pojedyncze wpisy) zostają na phpass. Na razie. John Blackbourn zaznaczył w ogłoszeniu, że temat wymaga osobnej analizy ze względu na specyficzny mechanizm weryfikacji tych haseł.

Argon2id jako alternatywa

Jeśli serwer obsługuje Argon2, można podmienić algorytm jedną linią przy użyciu filtra wp_hash_password_algorithm:

add_filter( 'wp_hash_password_algorithm', fn() => PASSWORD_ARGON2ID );

Argon2id jest obecnie uznawany za mocniejszy od bcrypt – lepiej radzi sobie z atakami wykorzystujÄ…cymi dedykowany sprzÄ™t. Problem w tym, że wymaga libargon2 po stronie serwera i PHP skompilowanego z obsÅ‚ugÄ… Argon2. Przed użyciem tego filtra warto sprawdzić dostÄ™pność:

if ( in_array( 'argon2id', password_algos(), true ) ) {
    add_filter( 'wp_hash_password_algorithm', fn() => PASSWORD_ARGON2ID );
}

Pro tip: $wp prefix pojawia siÄ™ wyłącznie przy bcrypt. JeÅ›li przejdziesz na Argon2id, hash bÄ™dzie miaÅ‚ prefix $argon2id$ bez $wp na poczÄ…tku – WordPress nie stosuje SHA-384 pre-hashingu dla algorytmów innych niż bcrypt.

Stare hasła, MD5 i to, czego się nie spodziewałeś

WordPress od zawsze obsÅ‚ugiwaÅ‚ coÅ›, o czym wiÄ™kszość deweloperów nie wie albo dawno zapomniaÅ‚a: czysty MD5 w kolumnie user_pass. Nie phpass, nie bcrypt – zwykÅ‚y 32-znakowy hexadecymalny string, bez żadnego prefixu. JeÅ›li wstawiÅ‚eÅ› taki hash bezpoÅ›rednio do bazy, WordPress przy logowaniu go rozpoznawaÅ‚, weryfikowaÅ‚ i natychmiast rehashowaÅ‚ silniejszym algorytmem.

To właśnie na tym opierała się technika resetowania hasła przez phpMyAdmin, którą opisałem na początku.

Co się stało z MD5 w 6.8?

W pierwotnej wersji 6.8 John Blackbourn usunÄ…Å‚ wsparcie dla czystego MD5. W dyskusji na Make.WordPress.org szybko pojawiÅ‚y siÄ™ gÅ‚osy sprzeciwu – deweloperzy i specjaliÅ›ci od odzyskiwania stron wskazywali, że MD5 to dla wielu użytkowników jedyna praktyczna metoda rÄ™cznego resetu hasÅ‚a, szczególnie gdy e-mail nie dziaÅ‚a, a dostÄ™pu do WP-CLI nie ma. Po tej dyskusji wsparcie dla MD5 zostaÅ‚o przywrócone jeszcze przed finalnym wydaniem 6.8.

Wniosek praktyczny: stary sposób z phpMyAdmin nadal działa. Po zalogowaniu hash zostaje automatycznie wzmocniony do bcrypt. Długoterminowo jednak WP-CLI jest właściwą ścieżką i warto wyrobić ten nawyk:

wp user update 1 --user_pass="nowehaslo"

Jak wyglÄ…da baza po aktualizacji do 6.8?

Przez pewien czas – normalnie mieszanie. To nie jest błąd, to zamierzony mechanizm. wp_check_password() w wersji 6.8 rozumie caÅ‚y ten zestaw:

$wp$2y$   →  nowy default: SHA-384 pre-hash + bcrypt
$2y$      →  vanilla bcrypt (np. z roots/wp-password-bcrypt)
$P$ / $H$ →  stary phpass, nadal weryfikowany
[32 hex]  →  czysty MD5, nadal weryfikowany
$generic$ →  BLAKE2b (klucze aplikacji, nie hasła użytkowników)

Każdy z tych formatów jest poprawnie weryfikowany. Jeśli hash pochodzi ze starszego algorytmu, przy pierwszym logowaniu użytkownika zostaje automatycznie rehashowany do $wp$2y$ i zapisany z powrotem w bazie. Użytkownik nic nie zauważa.

Scenariusz, o którym warto wiedzieć: downgrade

JeÅ›li zaktualizujesz do 6.8, część użytkowników siÄ™ zaloguje – ich hasÅ‚a zostanÄ… rehashowane do bcrypt. Potem z jakiegoÅ› powodu cofniesz WordPress do 6.7. Ci konkretni użytkownicy nie bÄ™dÄ… mogli siÄ™ zalogować, bo starsza wersja nie rozumie $wp$2y$. Nie wszyscy – tylko ci, którzy logowali siÄ™ już po aktualizacji.

RozwiÄ…zanie: reset hasÅ‚a przez e-mail. Ale lepsza strategia to testowanie aktualizacji na stagingu zanim trafi na produkcjÄ™ – co zresztÄ… powinno być standardem niezależnie od tej zmiany.

Co to oznacza dla Twojego kodu?

JeÅ›li korzystasz z wp_hash_password() i wp_check_password() – nic nie musisz zmieniać. To jedyna sÅ‚uszna odpowiedź na 99% przypadków.

Problemy mogą pojawić się w kodzie, który bezpośrednio sprawdza wartość hasha zamiast weryfikować go przez API. Klasyczny przykład to sprawdzanie prefixu:

// Źle - założenie o formacie hasha
if ( str_starts_with( $user->user_pass, '$P$' ) ) {
    // "stary" hash
}

// Dobrze - pozwól core'owi to ocenić
if ( wp_password_needs_rehash( $user->user_pass ) ) {
    // hash wymaga aktualizacji
}

wp_password_needs_rehash() to nowa funkcja w 6.8, ale logika jest prosta: zwraca true jeśli hash nie odpowiada aktualnemu algorytmowi i opcjom. Przydatna jeśli budujesz własny flow migracji lub masz kod, który ręcznie zarządza hasłami użytkowników.

Kilka innych przypadków wartych sprawdzenia:

Pluginy SSO, social login i MFA w wiÄ™kszoÅ›ci nie sÄ… dotkniÄ™te tÄ… zmianÄ… – o ile korzystajÄ… ze standardowego flow uwierzytelniania WordPressa i nie dotykajÄ… bezpoÅ›rednio pola user_pass. Warto jednak przejrzeć kod jeÅ›li masz coÅ› niestandardowego.

JeÅ›li używaÅ‚eÅ› roots/wp-password-bcrypt – możesz go usunąć. Plugin nadpisywaÅ‚ wp_hash_password() i wp_check_password() wÅ‚asnÄ… implementacjÄ… bcrypt. WordPress 6.8 robi to samo, a istniejÄ…ce hashe w formacie $2y$ (vanilla bcrypt bez prefixu $wp) sÄ… w peÅ‚ni obsÅ‚ugiwane i zostanÄ… rehashowane do $wp$2y$ przy nastÄ™pnym logowaniu użytkownika.

Jedna puÅ‚apka na którÄ… warto uważać: post passwords. JeÅ›li masz wÅ‚asny kod obsÅ‚ugujÄ…cy cookie wp-postpass_, który korzystaÅ‚ z wp_hash_password() – przestaÅ‚ dziaÅ‚ać. wp_hash_password() nie używa już PasswordHash, wiÄ™c cookie generowane starym sposobem nie przejdzie weryfikacji. Tymczasowe obejÅ›cie to bezpoÅ›rednie użycie klasy PasswordHash:

require_once ABSPATH . WPINC . '/class-phpass.php';
$hasher = new PasswordHash( 8, true );
$cookie_value = $hasher->HashPassword( wp_unslash( $password ) );

Brzydkie, ale działa do czasu aż core ustandaryzuje obsługę post passwords.

Co ta zmiana oznacza, gdy dojdzie do wycieku bazy?

Wyciek bazy danych to nie abstrakcja. ZdarzajÄ… siÄ™ regularnie – przez niezaÅ‚atanÄ… podatność w pluginie, skompromitowane dane dostÄ™powe do hostingu, źle skonfigurowane uprawnienia do plików. Jak pokazywaÅ‚em przy okazji analizy CVE-2023-46182, dane użytkowników to czÄ™sto główny cel atakujÄ…cego.

Załóżmy, że atakujący ma dump tabeli wp_users. Co może z tym zrobić?

Przy bazie sprzed 6.8, z hashami $P$: Hashcat, tryb -m 400, przeciÄ™tna karta graficzna – kilkadziesiÄ…t milionów prób na sekundÄ™. Lista najpopularniejszych haseÅ‚, sÅ‚owniki, reguÅ‚y mutacji. SÅ‚abe i Å›rednie hasÅ‚a – zÅ‚amane w minuty do godzin. Mocne hasÅ‚a – dni, tygodnie. Ale wiÄ™kszość użytkowników nie używa mocnych haseÅ‚.

Przy bazie po 6.8, z hashami $wp$2y$: bcrypt z domyÅ›lnym cost factorem 10 oznacza, że ta sama karta graficzna zrobi kilkaset prób na sekundÄ™, nie milionów. Trzy, cztery rzÄ™dy wielkoÅ›ci wolniej. HasÅ‚o które przy phpass pÄ™ka w godzinÄ™, przy bcrypt wymaga lat. Przy Argon2id – jeszcze gorzej dla atakujÄ…cego.

To jest realna różnica dla użytkowników, którzy używajÄ… haseÅ‚ typu Warszawa2019! albo imienia i roku urodzenia. Przy phpass – do zÅ‚amania. Przy bcrypt – praktycznie nie do ruszenia w rozsÄ…dnym czasie.

Zmiana jest transparentna dla użytkowników i wymaga zera działań ze strony administratora. Ale efekt jest konkretny: każda baza WordPressa zaktualizowana do 6.8, w której użytkownicy zdążyli się zalogować, jest znacząco trudniejszym celem niż była tydzień wcześniej.

Change consents