Okta Bcrypt Authentication Bypass Explained
Last week, on October 30th, Okta released an interesting security advisory detailing a vulnerability that could potentially lead to an authentication bypass. According to Okta, the vulnerability was discovered during an internal review and was promptly addressed. Okta was transparent about the issue, sharing the details publicly.
The advisory has generated significant discussion on social media, so I wanted to provide my perspective and explain the technical aspects of this vulnerability.
Let’s start with the official description provided by Okta:
This vulnerability affects cache key generation in AD/LDAP authentication. Specifically, it stems from an issue in the bcrypt algorithm used by Okta. If exploited, the vulnerability could allow a user to authenticate using a previously stored cache key from an earlier successful login.
Exploitation of the vulnerability requires specific conditions to be met, which limits its impact. However, one noteworthy condition is that the vulnerability is only exploitable when the username is 52 characters or longer.
Understanding the Technical Implications
This requirement around username length is particularly interesting because it hints at a limitation within bcrypt’s handling of longer strings in cache key generation. Bcrypt is commonly used for securely hashing passwords, due to work factor, number of iterations can make brute force attack too expensive to crack but in this case, it seems the bcrypt algorithm’s size limitation causes unexpected behavior.
When a username exceeds 52 characters, it could lead to a situation where the hash control fails and this effectively allows a user to bypass the intended authentication flow by using a previous session’s credentials.
To understand the problem better, we can check the implementation of bcrypt in different languages.
The Golang implementation at https://pkg.go.dev/golang.org/x/crypto/bcrypt is straightforward and gives us insight into what the problem might be.
As shown in bcrypt.go on line 96, if the input is less than 72 bytes, it returns an error message.
So, either the implementation is not checking the size, or the developer might have failed to handle errors in the library.
Let’s check the one of the node.js implementation, node.bcrypt.js:
As seen in the code above, node.js hash function does not check for data size or throws errors.
Another interesting finding in the same library was in the /src/bcrypt.cc
file, where the key_len
was kept at 72 bytes even when the length exceeded this limit, which I believe this is the root cause.
...
220. /* cap key_len at the actual maximum supported
221. * length here to avoid integer wraparound */
222. if (key_len > 72)
223. key_len = 72;
...
To confirm our findings let’s develop an flawed usage and see the problem with our own eyes, the source code can be found here :
const bcrypt = require('bcrypt');
const saltRounds = 10;
// This is how bcrypt has been used:
// bcrypt(userid + username + password()
// we don't know how the userId's are generated, so use UUIDv4
var userid = "b91fa9b4-69f1-4779-8d45-73f8653057f3";
// very long username
var username = "my.very.long.username.with.more.characters@kondukto.io" // 54 bytes long
// valid random password
var password = "randomStrongPassword"
var validInput = userid + username + password;
// simulate bypass input -- can be anything
var password2 = "AAAAAAAAAAAAAAAAAAA"
var bypassInput = userid + username + password2;
bcrypt.genSalt(saltRounds, function(err, salt) {
bcrypt.hash(validInput, salt, function(err, hash) {
console.log(hash);
});
bcrypt.hash(bypassInput, salt, function(err, hash) {
console.log(hash);
});
});
└> node main.js
$2b$10$nI463MI5Jy9iCq1G6pAFxeiPQJm7jdIINwvs./c7ENYMI7ruPGUKe
$2b$10$nI463MI5Jy9iCq1G6pAFxeiPQJm7jdIINwvs./c7ENYMI7ruPGUKe
As expected two different inputs validInput
and badInput
return the same hash.
Apparently, nodejs implementation of bcrypt does not throw an error but rather ignores the rest of the input if more than 72 bytes long strings have been provided.
Mitigations
The simplest mitigation is to check and limit data to less than 72 bytes. Boundary checking is an effective defensive programming practice and I strongly advice all the developers to use boundary checks.
Alternatively, as Yan (https://x.com/bcrypt) suggest, hashing the input with SHA-256 before applying bcrypt cloud be an effective and practical solution.
Takeaways
This bug is noteworthy not only because of its potential impact on one of the largest security companies in the world but also because it underscores the importance of security testing of critical applications.
It is a particularly hard-to-find bug with potentially serious consequences, serving as a clear example of why we must be especially cautious when using open-source components.
This bug raises an important question: was it the fault of the open-source library developer or the application developer? Do developers truly understand the tools and libraries they use? I’m not looking for a scapegoat, but it’s clear that both sides might have prevented this bug.
Open-source tools and libraries will always be a part of the development process, but we must remember that even popular tools may have flaws. Application developers should be aware of the risks, and ideally, critical services should undergo rigorous testing, including periodic fuzzing.
We’ve added this example to our developer security training materials, and I believe it will help broaden developers’ perspectives on application security.
P.S. We don’t know exactly how Okta was using this library, but I believe this example demonstrates the potential use case and impact of the disclosed vulnerability.
Resources