The general attack scenario:
In 2013 Django had a general vulnerability as an attacker could create extremely intense CPU calculations via very large passwords [see the security notice here]. I'm unsure if this is still possible when using PHP's password_verify() and other password-hashing methods without any further checks.
The PHP documentation says:
Using the PASSWORD_BCRYPT for the algo parameter, will result in the password parameter being truncated to a maximum length of 72 characters.
But, PHP's code MAYBE says something different:
The C code behind PHP 5.5.0's password_verify() function however does not limit the passed argument directly (maybe on a deeper level inside the bcrypt algorithm ?). Also, the PHP implementation does not limit the argument.
The question:
Is password_verify() (and other functions of the same function set) vulnerable against DoS via maxed out POST parameters ? Please also consider site-wide config situations of POST upload sizes much larger than 4MB.
The password is limited to 72 characters internally in the crypt algorithm.
To see why, let's look at crypt()
's source: ext/standard/crypt.c
} else if (
salt[0] == '$' &&
salt[1] == '2' &&
salt[3] == '$') {
char output[PHP_MAX_SALT_LEN + 1];
memset(output, 0, PHP_MAX_SALT_LEN + 1);
crypt_res = php_crypt_blowfish_rn(password, salt, output, sizeof(output));
if (!crypt_res) {
ZEND_SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1);
return NULL;
} else {
result = zend_string_init(output, strlen(output), 0);
ZEND_SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1);
return result;
}
The password
field is a simple char*
field. So there's no length information. All that's passed is a normal pointer.
So if we follow that through, we'll eventually land at BF_set_key
.
The important part is the loop:
for (i = 0; i < BF_N + 2; i++) {
tmp[0] = tmp[1] = 0;
for (j = 0; j < 4; j++) {
tmp[0] <<= 8;
tmp[0] |= (unsigned char)*ptr; /* correct */
tmp[1] <<= 8;
tmp[1] |= (BF_word_signed)(signed char)*ptr; /* bug */
if (j)
sign |= tmp[1] & 0x80;
if (!*ptr)
ptr = key;
else
ptr++;
}
diff |= tmp[0] ^ tmp[1]; /* Non-zero on any differences */
expanded[i] = tmp[bug];
initial[i] = BF_init_state.P[i] ^ tmp[bug];
}
BF_N
is defined to be 16. So the outer loop will loop 18 times (BF_N + 2
).
The inner loop will loop 4 times. 4 * 18 == 72.
And there you have it, only 72 characters of the key will be read. No more.
Note
Now, there's an interesting side-effect to that algorithm. Because it uses C-Strings (strings terminated by a \0
null byte), it's impossible for it to use anything past \0
. So a password that contains a null-byte will lose any entropy past it. Example: http://3v4l.org/Y6onV