StyleSmuggler Remediation: Invalidating Every Magento Admin Session and Password
This post follows on from the StyleSmuggler exploit coverage: the VULN-39341 patch analysis and removing Adobe’s Fastly rules that break admin page saves.
Patching CVE-2026-75650 closes the hole. It does nothing about anyone already inside. If the exploit was used against you before you patched, the attacker may be sitting on a valid admin session cookie, and possibly on a copy of your admin_user table. Neither expires because you deployed a patch.
There is no admin UI for this and no bin/magento command that does it. The quickest way to end every admin session and every admin password at once is two SQL statements:
UPDATE admin_user SET password = 'invalid'; UPDATE admin_user_session SET status = 3 WHERE status = 1;
Run them in that order, on the live database, and everyone is out.
Wiping every admin password hash
That is the point of the first statement. A stolen hash is now useless and so is a password the attacker already cracked or phished, because nothing they can type will match.
It does not throw a 500 on the next login attempt, which is the thing worth knowing before you run it. Magento\Framework\Encryption\Encryptor::isValidHash() calls explodePasswordHash(), which splits on : and expects three parts. The string invalid has none, so it throws RuntimeException('Hash is not a password hash'). That is caught one frame up by a catch (\Throwable) which falls back to $recreated = $this->hash($password), and the subsequent Security::compareStrings() fails. The admin gets the ordinary “The account sign-in was incorrect or your account is disabled temporarily” message. No fatal, no stack trace, no clue to anyone probing the login form that the column has been wiped.
Ending every live session
The second statement does that. admin_user_session is owned by Magento_Security, and its status column is declared in vendor/magento/module-security/etc/db_schema.xml as an unsigned smallint defaulting to 1. There is no lookup table, no enum and no foreign key on it, so the meaning of the number lives entirely in PHP.
Where the constants are
They are class constants on Magento\Security\Model\AdminSessionInfo, which is vendor/magento/module-security/Model/AdminSessionInfo.php. All four in full:
public const LOGGED_OUT = 0; public const LOGGED_IN = 1; public const LOGGED_OUT_BY_LOGIN = 2; public const LOGGED_OUT_MANUALLY = 3;
What each one means in practice:
0,LOGGED_OUTA normal end to a session. Written byAdminSessionsManager::processLogout()when someone signs out, byAdminSessionInfo::checkActivity()when the row is older than the configured admin session lifetime, and byUserExpirationManagerwhen an account’s expiry date passes.1,LOGGED_INLive. This is the only value the plugin below treats as valid, and the only one you need to match on.2,LOGGED_OUT_BY_LOGINThe same account signed in somewhere else and the config is set to allow a single session. Produces “Someone logged into this account from another device or browser. Your current session is terminated.”3,LOGGED_OUT_MANUALLYSomebody deliberately ended the session from outside it. Produces “Your current session is terminated by another user of this account.”
There is a fifth value that is not on this class at all. AdminSessionsManager::LOGOUT_REASON_USER_LOCKED = 10 lives on Magento\Security\Model\AdminSessionsManager and is only ever used to pick a logout message, never written to the column. Do not use it in SQL.
Why 3 is the right number
LOGGED_OUT_MANUALLY is not an arbitrary pick. It is exactly what core writes when an admin uses the “Log Out All Other Sessions” button on their account page. That controller is Magento\Security\Controller\Adminhtml\Session\LogoutAll, and it calls AdminSessionsManager::logoutOtherUserSessions(), which ends with:
$collection->setDataToAll('status', \Magento\Security\Model\AdminSessionInfo::LOGGED_OUT_MANUALLY) ->save();
The same method is called internally when a password changes. So the SQL above is the core behaviour with the per-user filter taken off: instead of “every other session belonging to me”, it is “every live session belonging to anyone”.
How the status gets enforced
Enforcement is a plugin, security_admin_sessions_prolong, declared in vendor/magento/module-security/etc/adminhtml/di.xml and pointing at Magento\Security\Model\Plugin\AuthSession::aroundProlong(). Every admin request prolongs the session, and that method starts with:
if (!$this->sessionsManager->getCurrentSession()->isLoggedInStatus()) { $session->destroy(); $this->addUserLogoutNotification(); return null; }
isLoggedInStatus() is a straight getData('status') == self::LOGGED_IN comparison against the database row. Set it to anything else and the next request the attacker makes destroys their session server side. You do not need to flush Redis or clear var/session for this to take effect, because the check is against the row, not the session payload.
The message the user sees comes from AdminSessionsManager::getLogoutReasonMessageByStatus(), a switch over those same constants. That is the practical argument for 3 over 0: your own staff get told the session was terminated rather than being silently dumped at the login screen.
Getting back in
You have just locked yourself out too, so know the way back before you run it.
The forgot-password flow still works. It sets a fresh rp_token and the reset writes a new hash without ever validating the old one, so any admin whose email address is correct and whose mail is actually being delivered can recover on their own. Test that transactional email is working before you wipe the column, not after.
Failing that, create a new admin from the CLI:
bin/magento admin:user:create \ --admin-user=recovery \ --admin-password='...' \ --admin-email=you@example.com \ --admin-firstname=Recovery \ --admin-lastname=Admin
If you want the option of putting things back exactly as they were, take a copy of the column first:
CREATE TABLE admin_user_password_backup AS SELECT user_id, username, password FROM admin_user;
Think about whether you want that, though. After a suspected compromise the old hashes are the thing you are trying to get rid of, and a table full of them sitting in the same database is a liability. Drop it once everyone has reset.
Caveats
Magento_Security has to be enabled
With the module off the second statement does nothing. No plugin, no status check, and the rows are inert. Some projects disable that module to stop the “session terminated” behaviour annoying staff who share logins. Check with bin/magento module:status Magento_Security first. If it is off, flush the session storage instead: redis-cli -n <session-db> FLUSHDB, or rm -rf var/session/* on a file-backed store.
session_id is not what it looks like
The column is declared varchar(1) and commented “Deprecated: Session ID value no longer used” in db_schema.xml. Do not try to target individual sessions through it.
This covers admin sessions only
Integration and API access is separate, in oauth_token and the integration tables. Customer sessions are separate again. If the compromise reached the point of creating an integration, revoking admin passwords does not touch it, and that is the persistence mechanism I would expect to find rather than a session cookie.
Patch first, then invalidate
Kicking everyone out of an unpatched store buys you minutes. The order is patch, deploy, then invalidate, then have everyone reset through the forgot-password flow.
I ran the code paths above against an Adobe Commerce 2.4.8-p5 installation, magento/module-security 100.4.8-p4. The constants and the plugin have been stable for a long time, but check them on your own version before trusting a number out of a blog post.
Found this useful? Everything here is free and stays that way. If it saved you an afternoon, you can buy me a coffee.