Увійти Реєстрація
Вакансії
Вакансії Laravel Компанії Статистика
Підготовка
Як підготуватися < href="https://laravelukraine.com/interviews" wire:navigate.hover class="flex items-center gap-3 rounded-md px-3 py-2 text-base font-medium text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-900 dark:hover:text-white" >Співбесіди Тестування Документація
Блог
Всі статті Серії
Екосистема
Пакети Ресурси Проєкти Інструменти Події Відео
Інше
Про нас Реклама
Новини 10 листопада 2026

Bcrypt і 72 байти: два RFC для PHP 8.7 і що робити в Laravel вже зараз

Bcrypt, яким Laravel хешує паролі за замовчуванням, бере до уваги лише перші 72 байти пароля і мовчки відкидає решту. У PHP internals зараз одночасно обговорюють два RFC, що мають це виправити на рівні мови: PASSWORD_BCRYPT_SHA256 і PASSWORD_BCRYPT_WRAP. Обидва націлені на PHP 8.7, обидва ще в обговоренні, і дат голосування в жодного немає.

Перший RFC ми коротко згадували в огляді жовтневих пропозицій для PHP 8.7. Відтоді з'явився конкурент, тож час розібратися, у чому проблема і чим підходи відрізняються.

У чому проблема

Алгоритм bcrypt має жорстку межу в 72 байти. Усе, що далі, не впливає на хеш, тож два різні паролі з однаковими першими 72 байтами вважаються однаковими:

  $hash = password_hash(str_repeat('a', 72) . 'перший', PASSWORD_BCRYPT);

  password_verify(str_repeat('a', 72) . 'зовсім інший', $hash); // true

Для звичайного пароля з восьми-дванадцяти символів це не має значення. Але 72 байти - не 72 символи. Кирилична літера в UTF-8 займає два байти, емодзі - чотири, тож парольна фраза українською впирається в межу вже на 36 літерах. А ще небезпечніше, коли в password_hash() потрапляє не сам пароль, а склеєний рядок: префікс, ідентифікатор, інший хеш. RFC про виняток для довгих паролів наводить як приклад CVE-2025-68402 у FreshRSS: 64-символьний nonce перед 60-символьним хешем заповнював увесь 72-байтний простір статичними даними, і перевірка проходила з будь-яким паролем.

Друга вада - NUL-байт. На рівні crypt() і Blowfish рядок на деяких C-бібліотеках обрізається на першому \0. Сам password_hash() із PASSWORD_BCRYPT такий пароль уже не приймає і кидає ValueError ("Bcrypt password must not contain null character"). Проблема виринає тоді, коли розробник сам робить попереднє хешування з сирим бінарним виходом, наприклад hash('sha256', $password, true): у такому рядку NUL-байт трапляється часто.

Чому попередні спроби не пройшли

Обидва RFC починаються з переліку того, що вже пробували:

  • Кидати помилку на довгих паролях. RFC Сьорда Лангкемпера пропонував E_DEPRECATED у PHP 8.7 і ValueError у PHP 8.8 для паролів довших за 72 байти. Він досі у статусі чернетки: будь-яка зміна поведінки PASSWORD_BCRYPT, під яким у продакшн-базах лежать мільйони хешів, викликає опір, а відмова приймати раніше прийнятний вхід - сама по собі порушення сумісності.
  • Виправити NUL-байти в реалізації Blowfish. PR #21675 досі відкритий - з тих самих міркувань.
  • Зробити PASSWORD_DEFAULT алгоритмом Argon2. Argon2 потребує зовнішньої залежності, яка є не в кожній збірці PHP, а bcrypt вбудований завжди і досі вважається одним із найкращих для інтерактивної автентифікації.

Тож обидва нові RFC йдуть іншим шляхом: не чіпають PASSWORD_BCRYPT, а додають окремий опційний алгоритм, який спершу хешує пароль швидкою функцією, а результат у текстовому кодуванні віддає bcrypt. Вхід bcrypt тоді завжди має фіксовану довжину і ніколи не містить NUL-байта.

PASSWORD_BCRYPT_SHA256: формат Passlib

