PASSWORD_BCRYPT_SHA256 is still a draft. I would not invent the constant.

PASSWORD_BCRYPT_SHA256 is still a draft. I would not invent the constant.

On Ubuntu 24.04 PHP 8.3.6, PASSWORD_BCRYPT_SHA256 is undefined and a Passlib $bcrypt-sha256$ string is unknown. The RFC is Under Discussion with a 0–0–0 vote; Laravel 13 still hashes with PASSWORD_BCRYPT. I would not wait for 8.7 to close the 72-byte window.

· 10 min read #php #security #rfc #bcrypt #laravel #php-8-7

 Today's CLI has no PASSWORD_BCRYPT_SHA256; the RFC and Laravel hasher are still drafts apart

Caption: Three columns I will not mix up. Today's password_hash() is silent bcrypt. PASSWORD_BCRYPT_SHA256 is an opt-in RFC for the next 8.x. Laravel 13 still calls PASSWORD_BCRYPT with BCRYPT_LIMIT null.

I did not get a $bcrypt-sha256$ hash. I ran Ubuntu 24.04 archive PHP 8.3.6 (php -v built 2 Sep 2026) and asked whether the constant exists. defined('PASSWORD_BCRYPT_SHA256') printed no. password_algos() printed 2y,argon2i,argon2id. password_hash('x', 'bcrypt-sha256') threw ValueError: password_hash(): Argument #2 ($algo) must be a valid password hashing algorithm.

Last Friday I wrote the 72-byte throw RFC. This is the follow-up, not a rewrite. php.net still cautions that PASSWORD_BCRYPT truncates to 72 bytes. php.watch still lists PASSWORD_DEFAULT as "2y" through 8.5, 8.6, and the 8.7 column. Lab note. I did not boot a Laravel app. I asked for an algorithm this binary does not have.

What changed

The throw RFC moved to Inactive. The gentler RFC did not vote.

What I will treat as locked this week:

  • Silent truncation is still documented as of 16 Oct 2026. This CLI hashed $2y$10$ — PHP 8.3 still defaults cost 10. Cost 12 is a PHP 8.4 change. Laravel’s BCRYPT_ROUNDS is already 12. Distro password_hash() is not Illuminate.
  • RFC: add PASSWORD_BCRYPT_SHA256. wiki.php.net/rfc/bcrypt_sha256, changelog 2026-10-04. Sjoerd Langkemper announced it 5 Oct 2026 09:20:29 UTC (internals 132784). Proposed next PHP 8.x (e.g. 8.7). Opt-in. PASSWORD_DEFAULT and PASSWORD_BCRYPT stay put. Vote 0–0–0. The RFC index retrieved 16 Oct 2026 still lists it Under Discussion.
  • HMAC-SHA256 keyed with the 22-character bcrypt salt, then 44-byte base64 into bcrypt. Passlib v2: $bcrypt-sha256$v=2,t=2b,r=<rounds>$<salt>$<digest>. The RFC example is 83 characters. Ordinary bcrypt is 60. Default cost 12.
  • This Week in PHP Internals | Oct 8, 2026 (dev.to 9 Oct 2026; meta 132842) made it the top story. Recap, not a merge. No Oct 15 episode that I can cite.
  • No vote. Last reply I can cite is 9 Oct 2026 14:02:48 UTC (132855): Passlib is not an IETF spec; OWASP’s pre-hash example uses HMAC-SHA384 plus a pepper stored off the hash. Tim Düsterhus on 5 Oct (132788) called it better than changing PASSWORD_BCRYPT. PR php-src#24073 opened 2 Oct, last update 6 Oct, still open.
  • The throw RFC is now Inactive on the official index. bcrypt_max_password_length still shows vote 0–0–0. That matches Sjoerd’s 5 Oct note: not worth pursuing for now.
  • Laravel 13 has no SHA256 driver. Hashing docs: driver bcrypt. BCRYPT_LIMIT is null. BcryptHasher calls PASSWORD_BCRYPT and, with HASH_VERIFY true, throws RuntimeException: This password does not use the Bcrypt algorithm. unless algoName === 'bcrypt'. HashManager: bcrypt, argon, argon2id. Not a Laravel-core CVE.

