Nowy standard. Twoje centrum tworzenia inteligentnego, szybkiego i zrównoważonego oprogramowania AI

Zobacz teraz
Ilustracja kłódki obok paskowanych słupków uciętych na przerywanej linii, przedstawiająca obcinanie haseł przez bcrypt po przekroczeniu limitu 72 bajtów

Bezpieczniejsze hasła w bcrypt: jak obejść limit 72 bajtów

Roksana Rothenberger
|   Updated Oct 16, 2026

bcrypt po cichu ignoruje każdy bajt hasła po 72., dlatego rozwiązaniem jest jawne odrzucanie dłuższych haseł albo przejście na Argon2id. Niepozorny błąd implementacyjny, taki jak limit 72 bajtów w bcrypt, może podważyć cały model bezpieczeństwa bez żadnego błędu ani ostrzeżenia. System niczego nie zgłasza ani nie zapisuje w logach, a użytkownik nigdy nie dostaje takiej informacji. A jeśli używasz pepper, odpowiednio długie hasło może sprawić, że pepper całkowicie zniknie z hasha, i to bez żadnego sygnału.

W tym artykule wyjaśniamy, na czym polega problem, pokazujemy go w Ruby na konkretnych przykładach i omawiamy trzy sposoby poradzenia sobie z nim, w tym także to, dlaczego naiwne wstępne haszowanie, które często się proponuje, jest niebezpieczne. Nasz zespół Ruby on Rails tworzy produkcyjne systemy w Rails od 2008 roku. Wśród nich jest także Seat Unique, platforma sprzedaży biletów obsługująca ponad 1 mln odwiedzających miesięcznie i 6 000 transakcji w miesiącu.

Podsumowanie

bcrypt ogranicza dane wejściowe hasła do 72 bajtów i po cichu ignoruje wszystko, co wykracza poza ten limit, bez żadnego błędu ani ostrzeżenia dla dewelopera czy użytkownika, że doszło do obcięcia.

Ryzyko rośnie, gdy używasz pepper: długie hasło może wypchnąć pepper poza ten limit i całkowicie usunąć go z hasha, podważając sens jego stosowania.

W istniejących systemach opartych na bcrypt możesz zamknąć tę lukę, dodając jawne sprawdzanie długości w bajtach, a w nowych systemach warto użyć Argon2id, który OWASP rekomenduje dziś jako podstawowy wybór i który nie ma tego problemu.

Dlaczego limit 72 bajtów w bcrypt to realne ryzyko na produkcji?

Limit 72 bajtów nie wywołuje błędu, więc zestaw testów, który sprawdza tylko „czy logowanie działa?”, tego nie wychwyci. Odkryją to atakujący albo nikt. Hasła w praktyce są słabsze, niż zakłada zespół, a pepper znika z hasha, który miał chronić, bez żadnego sygnału, że stało się jedno lub drugie.

To już trafiło na produkcję w dojrzałych, szeroko używanych systemach. Raport HackerOne dotyczący GitLab opisał dokładnie ten problem z obcinaniem już w 2020 roku. Spring Security dostarczył równoważny błąd jako CVE-2025-22228, opisany we własnym komunikacie Springa tak: „BCryptPasswordEncoder.matches(CharSequence,String) will incorrectly return true for passwords larger than 72 characters as long as the first 72 characters are the same.”

Tę lukę można zamknąć na trzy sposoby, a każdy z nich oznacza inny kompromis:

Podejście

Nakład pracy

Poziom bezpieczeństwa

Koszt migracji

Najlepsze dla

Migracja do Argon2id

Średni: nowa biblioteka i strategia ponownego haszowania

Najwyższy: podstawowa rekomendacja OWASP, brak praktycznego limitu długości

Hasła są ponownie haszowane przy okazji logowania

Nowe systemy albo istniejące, które mają przestrzeń na migrację

Dodanie walidacji długości, bez odchodzenia od bcrypt

Niski: kilka linijek kodu walidacji

Podstawowy: eliminuje obcinanie i znikanie pepper, ale OWASP nadal klasyfikuje bcrypt jako rozwiązanie legacy

Brak: bez migracji hashy

Istniejące systemy, które na ten moment nie mogą odejść od bcrypt

Wstępne haszowanie przed bcrypt

Średnio wysoki: trzeba poprawnie zaimplementować HMAC z kluczem