RFC Сьорда Лангкемпера (реалізація - PR #24073) переносить у PHP алгоритм bcrypt_sha256 з Python-бібліотеки Passlib у його другій версії. Пароль проходить через HMAC-SHA256, де ключ - сіль bcrypt, 32 байти результату кодуються в Base64 (44 символи) і йдуть у bcrypt:

  $hash = password_hash($password, PASSWORD_BCRYPT_SHA256);
  // $bcrypt-sha256$v=2,t=2b,r=12$n79VH.0Q2TMWmt3Oqt9uku$Kq4Noyk3094Y2QlB8NdRT8SvGiI4ft2

  password_verify($password, $hash); // true

Головний аргумент - не вигадувати нічого свого. Алгоритм уже пройшов публічну перевірку, а хеші, створені в PHP і Python, взаємозамінні: застосунки на двох мовах зі спільною базою користувачів можуть читати хеші одне одного. Вартість cost така сама, як у PASSWORD_BCRYPT (4-31, за замовчуванням 12), а password_verify(), password_needs_rehash() і password_get_info() працюють без змін.

PASSWORD_BCRYPT_WRAP: свій формат і суперечка про деталі

RFC Андрея Андреєва (Andrey Andreev) з PR #25040, який поки позначений як WIP, з'явився 29 жовтня і відкрито полемізує з першим. Конструкція та сама, деталі інші:

  • дві функції попереднього хешування на вибір через опцію prehash: cshake256 (за замовчуванням) і sha512 у вигляді HMAC;
  • 54 байти результату в Base64 займають рівно 72 байти, тобто весь простір bcrypt;
  • формат PHC Strings зі стандартним Base64 замість формату Passlib;
  • ліміт пароля 1024 байти проти явного зловживання.
  $hash = password_hash($password, PASSWORD_BCRYPT_WRAP, ['prehash' => 'sha512']);
  // $bcrypt-wrap$v=0$cost=12,prehash=sha512$<сіль>$<хеш>

Розділ FAQ пояснює, чому автор вважає bcrypt-sha256 гіршим. SHA-256 - 32-бітна функція, яку дешево перебирати на GPU, тоді як 64-бітні SHA-512 і cSHAKE256 стають вузьким місцем для зломщика безкоштовно для захисника. Формат Passlib базується на застарілому Modular Crypt Format, несумісному з PHC. Сама Passlib, за даними RFC, роками стояла без супроводу, і власник пакета змінився лише наприкінці жовтня 2026 року.

Окремо RFC розбирає shucking (hash shucking): атаку, за якої зломщик ламає не весь bcrypt, а внутрішній швидкий хеш, підставляючи готові хеші з чужих витоків. Від неї рятує те, що внутрішній хеш залежить від унікального ключа. У bcrypt-wrap це сіль із префіксом bcrypt-wrap:0:, що прив'язує внутрішній хеш і до конкретного bcrypt, і до версії формату. bcrypt-sha256 у нинішній версії теж використовує сіль як ключ HMAC, але в RFC нагадують, що Passlib була вразлива до shucking до версії 1.7.3 2020 року.

Вибір між двома RFC - не стільки про безпеку, скільки про пріоритети: сумісність із наявною екосистемою проти формату, спроєктованого з нуля під сучасні стандарти. Хеші обох довші за звичайні 60 символів bcrypt (у bcrypt-wrap - 93-97 символів), але стандартна колонка password у Laravel - це string, тобто VARCHAR(255), тож місця вистачить.

Що робить Laravel сьогодні

Драйвер за замовчуванням у config/hashing.php - bcrypt, і BcryptHasher викликає password_hash() з PASSWORD_BCRYPT без жодного попереднього хешування. Перевірка довжини в ньому є, але вимкнена: опція limit береться з BCRYPT_LIMIT і за замовчуванням дорівнює null.

  'bcrypt' => [
      'rounds' => env('BCRYPT_ROUNDS', 12),
      'verify' => env('HASH_VERIFY', true),
      'limit' => env('BCRYPT_LIMIT', null),
  ],

Якщо задати BCRYPT_LIMIT=72, Hash::make() для довшого рядка кине InvalidArgumentException. Перевірка рахує саме байти (strlen()), і вона стосується тільки make(): на Hash::check() уже збережених хешів вона не впливає. Пароль із NUL-байтом Hash::make() і без цього не прийме: ValueError від PHP перехоплюється і перетворюється на RuntimeException з не надто очевидним текстом "Bcrypt hashing not supported."

Пастка - у валідації. Правило max:72 рахує символи через mb_strlen(), а не байти, тож для кириличного пароля воно пропустить до 144 байтів. Якщо хочете чесно обмежити довжину на реєстрації, перевіряйте байти:

  'password' => [
      'required',
      Password::defaults(),
      function (string $attribute, mixed $value, Closure $fail) {
          if (strlen($value) > 72) {
              $fail('Пароль задовгий: максимум 72 байти.');
          }
      },
  ],

Коли має сенс argon2id

Laravel уже підтримує драйвери argon і argon2id, і вони обмеження в 72 байти не мають. Перевірте, чи ваша збірка PHP вміє Argon2: password_algos() має повернути argon2id. У Docker-образах і на хостингах це не гарантовано, саме тому PHP досі не зробив Argon2 типовим.

Перехід на HASH_DRIVER=argon2id у наявному застосунку потребує уваги. З HASH_VERIFY=true, як у типовому конфігу, Argon2IdHasher::check() на старому bcrypt-хеші кидає RuntimeException ("This password does not use the Argon2id algorithm."), тобто користувачі зі старими паролями не зможуть увійти. На час міграції verify треба вимкнути: тоді password_verify() перевірить хеш будь-якого алгоритму, а rehash_on_login перехешує пароль у новий формат при наступному вході.

Що робити зараз

  • Не склеюйте нічого з паролем перед Hash::make(). Префікси, ідентифікатори й інші хеші з'їдають 72-байтний бюджет і можуть зробити хеш незалежним від пароля.
  • Перевіряйте довжину в байтах, а не в символах, якщо обмежуєте пароль на реєстрації, і подумайте про BCRYPT_LIMIT=72 як про другий запобіжник.
  • Не робіть власного попереднього хешування з hash(..., true): сирий бінарний вихід із NUL-байтами - саме та помилка, від якої захищають обидва RFC.
  • Argon2id - розумний вибір для нового проєкту, якщо збірка PHP його підтримує. Для наявного - тільки з продуманою міграцією.
  • Стежте за обговоренням. Обидва RFC змінюються: bcrypt-sha256 оновився 1 листопада, bcrypt-wrap - 2 листопада. Якщо вам є що сказати, гілки про bcrypt-sha256 і про bcrypt-wrap відкриті. А після ухвалення одного з них ще знадобиться підтримка нового алгоритму у фреймворку - про неї поки нічого не відомо.
20

Читати в документації

Коментарі

Увійдіть, щоб залишити коментар

Будьте першим, хто залишить коментар!

Читайте також

Новини 14 листопада 2026

Laravel Octane 2.23: стрімінг відповідей-генераторів запрацював на FrankenPHP і Swoole

Octane 2.23.0 виправив баг, через який response()->stream() з генератором на FrankenPHP і Swoole віддавав 200 з порожнім тілом і ламав SSE та стрімінг Laravel AI SDK. Також у релізі підтримка RoadRunner 3.x, мінімальна версія FrankenPHP 1.13.2 і слухачі як singletons.

9
Новини 14 листопада 2026

RFC пропонує хуки життєвого циклу запиту для воркер-режиму PHP

Кевін Дюнгла виніс на обговорення RFC «Request lifecycle hooks for SAPIs with a worker mode»: два нові хуки модулів request_begin і request_end та функції php_request_begin() і php_request_end(). Вони мають замінити крихкі скидання стану, через які FrankenPHP цього року виправляв витік session ID і помилки пам'яті. Ціль - PHP 8.7, голосування ще не призначене.

9

Вакансії за темою

Програміст PHP (Laravel, WordPress, гіг-контракт)

Розробка на фреймворку Laravel та створення тем і плагінів для WordPress. Завдання включає роботу з jQuery, JS, CSS, HTML і PHP, використання git та оптимізацію швидкості завантаження сторінок. Оформлення за гіг-контрактом з погодинною оплатою.

Starlight Media Нова
2 дні тому

Senior Full-Stack Developer (PHP / Go / Vue.js)

Senior Full-Stack розробник матиме розвивати й підтримувати існуючі веб-платформи, писати модулі, сервіси та API, а також розробляти клієнтську частину на Vue.js і Vanilla JS. Основний стек: PHP 8.x з Yii2 та/або Laravel, Go для сервісів, MySQL/MariaDB, REST API і Webhooks. Вимоги: досвід від 3 років, знання ООП і шаблонів проєктування, ES6+, Vue.js, SPA, Git, Docker і Composer; англійська B1-B2.

DL SOLUTION, ТОВ
4 дні тому

Full Stack Developer (PHP / React) Middle / Middle+

Full Stack розробник розвиватиме та підтримуватиме власний аналітичний продукт для перевірки контрагентів і оцінки ризиків, працюючи зі складною бізнес-логікою та великими обсягами даних. Основний стек: PHP 8.x, Laravel або Symfony, MySQL, React; також потрібні проєктування API, оптимізація SQL-запитів, Docker, CI/CD. Вимоги: комерційний досвід від 3 років, досвід підтримки legacy-коду, англійська B1+ для читання документації.

Пакети за темою

Kalimera

welldigit/kalimera

Interactive installer that scaffolds Laravel applications the Well Digit way.

v1.0.0 17

Bagisto

bagisto/bagisto

Bagisto — це платформа для електронної комерції, побудована на Laravel. Вона надає готове рішення для створення та управління інтернет-магазинами з підтримкою каталогу товарів, замовлень, платежів та клієнтів.

28,369 v2.5.1 13 28