What I will treat as rumor:

  • Shipped in 8.5 or 8.6. php.watch 8.5 (retrieved 16 Oct 2026): latest stable is still 8.5.11 (24 Sep 2026). 8.5.12 is an RC. php.watch 8.6: 8.6.0RC3 latest; GA 19 Nov 2026. php.net’s 2026 archive still points RC4 at 22 Oct 2026. This RFC targets the next 8.x.
  • FPM already has the constant. Missing here. Vote table empty.
  • Hash::make emits $bcrypt-sha256$ after apt upgrade. Illuminate has no such driver. password_get_info on the Passlib example returned unknown. password_verify('password', $hash) returned false.

What I would change in a real Laravel/PHP app this week

Three edits. None of them is “wait for 8.7” or a fake constant.

1. Run the CLI lab on the binary Nginx actually uses. Do not paste a working $bcrypt-sha256$ login.

php -v
php -r 'echo PASSWORD_DEFAULT, PHP_EOL, defined("PASSWORD_BCRYPT_SHA256") ? "yes" : "no", PHP_EOL, implode(",", password_algos()), PHP_EOL;'
php-fpm8.3 -v   # or php-fpm8.4 / php-fpm8.5 — the pool in the vhost

Then the before. I am not inventing the 8.7 identifier:

$long = str_repeat('a', 80);
$hash = password_hash($long, PASSWORD_BCRYPT);

var_dump(password_verify($long, $hash));                 // true
var_dump(password_verify(substr($long, 0, 72), $hash));  // true

$passlib = '$bcrypt-sha256$v=2,t=2b,r=12$n79VH.0Q2TMWmt3Oqt9uku$Kq4Noyk3094Y2QlB8NdRT8SvGiI4ft2';
var_dump(password_get_info($passlib)['algoName']);       // unknown
var_dump(password_verify('password', $passlib));         // false

try {
    password_hash('x', 'bcrypt-sha256');
} catch (ValueError $e) {
    echo $e->getMessage(), PHP_EOL;
}

On this box that printed:

PHP 8.3.6 (cli) (built: Sep  2 2026)
PASSWORD_DEFAULT=2y
defined_BCRYPT_SHA256=no
algos=2y,argon2i,argon2id
verify_80=true
verify_first_72=true
passlib_algoName=unknown
passlib_verify=false
ValueError: password_hash(): Argument #2 ($algo) must be a valid password hashing algorithm

No deprecation handler fired. CLI 8.3.6 and an 8.5 FPM socket are two labs. The Passlib string is the RFC’s own interoperability example. Today it is just text.

2. Stop concatenating a pepper into bcrypt. Do not call a constant that is not defined.

The wall in app/ is still Hash::make($pepper . $request->password) filling seventy-two bytes with a static prefix. Laravel 13 will not grow PASSWORD_BCRYPT_SHA256 this week. BCRYPT_LIMIT is still null.

use Illuminate\Support\Facades\Hash;
use InvalidArgumentException;

$password = $request->string('password')->toString();

// Before — bcrypt sees strlen() bytes. A 40-byte pepper already crosses 72.
// Hash::make($pepper . $password);
// password_hash($password, PASSWORD_BCRYPT_SHA256); // undefined on this CLI

// After — keep the hasher I actually have.
if (strlen($password) > 72) {
    throw new InvalidArgumentException('Password exceeds bcrypt 72-byte window.');
}

$hash = Hash::make($password);

Password::max(72) counts characters. bcrypt cuts bytes. A 36-character Cyrillic passphrase is already 72 bytes. If I need a pepper, hash_hmac('sha256', $password, $pepper) then argon2id — not $pepper . $password into bcrypt, and not a constant this binary rejects. The RFC’s HMAC uses the bcrypt salt as the key, not an app pepper. Those are different constructions. I would not invent that Illuminate already wraps Passlib v2.

