From the forge at Anvil Secure, welcome to Field Notes.
Each installment takes a real finding from our work and follows it from first clue to impact, looking at the small decisions and assumptions that allowed the issue to emerge.
Details have been altered to protect client confidentiality. The technical substance and lessons remain intact.
Field Notes: Issue 1
Predictable Usernames and Passwords
In this installment, we're looking at a code review finding โ predictable usernames and passwords โ from a medical health platform. It was a bug consisting of two parts, which is interesting mostly because the two halves together were worse than either on its own.
The platform supported a workflow that involved creating temporary user accounts on the fly. Think of sending appointment data for one-off patients that have not been fully signed up to the patient portal yet. What matters is that these accounts were real and active and usable for authentication against the platform.
The first part of the code that was responsible for this account was the following utility function.
function random_string() {
return substr(str_shuffle(md5(microtime())), 0, 10);
}
This function was then used further down in the code base:
$r = random_string();
$user->username = "temp_user_" . $r . "@[domain].com";
$user->email = "temp_user_" . $r . "@[domain].com";
$user->password = $user->hash("temp-prefix-" . $r);
$user->status = USER_ACTIVE;
Two things are wrong here. The first one is the one most reviewers notice immediately. The second one is the one that matters.
The obvious half
The random_string() looks random at a glance, but it isn't in any sense a cryptographer would recognize. The function microtime() returns a timestamp with microsecond precision, which means that if an attacker can predict when this function is being called (as they often can if they know the application flows in which this function is used), the input space to predict the output is something on the order of just a few million candidates. The MD5 hash of that input is deterministic, obviously, so it doesn't add entropy and just rearranges what little entropy there was.
The function str_shuffle() is worse, as it uses the internal PHP non-cryptographic random number generator and it generates predictable values based on the same seed state. Subsequently, the truncation to just ten characters of the hexadecimal string gives just a 40-bit (10 hexadecimal characters encode just 5 full bytes) output space, which means the whole output space is even narrower than what is generated. None if it is very subtle and all these choices would be a finding on their own; using microtime, the non cryptographically secure random number generator, MD5 as the stretching function, etc.
The half that matters
Now let's look at the username and the password generation. Both are derived from the same "randomly" generated ten-character string. The username is just `temp_user_` plus the string plus the domainname which is obviously known to the attacker. The password is a known prefix plus the same string.
This means the two values aren't independent. If an attacker can predict the username, which they can due to the first half of this finding as that one is entirely about how predictable the username is, they also already know the password. The password is not another factor of the account's identity next to their username, it is simply the same factor, wearing a different label.
Now let's assume that random_string() would be replaced with something cryptographically sound or at least unpredictable, for example, a UUID4 or just reading ten bytes from a local entropy source like /dev/random. The username would be completely unguessable, but the password would be derivable from the username directly for anyone who knows about the construction of the password. This means that instead of just it being about the random string generation being broken, just fixing that wouldn't fix the bug. Any leak of the username through any other channel (like a logfile, support ticket, misconfigured cache, or simply the assumption that usernames are quite often not considered secrets) and the password would still be calculable for free by an attacker.
Why it happens
The code that produced this finding was probably written quickly for a workflow that probably felt internal at the time it was being implemented. The temporary user has this connotation of not being a real valid user and it just being a placeholder. The credentials were subsequently treated as scaffolding, and nobody's supposed to care. With that framing, it might end up explaining why the password ended up being a function of the username. The developer wasn't thinking of it as a password in the way the login form down the hall was thinking of it as a password. The password was just a tag, attached to an account, and just to make the downstream auth check happy by having a set of credentials.
The general pattern is that there was one random value used for two purposes that the security model treats as independent. A username and password are supposed to be two different things. The username identifies, and the password authenticates. When they are drawn from the same source, the authentication layer is just doing theatrics. Analogously, Anvil has seen similar patterns at other engagements; think of a session token and a CSRF token derived from the same seed, or a password reset token and an account identifier also generated in the same call. In each case, the two controls that the system is treating as independent are in fact correlated, and the weaker of the two collapses to the strength of whatever channel leaks the other.
What to look for
When reviewing an authentication flow, it is worth asking not just "is this random value used here strong enough" but also "how many (in-)dependent random value does this flow actually have?". If the answer is fewer than the number of things which are supposed to be independent, there is a bug even when the RNG is perfect.
And related, anytime the word temporary pops up in a codebase, it is a prompt for the reviewer to read more carefully, not less. Temporary quite often means shortcut and shortrcuts accumulate over time while never getting cleaned up, and they can cause serious issues. Temporary accounts, temporary files, temporary permissions are all examples of such potential shortcuts. They all have their use, obviously, just particular care should be taken when reviewing them. In this case, the accounts for this health care application were intended to be very short-lived. This was the reason the code looked the way it did, and this issue manifested.
If any of this sounds familiar, or if you are curious to see what a careful look at your own authentication flows might turn up, please know that Anvil exists to help you answer that question. Get in touch if you'd like to talk it through, and subscribe to our newsletterย for more Field Notes and other research news from Anvil.
All details in this post have been altered to protect client confidentiality. The class of issues and technical substance is preserved; specifics are not.
Explore Further
Worried about predictable usernames and passwords in your own systems?
A careful look at your own systems might turn up more than you'd expect. We are here to help.
