4

The "Forgot your password?" password reset form in Joomla 3.x can disclose that an email address is registered with the site. This is a personal privacy violation which is illegal under some privacy laws.

The problem is that the core Joomla function reports two different messages and formats:

  1. "Reset password failed: Invalid email address" when a
    non-registered address is entered into the reset form, and
  2. "An email has been sent to your email address. The email has a verification code, please paste the verification code in the field below to prove that you are the owner of this account." when a registered address is entered.

A third party can therefore determine that any email address is registered to and is associated with the site. Email addresses are commonly widely known, and in many cases are in the form Firstname.Lastname@

Can this be corrected with an override, to return the #2 response above regardless of the not/registered status of the email address entered?

If not, what core file(s) need to be changed?

I am not mentioning Username here because I am using a plugin which allows authentication by email address and password instead of Username and password.

Please note! If you are unfamiliar with these specific responses, they are different. #1 is text that appears in a Joomla error box. #2 appears as text at the top of the form. This disclosure problem is not solved by a language file override making the text identical.

2
  • I love this phoenix which has risen from the ashes of an earlier difficult question. This is good content. A clear and useful question peppered with very different and insightful answers. This is the best kind of outcome. Lots of knowledge being shared here, good job everyone -- very proud of this community right now. Oct 14, 2019 at 11:11
  • I'm delighted with the answers, all which I have read. I have not quite had time to dig in and apply them (disruptive life situation) but they have already served as a educational and useful point which to move forward on. One delay: I'm finally setting up Eclipse on Joomla so I can step through the code and understand the Joomla classes and MVC more clearly. I'll report back this week and also select an answer. Thank for everyone's help, forgiveness for my earlier muddled question and impatience.
    – brett
    Oct 15, 2019 at 15:57

3 Answers 3

2

Great Question Brett. This is one of the only core hacks I've ever had to make. If someone has a better solution then please post it here.

In this file: https://github.com/joomla/joomla-cms/blob/staging/components/com_users/models/reset.php

In the public function processResetRequest($data)

Where it checks to see if the user exists just return true early. This thus stops the email and reset process but also shows the regular success message to the user.

Original lines 398-404

// Check for a user.
    if (empty($userId))
    {
        $this->setError(JText::_('COM_USERS_INVALID_EMAIL'));
        return false;
    }  

Changed to:

// Check for a user.
    if (empty($userId))
    {
        /* === BEGIN CORE HACK === */
        return true;
        /* === END CORE HACK ===== */

        $this->setError(JText::_('COM_USERS_INVALID_EMAIL'));
        return false;
    }
2
  • Thanks for a super clear answer James. Unfortunately I'm still getting the Joomla error text box vs in-form status messages. Looking at the code you pointed me to (thanks!) I don't see why it wouldn't work at this late hour. Maybe something related to the auth-email plugin I am using changed something related? I will look at this more closely tomorrow and report my findings. BTW, I like your clear commenting, "BEGIN CORE HACK"
    – brett
    Oct 13, 2019 at 3:19
  • 1
    Note that if a nonexistent user takes a different amount of time to return, it will still be possible to tell if the email address is registered or not, even if the response message is identical. Also if there's some check when you register that tells you the email address is already taken or not, then this is pointless.
    – Kat
    Oct 13, 2019 at 15:31
2

To avoid a core hack there are two approaches I would suggest you could either create your own Model that would extend UserModelReset so that you can insert a fake user id when a valid one is not found. Or create an override for /com_users/view/reset/tmpl/default.php and replace line 24 to not call reset.request in the subcontroller reset.php but some code of your own.

<form id="user-registration" action="<?php echo JRoute::_('index.php?option=com_users&task=reset.request'); ?>" method="post" class="form-validate form-horizontal well">

Or if the plugin you mention is something you can edit then I would suggest you do your own check if the user exists in the com_user table and if not then pass a valid but fake userid to the regular processing, just don't allow a non-existent user to be passed. Not knowing when the plugin gets involved, I can't be sure this is a valid option for you, but would be easiest and cleanest way to do it.