3. Turn the brake on, or leave bcrypt. Measure the column I already have.

HASH_DRIVER=bcrypt
BCRYPT_ROUNDS=12
BCRYPT_LIMIT=72

Or, if password_algos() already lists argon2id (it did on this 8.3.6 CLI):

HASH_DRIVER=argon2id
ARGON_MEMORY=65536
ARGON_TIME=4
ARGON_THREADS=1

Then php artisan config:publish hashing if the file is missing, and php artisan config:clear. Cached config is how I would edit .env and still hash bcrypt for a week. 13.x defaults HASH_VERIFY=true, so a $bcrypt-sha256$ row — even the 83-character Passlib example — would throw RuntimeException on Hash::check because algoName is not bcrypt. varchar(255) already fits 83 characters. A 60-byte column would not.

What would bite on a self-hosted VPS

 CLI lab: defined(), 80-byte bcrypt, then a Passlib string that stays unknown

Caption: php -v first. Prove the constant is missing. Hash 80 bytes. Then feed the RFC’s Passlib example to password_get_info. unknown plus ValueError means I am still on today’s bcrypt, not the draft.

First bite: the socket, not the RFC. I can prove the constant is missing in CLI 8.3.6 and leave Nginx on an 8.4 pool I never checked.

Second bite: BCRYPT_LIMIT=72 without a form rule. Illuminate throws InvalidArgumentException. The user sees a 500. I would add a strlen() rule on register before I flip the env.

Third bite: argon2id is not a panel checkbox. It was in password_algos() here. Laravel’s default argon memory is 65536 KiB — tight on a 1 GB VPS. Distro PHP is still 8.3.6-0ubuntu0.24.04.11. I would not invent that apt shipped 8.7. config.platform.php stays on the FPM I serve.

Two RFCs, one empty vote, one Inactive

 RFC timeline from the 2 Oct PR to this 16 Oct lab

Caption: PR opened 2 Oct. Changelog 4 Oct. Internals announce 5 Oct. PR last touched 6 Oct. Internals recap 8–9 Oct. I am writing 16 Oct. The SHA256 RFC is Under Discussion. The throw RFC is Inactive. 8.6 GA is still 19 Nov 2026.

Honest target this week: Laravel 13 on the FPM I already have, BCRYPT_LIMIT either null or 72 on purpose, no pepper concatenated into bcrypt, no invented constant. Next calendar check is 8.6 RC4 on 22 Oct 2026 and 8.6 GA on 19 Nov 2026 — still not this identifier.

Are you on this in production?

If FPM already hashes the raw password with bcrypt, BCRYPT_LIMIT is still null, defined('PASSWORD_BCRYPT_SHA256') is false, and you never prepend a nonce or pepper into Hash::make, you are on this the boring way: the 72-byte window exists and the opt-in constant does not. If CLI and the Nginx socket disagree, or config:cache still has HASH_DRIVER=bcrypt after you edited .env, or the host will not give you argon2id in password_algos(), you are not. Shared hosting that will not let you set BCRYPT_LIMIT is the fourth camp. I did not flip a production hasher tonight. I want to know which camp you are in.

Did you hit the same wall?

I would get stuck on ValueError: Argument #2 ($algo) must be a valid password hashing algorithm the afternoon I pasted PASSWORD_BCRYPT_SHA256 into app/, or on RuntimeException: This password does not use the Bcrypt algorithm if I dropped a Passlib $bcrypt-sha256$ row into Laravel with HASH_VERIFY still true. Did you hit the same thing, or a different one — verify(80) === true with no notice, Password::max(72) letting a 40-character CJK passphrase through, a cached hashing config, argon2 missing from php -m? Tell me in the comments. I read them.

References

Share:

Get new posts in your inbox

No spam. One short email per new article — practical PHP, Laravel, devops, and AI-assisted workflows.

Comments

Powered by GitHub Discussions via Giscus. A free GitHub account is required.