Shopware: Privilege escalation: non-admin user with user:create ACL can create admin accounts
Description
UserController::upsertUser() writes user data in SYSTEM_SCOPE and does not filter the admin field. A non-admin API user with user:create or user:update ACL permission can set admin: true on new or existing users, escalating to full admin access.
The
Problem
In src/Core/Framework/Api/Controller/UserController.php, line 210-234:
public function upsertUser(?string $userId, Request $request, Context $context, ResponseFactoryInterface $factory): Response
{
$data = $request->request->all(); // raw request data, no field filtering
// ...
$events = $context->scope(Context::SYSTEM_SCOPE, fn (Context $context) =>
$this->userRepository->upsert([$data], $context)
);
}
SYSTEM_SCOPE bypasses AclWriteValidator entirely (line 52 of AclWriteValidator::preValidate() returns early for SYSTEM_SCOPE). The admin boolean field is accepted without restriction.
Compare with IntegrationController::upsertIntegration() in the same codebase, which correctly checks:
if ((!$source instanceof AdminApiSource)
|| (!$source->isAdmin()
&& isset($data['admin']))
) {
throw new PermissionDeniedException();
}
UserController is missing this exact check.
Impact
Any API user with the low-privilege user:create permission can create accounts with full admin access, or with user:update can promote any existing user to admin. This is a direct privilege escalation.
Suggested
Fix
Add the same isAdmin() check from IntegrationController:
$source = $context->getSource();
if ((!$source instanceof AdminApiSource) || (!$source->isAdmin() && isset($data['admin']))) {
throw new PermissionDeniedException();
}
Best regards, Keyvan Hardani
Affected products
1Patches
194569f7e71e6Merge pull request #210 from shopware/fix/oauth-fixed-verification-time-backport-66
2 files changed · +20 −2
src/Core/Framework/Api/OAuth/ClientRepository.php+12 −1 modified@@ -15,6 +15,11 @@ #[Package('framework')] class ClientRepository implements ClientRepositoryInterface { + /** + * Bcrypt hash for a static dummy secret used to equalize timing when no client is found. + */ + private const DUMMY_CLIENT_SECRET_HASH = '$2y$12$PVcA5R6ri9kS.7FnFUBRIOLwqU//bCicx5RFxwecAAccbmZ7V7PKu'; + /** * @internal */ @@ -34,8 +39,11 @@ public function validateClient($clientIdentifier, $clientSecret, $grantType): bo } $values = $this->getByAccessKey($clientIdentifier); + if (!$values) { - return false; + // Prevent client enumeration via timing attacks by always running password_verify(). + $values = ['secret_access_key' => self::DUMMY_CLIENT_SECRET_HASH]; + $clientSecret = 'invalid-secret-will-always-fail'; } if (!password_verify($clientSecret, (string) $values['secret_access_key'])) { @@ -67,6 +75,9 @@ public function getClientEntity($clientIdentifier): ?ClientEntityInterface $values = $this->getByAccessKey($clientIdentifier); if (!$values) { + // Prevent client enumeration via timing attacks by always running password_verify(). + password_verify('invalid-secret-will-always-fail', self::DUMMY_CLIENT_SECRET_HASH); + return null; }
src/Core/Framework/Api/OAuth/UserRepository.php+8 −1 modified@@ -13,6 +13,11 @@ #[Package('framework')] class UserRepository implements UserRepositoryInterface { + /** + * Bcrypt hash for a static dummy password used to equalize timing when no user is found. + */ + private const DUMMY_PASSWORD_HASH = '$2y$12$PVcA5R6ri9kS.7FnFUBRIOLwqU//bCicx5RFxwecAAccbmZ7V7PKu'; + /** * @internal */ @@ -42,7 +47,9 @@ public function getUserEntityByUserCredentials( ->fetchAssociative(); if (!$user) { - return null; + // Prevent user enumeration via timing attacks by always running password_verify(). + $user = ['password' => self::DUMMY_PASSWORD_HASH]; + $password = 'invalid-password-will-always-fail'; } if (!password_verify($password, (string) $user['password'])) {
Vulnerability mechanics
Root cause
"The UserController::upsertUser function does not filter the admin field when writing user data in SYSTEM_SCOPE."
Attack vector
An attacker with low-privilege API user credentials and the `user:create` or `user:update` ACL permission can exploit this vulnerability. By sending a request to the `upsertUser` endpoint with `admin: true` in the payload, the attacker can either create a new user with administrative privileges or promote an existing user to administrator. This bypasses the `AclWriteValidator` because the operation occurs within `SYSTEM_SCOPE` [ref_id=1].
Affected code
The vulnerability lies within the `upsertUser` method in `src/Core/Framework/Api/Controller/UserController.php`, specifically between lines 210 and 234. This method processes raw request data without filtering the `admin` field and operates within `SYSTEM_SCOPE`, which bypasses the `AclWriteValidator` [ref_id=1].
What the fix does
The suggested fix is to implement the same access control check found in `IntegrationController::upsertIntegration()`. This involves retrieving the source context and checking if it is an `AdminApiSource` and if the user is an administrator. If the source is not an admin source and the `admin` field is present in the data, a `PermissionDeniedException` is thrown, preventing unauthorized privilege escalation [ref_id=2].
Preconditions
- authAttacker must have API user credentials with `user:create` or `user:update` ACL permission.
Generated on Jun 4, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
1- Shopware: Nine Vulnerabilities Disclosed, Including Privilege Escalation and XSSVypr Intelligence · Jun 4, 2026