Zależy wyłącznie od implementacji: OWASP uznaje naiwną wersję za niebezpieczną

Brak: bez zmiany algorytmu

Systemy legacy, w których nie da się ruszyć samego wywołania bcrypt

Na czym polega problem z bezpieczeństwem bcrypt?

Tak wygląda to w praktyce. Hasło trochę za długie daje hash, który wygląda poprawnie i poprawnie przechodzi walidację:

irb(main)> password = "a" * 72 + "someextra"
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaasomeextra"
irb(main)> hash = BCrypt::Password.create(password)
=> "$2a$12$ijB8LuN33Y6.NoMl3gTLieyd9hYzp7SWD4H6immKiy80DNCTXYgQ."
irb(main)> BCrypt::Password.new(hash) == password
=> true

Problem widać, gdy porównasz ten sam hash tylko z pierwszymi 72 znakami „a”, całkowicie pomijając „someextra”. Taka wartość nadal przechodzi walidację:

irb(main)> hash
=> "$2a$12$ijB8LuN33Y6.NoMl3gTLieyd9hYzp7SWD4H6immKiy80DNCTXYgQ."
irb(main)> BCrypt::Password.new(hash) == "a" * 72
=> true

Jeśli pierwsze 72 bajty nadal tworzą bezpieczne hasło, może się wydawać, że to akceptowalne, ale to zakłada, że użytkownicy tworzą bezpieczne hasła, a tego nie da się zagwarantować. Gdy używasz pepper, to samo ciche obcięcie usuwa go z tego, co trafia do haszowania:

irb(main)> password = "a" * 72 + pepper
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaapepper"
irb(main)> hash = BCrypt::Password.create(password)
=> "$2a$12$kMR90psu4jhtp2xb7B35d.36lDuyewZsPEeDRXiVdlrWr9B7CQMVi"
irb(main)> BCrypt::Password.new(hash) == "a" * 72
=> true

Limit wynosi 72 bajty, a nie 72 znaki, i to ma znaczenie, bo większość haseł w aplikacjach webowych używa UTF-8, a nie każdy znak ma 1 bajt. Monterail to polska firma, a w języku polskim używamy kilku dodatkowych liter, takich jak „ą”, która w UTF-8 zajmuje 2 bajty:

irb(main)> "ą".bytesize
=> 2

Hasło złożone wyłącznie ze znaków „ą” ma w praktyce długość ograniczoną do 36:

irb(main)> password = "ą" * 36 + "extra"
=> "ąąąąąąąąąąąąąąąąąąąąąąąąąąąąąąąąąąąąextra"
irb(main)> hash = BCrypt::Password.create(password)
=> "$2a$12$3QyhYPBdqknleoFPiDHn5edo.OF4jgU4D0dYOa/MDncm5EDy0QUB."
irb(main)> BCrypt::Password.new(hash) == "ą" * 36
=> true

W praktyce polskie hasła łączą znaki łacińskie i rozszerzone, więc w większości przypadków ich efektywna długość nie spadnie o połowę. Ale niektóre systemy pisma w ogóle nie używają znaków łacińskich.

Przy znakach 3-bajtowych efektywna długość hasła spada do 24, a przy 4-bajtowych do 18.

To skrócenie zawsze działa tak samo: po cichu, bez błędu i bez żadnego sygnału dla użytkownika.

Co się zmienia, gdy naprawisz limit 72 bajtów w bcrypt?

Dodanie walidacji długości albo migracja do Argon2id to niewielka zmiana w kodzie: reguła walidacji albo podmiana biblioteki. Istotna jest zmiana zachowania: hasła przestają być po cichu obcinane, a pepper pozostaje w hashu dla każdego hasła.

To zamyka dokładnie tę lukę, którą opisują zarówno CVE, jak i wspomniany wyżej raport GitLab: hasło wygląda na zweryfikowane, choć nigdy nie zostało w pełni sprawdzone względem tego, co wpisał użytkownik. Dla zespołu to różnica między systemem uwierzytelniania, który przechodzi przegląd bezpieczeństwa, a takim, który kończy się uwagą w raporcie.

Haszowanie haseł to jeden z elementów szerszego podejścia secure by design do tworzenia oprogramowania. Jeśli tworzysz uwierzytelnianie od zera, zamiast łatać istniejący system, nasz przewodnik po uwierzytelnianiu i autoryzacji w Rails pokazuje, gdzie haszowanie haseł wpisuje się w szerszy kontekst.

