Security

Mystery user accounts in Joomla 4: the profile save that forgets to check login

Mystery user accounts in Joomla 4: the profile save that forgets to check login

Spam accounts that keep appearing with registration off often come in through a profile save that never checks login. How to find the hole and close it.

You open Users, Manage one morning and the list is longer than it was last night. A dozen accounts you never approved, with addresses nobody will ever read. You tighten the registration settings, delete them, and the next day there are more. The registration form is not the way in. A profile controller that saves without checking who is logged in is.

What goes wrong

The profile editor in com_users has two steps. The edit task opens the form for the logged-in user, and the save task writes the submitted data back. The edit task checks that the person is editing their own profile. The save task is supposed to be reachable only by someone who already passed that check.

On a stock, up-to-date Joomla site the save task is wired to a logged-in identity. The problem shows up when that controller has been changed: an override in a template or a plugin, a patch somebody pasted in years ago, or an edited core file that survived updates. If the changed save() never asks whether the visitor is a real, logged-in user, a request can arrive with no account behind it and the code treats the user id as 0. A user id of 0 is not "nobody"; to the save routine it is a brand new user waiting to be created. The form is submitted, the data validates, and Joomla stores an account. Repeat that in a loop and you get the list of strangers.

The giveaway is that the new accounts do not look like registration spam. Registration respects the New User Account Activation setting and the registration form fields. These come in through the profile path, so activation and the "allow user registration" switch do not stop them. Turning off registration changes nothing, which is exactly why it feels like the setting is broken.

Find out whether this is you

First, confirm the accounts are arriving through the profile save and not through registration. Look in the user list for accounts whose registration date is recent but which have no activation record, or whose usernames and emails follow a pattern a bot would generate. If you keep server access logs, search them for requests to index.php carrying task=profile.save from visitors who never logged in.

Then find out which controller is actually running. The file that ships with Joomla is components/com_users/src/Controller/ProfileController.php. Compare it against a clean copy of the same Joomla version, which you can download from the Joomla site or read in the public repository. If your file differs, something has edited it. Also check templates/ and any custom plugin for a class that extends or replaces the profile controller. An override is the usual culprit, because it is the one place a well-meaning edit does not get wiped by the next update.

The fix

The save routine needs to refuse anyone who is not logged in before it touches the form. Open the save() method and, right after it works out the current user and user id, add a guard:

$user   = $this->app->getIdentity();
$userId = (int) $user->id;

// A profile save only makes sense for a real, logged-in account.
if ($user->guest || $userId <= 0) {
    throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}

$user->guest is true for anyone without a session, and the id check catches the edge case where an identity exists but has no account behind it. Either one throws a 403 and the request stops before a single field is read. A logged-in person editing their own profile never trips it, so normal use does not change.

Make sure Joomla\CMS\Language\Text is in the file's use statements. It usually already is.

Put it in the right place

Do not edit the core file. An edit there is gone the moment you update Joomla, and you are back to deleting strangers. If the bad controller is an override, fix the override. If it is an edited core file, the better move is to restore the clean core file and, if you still need custom behaviour, move that behaviour into a proper override under templates/your-template/html/ or into a small plugin. The point is that your guard lives somewhere an update will not overwrite.

Test it on a copy first

Work on a staging copy, never on the live site. Log in with an ordinary Registered account, edit your profile, change a field and save. The save has to keep working and your change has to stick. That proves the guard does not lock out real users.

Then confirm the hole is closed. In Users, Manage, note how many accounts exist. Use the site the way a logged-out visitor would, without signing in, and try to reach the profile save. You should be turned away with an access error, and the account count must not move. If you see "Invalid token" instead of an access error, that is Joomla's forgery check stopping the request earlier; it does not prove your guard works, so test again from a logged-out session that still carries a valid form token.

When both tests pass on staging, apply the same change to the live site and watch the user list for a day.

Keep it from coming back

Two habits close this off for good. Keep Joomla updated, so the controller that runs is the current, reviewed one. And keep your customisations out of core files, in overrides or plugins, so an edit that adds a check does not quietly disappear and an edit that removes one is easy to spot. A profile save is a write to the user table. Anything that writes to the user table should start by asking who is asking.

Stuck on something else?

Doctor Joomla is a project of Cloudfaction, a Dutch agency building and maintaining Joomla sites since 1999. The articles are made with the help of AI, so tell us if something does not match your screen. Mail info@cloudfaction.nl if you want someone to look at yours.

Rather have someone look at it

Doctor Joomla is a Cloudfaction project.

A Dutch agency that has been building and running Joomla sites since 1999. If your site needs more than an article can fix, get in touch.