Installing contrib modules can lead to TFA accidently being bypassed

Created on 22 December 2023, 11 months ago
Updated 12 March 2024, 9 months ago

Problem/Motivation

If a contrib module modifies the login form to change the validators or submit handler TFA security can be bypassed.

Modules known to cause this include alogin, email_login_otp and fancy_login.

fancy_login appears to be mitigated by πŸ“Œ Decorate the user.auth service Fixed for the 2.x branch. A known public issue exists in the fancy_login queue #3283566: In testing it seems to bypass the TFA module β†’ .

The alogin and email_login modules fail in that they unset form submit handler and replace it with their own. Validation had already occurred for the password in validateAuthentication() with the expectation that the submit handler would redirect to the OTP Entry page. This bypasses part of the mitigation in πŸ“Œ Decorate the user.auth service Fixed as the password validation occurs between our lock.

This is debatable as to who holds responsibility for this in regards to Drupal API. The custom modules are following API in modifying the form, yet they forcibly bypass without consideration for other modules. Equally TFA provides no attempts to mitigate this from occurring and we assume we, like these other modules, assume we are the only module providing validation. Because of how we construct the forms other modules have little knowledge that TFA is involved in the process.

We would also likely want protection for this before implementing πŸ“Œ Evaluate restoring using a form alter instead of extending UserLoginForm Active as that may make it more likely to occur.

Steps to reproduce

Configure and require TFA for a user as normal.
Install and enable the alogin module, do not configure it to be configured/required for any user
Login as a user with TFA configured and required, observe no prompt for TFA token occurs as alogin as authenticated the user as if TFA was not required.

As TFA must be installed on a site and code can be modified we can never guarantee 100% that code will not bypass TFA, however we can and should be expected to attempt to provide reasonable security interlocks.

Proposed resolution

Add code that reduces the risk of this occurring.

Remaining tasks

Discuss possible fixes

User interface changes

TBD

API changes

TBD

Data model changes

TBD

πŸ› Bug report
Status

Fixed

Version

2.0

Component

Code

Created by