Opcja 1: przejście na Argon2id

Argon2id nie ma praktycznego limitu długości, bo jego limit wejścia wynosi 4 GiB, więc cały problem bajtów i znaków opisany w tym artykule po prostu go nie dotyczy. Z założenia jest memory-hard, co sprawia, że łamanie haseł na dużą skalę z użyciem GPU jest dużo droższe niż w przypadku bcrypt.

To najlepsza opcja dla nowych systemów albo istniejących, które mają wystarczająco dużo przestrzeni na migrację biblioteki haszującej i stopniowe ponowne haszowanie haseł. Zamiast obudowywać problem walidacją, usuwa całą klasę błędów, bo nie ma tu już żadnego istotnego limitu długości, który można źle obsłużyć.

Password Storage Cheat Sheet od OWASP wskazuje Argon2id jako podstawową rekomendację dla nowych systemów. Generator mix phx.gen.auth w Phoenix domyślnie używa bcrypt na Unixie i pbkdf2 w Windows, ale jego własna dokumentacja zaleca rozważenie argon2, „the most robust of all 3”, zespołom, których infrastruktura jest w stanie udźwignąć ten koszt.

Kosztem jest zużycie zasobów: Argon2id celowo mocno obciąża procesor i pamięć, więc twoje serwery autoryzacyjne muszą mieć odpowiedni zapas. Mimo to i tak ustaw rozsądny limit długości danych wejściowych, żeby blokować próby denial-of-service z użyciem złośliwie długich danych. Authentication Cheat Sheet od OWASP zaleca dopuszczenie co najmniej 64 znaków.

To, że Argon2id nie ma praktycznego limitu, nie znaczy, że brak jakiegokolwiek limitu to dobry pomysł.

Opcja 2: dodanie walidacji długości dla bcrypt

Jeśli musisz pozostać przy bcrypt, jawnie odrzucaj albo przycinaj hasła dłuższe niż 72 bajty na poziomie aplikacji, zamiast pozwalać bcrypt robić to po cichu.

To podejście pasuje do istniejących systemów opartych na bcrypt, w których pełna migracja biblioteki haszującej nie jest teraz realna. Jednocześnie zamyka lukę ujawnioną zarówno przez raport GitLab, jak i CVE w Spring Security, zamieniając ciche obcinanie w błąd walidacji widoczny dla użytkownika.

W Rails waliduj długość w bajtach, a nie liczbę znaków. length: { maximum: 72 } liczy znaki, więc hasła wielobajtowe przechodzą dalej:

validate :password_within_bcrypt_limit

def password_within_bcrypt_limit
  return if password.blank? || password.bytesize <= 72
  errors.add(:password, "is too long (maximum is 72 bytes)")
end

Jeśli używasz has_secure_password w Rails 7.1 lub nowszym, to sprawdzenie liczby bajtów jest już wbudowane. Moduł validatable w Devise sprawdza długość w znakach, więc w aplikacjach opartych na Devise trzeba to dodać.

Uwaga: jeśli używasz pepper, oblicz maksymalną bezpieczną długość jako 72 - pepper.bytesize, żeby pepper nigdy nie wypadł z hasha.

Opcja 3: wstępne haszowanie, ale ostrożnie

Zahaszowanie hasła szybką funkcją przed przekazaniem go do bcrypt usuwa limit bajtów bez zmiany algorytmu. To rozwiązanie, które sam raport GitLab proponował dla systemów legacy, gdzie nie da się bezpośrednio zmienić wywołania bcrypt. OWASP ostrzega jednak, że naiwne podejście jest niebezpieczne.

Wyjaśniają to dwa konkretne, udokumentowane ryzyka.

Po pierwsze, bajty null: bcrypt traktuje dane wejściowe jako łańcuch zakończony znakiem null, więc jeśli H($password)[0] == 0, to bcrypt(H($password)) == bcrypt(""). Zakodowanie pośredniego hasha w Base64 ogranicza to ryzyko.