If you follow the process of a normal password reset then you can see what is happening and why the code hack described by @jamesgarret is not working for you and you aren't getting to the final page displayed with the 'email sent' message. I would guess that your Plugin is calling processResetRequest directly and not going through the subcontroller reset.request.

As mentioned above default.php calls the subcontrolelr reset.php and function request. function request in turn calls processResetRequest in com_user/models/reset.php

If you read what is going on processResetRequest you can see that a database call for the user email address is made and that variable of $userId is returned at line 389.

As per @jamesgarret answer at line 399 a check is done for an empty $userId and it is at this point that you do NOT want have an empty $userId. If you used the core hack from @jamesgarret with the normal processing then return true gets you back to reset.request but if your Plugin is what called processResetRequest then you need to handle the redirect to the correct page yourself.

If you haven't already checked and passed a fake user id via your plugin then at line 401 you need to create a valid user id and give it to $userId so the processing can continue with valid User Id.

(changing line 401 would be a core hack or in an extended processReserRequest)

You have to create a fake user id in your com_user table that isn't a super user and has a valid email address that you can automatically discard when it is received by your mail server/account.

The reason you need this fake user id is so that processing can complete, an email is sent and you get to the bottom of the processResetRequest so that you get returned to the request function in the reset subcontroller. This is what redirects you to com_user/views/reset/tmpl/reset.php which is the screen you want with the right message that an email has been sent and everything looks genuine.

Incidentally the message you want to appear

"An email has been sent to your email address. The email has a verification code, please paste the verification code in the field below to prove that you are the owner of this account."

comes from com_users/models/forms/reset_confirm.xml and is a fieldset label at line 3.

<fieldset name="default" label="COM_USERS_RESET_CONFIRM_LABEL">
2

You can use a plugin onAfterRoute event to override controller tasks in a sense. It's not perfect but it beats core hacking. The following example mimics UsersControllerReset:request() but redirects to confirmation page even when model returns false.

defined('_JEXEC') or die;

use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\Route;

class PlgSystemExample extends CMSPlugin
{
    protected $app;

    public function onAfterRoute()
    {
        // Check that we are performing the correct task.
        if ($this->app->input->get('option') === 'com_users' && $this->app->input->get('task') === 'reset.request')
        {
            $this->request();
        }
    }

    protected function request()
    {
        // Check the request token.
        if (!$this->app->getSession()->checkToken('post'))
        {
            $this->app->enqueueMessage(Text::_('JINVALID_TOKEN_NOTICE'), 'error');
            $this->app->redirect(Route::_('index.php?option=com_users&view=reset', false));
        }

        $option = $this->app->input->get('option');

        // Define component paths. The model may need them.
        if (!defined('JPATH_COMPONENT'))
        {
            define('JPATH_COMPONENT', JPATH_BASE . '/components/' . $option);
        }

        if (!defined('JPATH_COMPONENT_SITE'))
        {
            define('JPATH_COMPONENT_SITE', JPATH_SITE . '/components/' . $option);
        }

        if (!defined('JPATH_COMPONENT_ADMINISTRATOR'))
        {
            define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR . '/components/' . $option);
        }

        // Load com_users language files.
        $this->app->getLanguage()->load('com_users');

        // Register com_users models.
        JModelLegacy::addIncludePath(JPATH_SITE . '/components/com_users/models', 'UsersModel');

        // Fetch the model.
        $model = JModelLegacy::getInstance('Reset', 'UsersModel');

        // Submit the password reset request.
        $data   = $this->app->input->post->get('jform', array(), 'array');
        $return = $model->processResetRequest($data);

        // Check for a hard error. It can occur when sending mail fails.
        if ($return instanceof Exception)
        {
            // Get the error message to display.
            if ($this->app->get('error_reporting'))
            {
                $message = $return->getMessage();
            }
            else
            {
                $message = Text::_('COM_USERS_RESET_REQUEST_ERROR');
            }

            // Go back to the request form.
            $this->app->enqueueMessage($message, 'error');
            $this->app->redirect(Route::_('index.php?option=com_users&view=reset', false));
        }

        // Redirect to confirmation page.
        $this->app->redirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false));
    }
}
1
  • This would be a great plugin if someone could throw together in GitHub or whatnot so basic users could download/install. Oct 15, 2019 at 17:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.