πŸ‡ΊπŸ‡ΈUnited States cmlara

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @cmlara
  • Status changed to Needs work 11 months ago
  • πŸ‡ΊπŸ‡ΈUnited States cmlara

    3410444-event-sub-user-auth is a work in progress to start discussion. This branch provides an event subscriber to catch when bypass_tfa_auth_for_user is utilized and no token validation has occurred.

    An alternative we might want to evaluate is if we can perform a check in hook_user_login(). Perhaps a combination of the two options might provide sufficient interlocking.

  • πŸ‡ΊπŸ‡ΈUnited States cmlara

    Updating title to better reflect that this is related to installing contrib modules leads to TFA accidentally being bypassed (as oppose to intentionally disabling/bypassing TFA)

    Linking related core issue for adding better MFA support into the login process since in context its ultimately the adhoc nature that causes issues.

  • πŸ‡ΊπŸ‡ΈUnited States cmlara

    hook_user_login() is a no-go. It isn't always called after authentication. This can be seen with the http_basic authentication method. While we don't expect http_basic to have the same faults as the form its best not to leave it as a possibility.

    I did find a AccountEvents::SET_USER event that is dispatched whenever AccountProxy::setAccount() is called. I am working on converting the solution in 3410444-event-sub-user-auth to to this new event. This event happens sooner which could allow us to depend more on core to handle responses. In the case of the form based we can destroy the exception before hook_user_login() is even called and throw an exception to end code execution. In the case of http_basic (and other similar methods) we could trigger before any code action could occur.

  • πŸ‡ΊπŸ‡ΈUnited States cmlara

    Looking at AccountEvents::SET_USER and I'm realizing that users being able to be set without going through TFA is inherently also the cause of issues such as πŸ“Œ SA-CONTRIB-2023-030 and 2.x Active .

    I've been investigate if we can use this for more than just protecting where the forms are modified and instead make it more holistic having the security system defend more generally.

    AccountEvents::SET_USER is called:

    From user_login_finalize() when starting a session for a user. This occurs when logging in through the forms, using password reset.
    Hook_user_login() is called later from inside user_login_finalize().

    From the AccountSwticher Service (AccountSwitcher::switchTo):
    Used when when opening up an impersonation (single request) to allow code to run as one user while not logging out of the first user.

    AuthenticationSubscriber::onKernelRequestAuthenticate():
    This is done on every request. Prior to calling the subscriber obtains the current user from

    • Session based authentication grabs the UID from $session->get('uid);
    • http_basic authentication provider decodes the username/password before calling the UserAuth service. Auth methods validating passwords will follow similar methods
    • Any other authentication provider that validates the information provided, without calling UserAuth

    Those method calling the UserAuth service are easy to work with, we could set a flag and know the UserAuth was called and the request is inherently validated through TFA. That covers user_login_finalize() and onKernalRequestAUthenticate for http_basic and similar.

    onKernelRequestAuthenticate() for session based authentication could be handled by setting a value iset n hook_user_login() to validate the value isn't changed outside of TFA knowledge (avoiding $session->set('uid', SOME_UID);)

    AccountSwitcher can be exmpeted with a decorator indicating that the request is part of a UserSwitch event. There could be an argument for denying these requests, perhaps that could be a followup for a feature where more discussion on the benefits/risk of doing so.

    onKernelRequestAuthenticate() allowing user/password authentication (tokens) while blocking others is the complex one. This, after some deeper thought, can also be solved with a (dynamic) decorator on the methods that are permitted to bypass TFA (such as a token based auth method). This would require some manual configuration by site admins, however it can be limited to a simple configuration parameter and for many sites would require no action.

    I have most of the code for this already built as POC, it just needs cleanup and finalized testing to be sure no scenarios are missed. Addtionaly it likely needs documentation improvements to go with it.

  • πŸ‡ΊπŸ‡ΈUnited States cmlara

    Uploaded a first version of this so that I can step back for a few days and give it another look with a fresh set of eyes.

    This includes some decent technical documentation, though I'm realizing as I type that I didn't document in an admin friendly section so will need work for that.

    At the moment this will break one time login links. I consider that a proof that this is actually fixing our root level problem in TFA. We can fix the one time login links in πŸ“Œ Use an EventSubscriber to process one time login links Needs work .

    Looks like one of the tests runs well in my local lab but not in GitLab, will need to investigate options.

  • Status changed to Needs review 9 months ago
  • πŸ‡ΊπŸ‡ΈUnited States cmlara

    Did some cleanup, mostly around enabling strict types and using static data providers with some minor spelling/wording changes.

    If i had to split this up into chunks to make it smaller it would look roughly like:

    -- Add flag to complete session login.
    src/Form/EntryForm.php
    src/Form/TfaLoginForm.php
    tests/src/Unit/Form/TfaEntryFormTest.php
    tests/src/Unit/Form/TfaLoginFormTest.php
    tests/src/Unit/TfaModuleTest.php
    tfa.module

    -- Add flag on complete User.Auth login.
    src/TfaUserAuth.php
    tests/src/Unit/TfaUserAuthTest.php

    -- Add Auth Decorators
    src/Authentication/Provider/TfaAuthDecorator.php
    src/Authentication/Provider/TfaChallengeAuthDecorator.php
    src/Compiler/TfaAuthDecoratorCompiler.php
    src/TfaServiceProvider.php
    tests/src/Unit/Authentication/TfaAuthDecoratorTest.php
    tests/src/Unit/Authentication/TfaChallengeAuthDecoratorTest.php
    tests/src/Unit/Compiler/TfaAuthDecoratorCompilerTest.php
    tests/src/Unit/TfaServiceProviderTest.php

    -- Add flag for user.switcher
    src/TfaAccountSwitcher.php
    tests/src/Unit/TfaAccountSwitcherTest.php

    -- Enforce TFA completed.
    docs/SUMMARY.md
    docs/configuration/SUMMARY.md
    docs/configuration/exempt-auth-provider.md
    docs/development/SUMMARY.md
    docs/technical/SUMMARY.md
    docs/technical/set-user-protection.md
    src/EventSubscriber/TfaUserSetSubscriber.php
    src/Exceptions/TfaAccessDenied.php
    tests/src/Unit/EventSubscriber/TfaUserSetSubscriberTest.php
    tests/src/Functional/TfaTestBase.php

  • Status changed to Fixed 9 months ago
  • πŸ‡ΊπŸ‡ΈUnited States cmlara
    • cmlara β†’ committed 01d74999 on 2.x
      Issue #3410444 by cmlara: Require TFA validation for SET_USER event
      
  • Automatically closed - issue fixed for 2 weeks with no activity.

Production build 0.71.5 2024