Multi-factor authentication is missing
CWE-308CWE-287OWASP A07:2021Updated September 4, 20265 min read
If an application offers no second factor, all access rests on one password. That password may have leaked elsewhere, been guessed, or been handed over through phishing, and in all three cases the account lies open. A second factor breaks that single dependency, provided the chosen method is itself resistant to phishing.
As long as an account is protected by a password alone, its security is only as strong as a secret your user may also have used elsewhere. That is the heart of the problem: you are securing your application with something you have no control over. A second factor changes that picture, and choosing the right form is the difference between a real barrier and an apparent one.
What is multi-factor authentication?
Multi-factor authentication (MFA) means a user proves their identity with evidence from more than one category: something they know (a password), something they have (a key or phone) and something they are (a fingerprint or face scan). What matters is the combination of categories, not the number of steps; two passwords in succession is not MFA.
The underlying idea is that it is plausible for one piece of evidence to fall into the wrong hands, and unlikely for both to do so at once. A password leaks through a breach at another service, gets guessed, or is handed over on a fake login page. A physical key or a linked phone does not automatically follow.
It is the bank vault that needs two keys at once: the customer’s and the bank employee’s. Not because either key is bad, but because the risk of both ending up with the same person is substantially smaller.
Which forms offer real protection?
Not every second factor is equally strong, and the difference lies mainly in resistance to phishing.
Vulnerable:
// One factor, and access is settled
app.post('/login', async (req, res) => {
const user = await users.findByEmail(req.body.email);
if (!user || !await argon2.verify(user.hash, req.body.password)) {
return res.status(401).send('Invalid credentials');
}
req.session.userId = user.id;
res.send('Welcome'); // full access
});
Whoever has the password is in. And that password comes, in practice, from one of three sources: a breach at another service where the user reused it, a successful guess, or a phishing page where they entered it themselves.
Safe:
app.post('/login', async (req, res) => {
const user = await users.findByEmail(req.body.email);
if (!user || !await argon2.verify(user.hash, req.body.password)) {
return res.status(401).send('Invalid credentials');
}
// No session yet: only a short-lived, restricted intermediate state
req.session.secondFactorFor = user.id;
req.session.expiresAt = Date.now() + 5 * 60 * 1000;
res.redirect('/login/verify');
});
app.post('/login/verify', async (req, res) => {
const id = req.session.secondFactorFor;
if (!id || Date.now() > req.session.expiresAt) {
return res.status(401).send('Verification expired');
}
if (!await verificationValid(id, req.body)) { // passkey or TOTP
await recordFailure(id);
return res.status(401).send('Verification failed');
}
req.session.regenerate(() => {
req.session.userId = id; // only now a real session
res.redirect('/');
});
});
The crucial detail is that after the password there is not yet a usable session. The intermediate state only grants the right to complete verification, is short-lived, and gives no access to any data. A common mistake is to create the session immediately and enforce the second factor only in the interface, which can then be bypassed with a direct request.
For the verification itself, strength varies widely. Passkeys and hardware keys following the WebAuthn standard are phishing-resistant because the key is bound to your domain. An authenticator app with one-time codes is a good middle ground. SMS is the weakest form, because the code can easily be requested on a fake page and relayed immediately.
What is the impact of missing MFA?
The severity is medium to high, depending on what sits behind the application and whether administrator access is involved. What sets this finding apart is that the attack need not begin in your application at all.
In credential stuffing, combinations from earlier breaches are tried automatically. Because password reuse is widespread, that reliably yields a percentage of successful logins, without anything being wrong with your security. In phishing, the user hands over their password voluntarily on a fake page. In both cases there is no vulnerability in your code; there is simply nothing stopping the stolen password.
The consequences follow accordingly. For an ordinary user account it means access to personal data and the ability to act on that person’s behalf. For an administrator account it means the whole system. And because the attacker arrives with valid credentials, their activity looks like normal use in the logs, which makes detection considerably harder.
How do you detect missing MFA?
The first observation is simple: does the application offer a second factor, and if so, which forms? Then begins the investigation that really says something.
A tester checks whether the second factor is enforced on the server by skipping the verification screen and calling a protected route directly with the session created after the password step. If that works, the MFA is cosmetic. They also check whether the intermediate state is short-lived, whether the number of verification attempts is limited, and whether one-time codes really can be used only once. The detours get attention too: the password reset function, recovery codes, and API endpoints or older protocols that ask only for a password. That last one is a classic finding: an application with proper MFA in the web interface whose API accepts the same credentials without a second factor. AssistSec tests those routes explicitly, because the weakest entrance determines how strong authentication really is.
How do you prevent missing MFA?
- Offer multi-factor authentication for all accounts and make it mandatory for administrators and sensitive roles.
- Prefer passkeys or hardware keys following WebAuthn; those resist phishing.
- Use an authenticator app with time-based codes as an alternative, and SMS only as a last resort.
- Create a full session only after the second factor has been verified, not before.
- Enforce verification on the server on every route, including APIs and older protocols.
- Limit the number of verification attempts and invalidate one-time codes immediately after use.
- Secure the recovery route as strongly as the login route; otherwise you only move the problem.
- Use number matching with push notifications to counter MFA fatigue.
- Ask for the second factor again before sensitive actions and on an unknown device or unusual location.
Sources
Frequently asked questions
Is SMS as a second factor better than nothing?
Yes, considerably better than nothing, but it is the weakest common form. SMS is vulnerable to SIM swapping, to interception in the mobile network and above all to phishing: a fake login page simply asks for the code and uses it immediately. Treat it as an interim step, not an endpoint.
What makes passkeys phishing-resistant?
The key is cryptographically bound to the domain it was created for. A fake site on a different address simply is not offered the key, however convincing it looks. There is no code the user can pass on, and therefore nothing to intercept.
Does MFA have to apply to every login?
Not necessarily. A trusted device can be remembered for a limited period, provided that choice is revocable and bound to the device. Do always ask for the second factor on a new device, from an unusual location, and before sensitive actions such as changing a password or payment details.
What is MFA fatigue?
An attack in which someone with a stolen password repeatedly attempts to log in, so the victim receives a stream of push notifications and eventually approves one to make it stop. Number matching and a limit on the number of attempts are the countermeasures.
Related articles
- VulnerabilitiesCWE-287A07:2021Broken authenticationBroken authentication explained: how attackers take over accounts through brute force, leaked passwords and predictable session tokens, and how to stop them.
- VulnerabilitiesCWE-307A07:2021Brute force and credential stuffingBrute force and credential stuffing explained: how attackers guess passwords or replay leaked logins, and how throttling and MFA shut both attacks down.
- VulnerabilitiesCWE-308A07:2021Multi-factor authentication is not enforcedMFA that is available but not mandatory gets enabled by few users. Learn how to enforce it without locking your users out.
- VulnerabilitiesCWE-204A07:2021Discovering valid usernamesDifferent error messages at login reveal which accounts exist. Learn how attackers abuse that and how to make the responses identical.
- VulnerabilitiesCWE-521A07:2021Weak password requirementsComplexity rules produce weaker passwords than length and a blocklist. Learn which requirements actually work under current guidance.