Po drugie, password shucking: jeśli wewnętrzna funkcja haszująca jest używana gdzie indziej bez klucza, na przykład jako zwykłe SHA-512, a atakujący zdobędzie wyciekły hash, może zdjąć warstwę bcrypt i bezpośrednio atakować słabszy hash wewnętrzny. Tę technikę spopularyzowało wystąpienie Sama Croley’a na DEF CON, a jej opis znajdziesz w tekście poświęconym tej technice.

Bezpieczna formuła OWASP eliminuje oba problemy: bcrypt(base64(hmac-sha384(data: $password, key: $pepper)), $salt, $cost). HMAC musi być oparty na kluczu w postaci pepper przechowywanego poza bazą danych. Jeśli pominiesz klucz, wracasz do opisanego wyżej ryzyka password shucking.

Co musi się zgadzać, zanim zmienisz sposób haszowania haseł

Integracja

Argon2id ma dojrzałe wsparcie bibliotek w popularnych stosach technologicznych: gem argon2id w Ruby, pakiet argon2 na npm, argon2-cffi w Pythonie, alexedwards/argon2id w Go, wbudowany Argon2PasswordEncoder ze Spring Security w Javie oraz natywne wsparcie przez password_hash() od PHP 7.3. Zanim wpiszesz migrację do harmonogramu, upewnij się, że wersja w twoim stosie technologicznym ją obsługuje.

Zgodność z przepisami i wymagania regulacyjne

Słabe przechowywanie haseł to rodzaj luki, który wychwyci audyt SOC 2 albo przegląd ochrony danych pod kątem RODO. Artykuł 32 RODO wymaga „odpowiednich środków technicznych i organizacyjnych” do zabezpieczenia danych osobowych, a udokumentowane CVE dotyczące twojej biblioteki haszującej trudno wyjaśnić podczas audytu.

Skalowalność

Memory-hardness w Argon2id to cecha bezpieczeństwa, ale też koszt. Dostosuj współczynnik pracy do rzeczywistego wolumenu logowań i zapasu zasobów serwera, bo w przeciwnym razie ruch w godzinach szczytu może stać się problemem wydajnościowym.

Zaufanie organizacyjne i zarządzanie zmianą

Istniejących hashy bcrypt nie da się bezpośrednio przekonwertować na Argon2id. Migracja polega na opportunistycznym ponownym haszowaniu haseł przy kolejnym udanym logowaniu, bo nigdy nie masz dostępu do jawnego tekstu już zahaszowanego hasła.

Zaktualizuj dokumentację bezpieczeństwa i ścieżkę audytową tak, by odzwierciedlały nowy algorytm i współczynnik pracy, i upewnij się, że osoba, która przejmie potem kod odpowiedzialny za uwierzytelnianie, będzie wiedzieć, po co jest ta reguła walidacji. To właśnie zapobiega regresji takiej poprawki, gdy odpowiedzialność za kod przechodzi na kogoś innego i nikt tego nie zauważa.

Najważniejsze wnioski

  • bcrypt po cichu obcina dane po 72 bajtach. Nie zgłasza błędu, więc długie hasło i jego pierwsze 72 bajty dają identyczne hashe.

  • Ten błąd trafił jako CVE do jednego z dużych frameworków Javy, a GitLab miał zgłoszenie w HackerOne już kilka lat wcześniej.

  • Jeśli używasz pepper, długie hasło może wypchnąć go poza limit i poza hash. Ogranicz długość do 72 - pepper.bytesize.

  • OWASP klasyfikuje dziś bcrypt jako rozwiązanie legacy. Podstawową rekomendacją jest Argon2id, a scrypt pozostaje opcją zapasową.

  • Naiwne wstępne haszowanie otwiera drogę do kolizji związanych z bajtem null i password shucking. Bezpieczna forma OWASP używa przed bcrypt HMAC z kluczem.

Którego algorytmu użyć?

bcrypt jest szeroko stosowany i ma mocną historię, ale po cichu ogranicza hasła do 72 bajtów.

W istniejących systemach korzystających z bcrypt najbezpieczniejszą poprawką jest jawna walidacja długości.

W nowych systemach użyj Argon2id. Jeśli Argon2id nie jest dostępny, wybierz scrypt.

Jeśli planujesz przejście z bcrypt na Argon2id albo chcesz, żeby ktoś jeszcze spojrzał na twój obecny mechanizm uwierzytelniania, nasz zespół Ruby on Rails może pomóc.

FAQ: limit 72 bajtów w bcrypt