Skip to content

Multi-factor authentication can be disabled without verification

CWE-306CWE-620OWASP A07:2021Updated September 4, 20265 min read

If multi-factor authentication can be disabled with only a valid session, the measure is bypassable by anyone holding such a session. The same applies to recovery codes that can simply be requested again. Disabling should demand the same confirmation as the action it protects.

A second factor exists to keep working at the moment something goes wrong with the password or the session. If that factor can be switched off with a single click, it stops working precisely then. Disabling deserves the same protection as the function itself.

What goes wrong when MFA is switched off?

Multi-factor authentication is judged on how well it protects logging in. What regularly stays out of view is the question of how it can be switched off. Disabling without verification means a logged-in user can remove the second factor without demonstrating again who they are.

The consequence is that the protection is exactly as strong as the session. And sessions are precisely what a second factor is meant to compensate for: they can be hijacked through a shared computer, a stolen cookie, cross-site scripting or an unattended workstation. In all those cases the second factor is the layer that remains, unless it can be lifted with one request.

The same holds for the detours around it. Recovery codes are a full alternative to the second factor; if they can be requested again without confirmation, that is the same door under another name. It makes the lock on the safe comparable to a lock with the key hanging next to it.

How does an attacker switch off your MFA?

Vulnerable:

// Only a valid session is required
app.post('/account/mfa/disable', requireLogin, async (req, res) => {
  await users.disableMfa(req.user.id);
  res.send('Two-factor authentication disabled');
});

// And the recovery codes can simply be fetched again
app.get('/account/mfa/recovery-codes', requireLogin, async (req, res) => {
  res.json(await users.recoveryCodes(req.user.id));
});

An attacker who obtains a session in any way does not have to bypass the second factor; they switch it off. Then they change the password, and the account is theirs, with the protection the user had deliberately enabled as the first casualty.

The second endpoint is quieter still. The attacker leaves the second factor enabled, fetches the recovery codes and uses one later. The user notices nothing: their authenticator still works, nothing has been switched off, and no notification has been sent.

Safe:

app.post('/account/mfa/disable', requireLogin, async (req, res) => {
  const user = await users.find(req.user.id);

  // Password and the factor itself: prove that you possess it
  const passwordOk = await argon2.verify(user.hash, req.body.password ?? '');
  const factorOk = await checkMfa(user, req.body.code ?? '');

  if (!passwordOk || !factorOk) {
    await recordFailure(user.id, 'mfa-disable');
    return res.status(403).send('Confirmation failed');
  }

  await users.disableMfa(user.id);
  await users.removeRecoveryCodes(user.id);
  await sessions.removeOthersFor(user.id, req.sessionID);

  await mail.send(user.email,
    'Two-factor authentication has been disabled for your account. ' +
    'Was this not you? Restore it immediately here: ...');
  await auditlog.write('mfa disabled', { user: user.id, ip: req.ip });

  res.send('Two-factor authentication disabled');
});

The action now asks for the password and for the second factor itself. That last part is the decisive point: the user demonstrates that they possess the factor, not merely that they hold a session. On top of that, the recovery codes are invalidated, other sessions are ended, and the user receives a notification with a way back.

Do not forget the recovery route. An application where disabling is well protected but where the second factor can be re-enrolled through a single email confirmation is in reality only as strong as that email account. Attackers consistently pick the easiest of the available routes.

What is the impact of MFA that can be disabled without verification?

The severity is medium to high. What characterises this finding is that it removes the value of a measure the organisation regards as settled, which makes the risk invisible in its own reporting.

The scenario is always the same: an attacker holds a session or a password, but not the second factor. That is precisely the situation the factor exists for. If they can switch it off or fetch the recovery codes from that position, the barrier is gone and the takeover is complete.

For accounts with extensive rights this weighs heavier. An administrator for whom a second factor is mandatory, but who can switch it off themselves without confirmation, offers in practice no more protection than an administrator without the requirement. And because disabling is often neither logged nor notified, it goes unnoticed for a long time.

How do you detect MFA that can be disabled without verification?

A tester enables a second factor and then tries every route to get rid of it again. Can it be switched off with only the existing session? Is the password requested, and if so, is it actually verified on the server? Is the factor itself requested?

Then the detours are examined. Can recovery codes be requested again without confirmation? Can a new factor be enrolled alongside the existing one without confirming the old one, which lets the attacker add their own device without anything being switched off? Does the recovery process on loss work through a single email confirmation? And does the API impose the same requirements as the web interface? Testers also check whether the user receives a notification and whether the action lands in an audit log. AssistSec tests those routes separately, because protection is determined by the weakest of them and that is rarely the main route.

How do you prevent MFA that can be disabled without verification?

  • Ask for the current password and for the second factor itself when disabling it.
  • Impose the same requirements when adding or replacing a factor, not only when removing one.
  • Protect recovery codes equally well: show them once and ask for re-authentication before issuing a new set.
  • Invalidate recovery codes as soon as the second factor is disabled or re-enrolled.
  • Route recovery on loss through a verified second channel or through your service desk with an identity check.
  • Send an immediate notification to the known address, with an option to intervene.
  • End other sessions when the authentication settings change.
  • Record every change to the second factor in an audit log.
  • Impose the same requirements on the API as on the web interface.

Sources

Frequently asked questions

What should I ask for when disabling?

The current password and the second factor itself. The latter matters most: whoever wants to switch off the factor should demonstrate that they possess it. Otherwise someone with only a hijacked session can lift the protection.

What about recovery codes?

They are a full alternative to the second factor and therefore need the same protection. Show them once at setup, ask for re-authentication before showing them again, and invalidate a code after use.

And if a user loses their phone?

Then you need a recovery process, and that is exactly where things often go wrong. Route it through a verified second channel or through your service desk with an identity check, never through a single email confirmation. The detour must not be easier than the main road.

Should I notify the user?

Yes, always and immediately. Send a notification to the known email address as soon as the second factor is disabled or re-enrolled, with a way to intervene. Often that is the only signal the rightful owner receives.

Related articles

Press / to search · Esc